rerumaccusamus
Version:
The meta-framework suite designed from scratch for frontend-focused modern web development.
275 lines (225 loc) • 8.5 kB
Markdown
---
title: 完整使用 Model
---
上一章节中,我们初步引入**客户端应用架构**,从【 视图组件 】中拆分出【 业务模型(Model)】,`AllContacts` 中不再包含 UI 无关的业务逻辑实现细节,只需要使用 Model,就能实现同样的功能。
这一章节中,我们要进一步利用 Model 中实现的业务逻辑,让 `AllContacts` 和 `ArchivedContacts` 都从 BFF 获取数据,实现 Archive 按钮,点击按钮能把联系人归档,只显示在 Achives 列表里,不显示在 All 列表里。
先改造 `Item` 组件,增加 Archive 按钮的交互实现:
```tsx title="src/contacts/components/Item/index.tsx"
import Avatar from '../Avatar';
type InfoProps = {
avatar: string;
name: string;
email: string;
archived?: boolean;
};
const Item = ({
info,
onArchive,
}: {
info: InfoProps;
onArchive?: () => void;
}) => {
const { avatar, name, email, archived } = info;
return (
<div className="flex p-4 items-center border-gray-200 border-b">
<Avatar src={avatar} />
<div className="ml-4 custom-text-gray flex-1 flex justify-between">
<div className="flex-1">
<p>{name}</p>
<p>{email}</p>
</div>
<button
type="button"
disabled={archived}
onClick={onArchive}
className={`text-white font-bold py-2 px-4 rounded-full ${
archived
? 'bg-gray-400 cursor-default'
: 'bg-blue-500 hover:bg-blue-700'
}`}>
{archived ? 'Archived' : 'Archive'}
</button>
</div>
</div>
);
};
export default Item;
```
`ArchivedContacts` 和 `AllContacts` 需要共用同一套状态(联系人列表数据、联系人是否被归档),并且由于 Achives 列表和 All 列表都可能是第一屏页面(从不同 URL 访问),这两个组件都需要包含加载初始数据的逻辑(如果客户端没有联系人列表数据,就请求 BFF),所以这类两个组件公用的实现逻辑应该合并到一起:
我们删除原有的两个组件,创建一个新的 `Contacts` 组件:
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs>
<TabItem value="macOS" label="macOS" default>
```bash
rm -r src/contacts/components/*Contacts
mkdir -p src/contacts/components/Contacts/
touch src/contacts/components/Contacts/index.tsx
```
</TabItem>
<TabItem value="Windows" label="Windows">
```powershell
rm -r src/contacts/components/*Contacts
mkdir -p src/contacts/components/Contacts/
ni src/contacts/components/Contacts/index.tsx
```
</TabItem>
</Tabs>
修改`components/Contacts/index.tsx` ,内容如下:
```tsx title="src/contacts/components/Contacts/index.tsx"
import { useEffect } from 'react';
import { useLocalModel } from '@modern-js/runtime/model';
import { List } from 'antd';
import contacts from '../../models/contacts';
import Item from '../Item';
const Contacts = ({ source }: { source: 'archived' | 'items' }) => {
const [state, actions] = useLocalModel(contacts);
const { items, error, pending } = state;
useEffect(() => {
if (!items.length && !error && !pending) {
actions.load();
}
});
const data = state.items.filter(item =>
source === 'archived' ? item.archived : true,
);
return (
(items.length && (
<List
dataSource={data}
renderItem={info => (
<Item
key={info.email}
info={info}
onArchive={() => {
actions.archive(info.email);
}}
/>
)}
/>
)) || (
<div className="p-4 items-center border-gray-200 border-b border-t custom-text-gray">
Pending...
</div>
)
);
};
export default Contacts;
```
:::note 注
由于 computed 功能还未提供,这里先在组件里将传入的数据做预处理。
:::
最后改造 `App.tsx`,利用 Contacts 实现 Archives 列表和 All 列表:
```tsx title="src/contacts/App.tsx"
import { useState } from 'react';
import { Radio, RadioChangeEvent } from 'antd';
import { Route, useHistory } from '@modern-js/runtime/router';
import { Helmet } from '@modern-js/runtime/head';
import 'tailwindcss/base.css';
import 'tailwindcss/components.css';
import 'tailwindcss/utilities.css';
import './styles/utils.css';
import Contacts from './components/Contacts';
function App() {
const history = useHistory();
const [currentList, setList] = useState(history.location.pathname || '/');
const handleSetList = (e: RadioChangeEvent) => {
const { value } = e.target;
setList(value);
history.push(value);
};
return (
<div className="container lg mx-auto">
<div className="h-16 p-2 flex items-center justify-center">
<Radio.Group onChange={handleSetList} value={currentList}>
<Radio value="/">All</Radio>
<Radio value="/archives">Archives</Radio>
</Radio.Group>
</div>
<Route path="/" exact={true}>
<Helmet>
<title>All</title>
</Helmet>
<Contacts source="items" />
</Route>
<Route path="/archives" exact={true}>
<Helmet>
<title>Archives</title>
</Helmet>
<Contacts source="archived" />
</Route>
</div>
);
}
export default App;
```
执行 `pnpm run dev`,访问 `http://localhost:8080/contacts/`,点击 Archive 按钮后,可以看到按钮置灰:

接下来点击顶部导航,切换到 Archives 列表,我们预期的时候能看到列表里显示刚才归档的联系人,但实际上列表是空的:

出现这个问题的原因是,我们继续沿用了上一节的 `useLocalModel` API 来使用 Model,状态被保存到了组件内部的 state 里,而 `All` 列表和 `Archives` 列表中分别调用的 `Contacts` 组件,是两个各自独立的组件:
```tsx title="src/contacts/App.tsx"
<Route path="/" exact={true}>
<Helmet>
<title>All</title>
</Helmet>
<Contacts source="items" />
</Route>
<Route path="/archives" exact={true}>
<Helmet>
<title>Archives</title>
</Helmet>
<Contacts source="archived" />
</Route>
```
所以它们有各自独立的内部 state,互相不共享状态,渲染 `Archives` 列表的时候,`items` 仍然是初始状态。
要解决这个问题,一种方式把 `useLocalModel` 的逻辑提升到父组件里,把状态分别传给两个 `Contacts` 组件。更清晰、完善的方式,是启用全局唯一的「 应用状态 」,两个 `Contacts` 组件「 连接 」应用状态。
在 Modern.js 里实现应用状态管理很简单,只需要把 `useLocalModel` 换成 `useModel`。
修改 `components/Contacts/index.tsx` 的内容:
```tsx title="src/contacts/components/Contacts/index.tsx"
import { useEffect } from 'react';
import { useModel } from '@modern-js/runtime/model';
import { List } from 'antd';
import contacts from '../../models/contacts';
import Item from '../Item';
const Contacts = ({ source }: { source: 'archived' | 'items' }) => {
const [state, actions] = useModel(contacts);
const { items, error, pending } = state;
useEffect(() => {
if (!items.length && !error && !pending) {
actions.load();
}
});
const data = state.items.filter(item =>
source === 'archived' ? item.archived : true,
);
return (
(items.length && (
<List
dataSource={data}
renderItem={info => (
<Item
key={info.email}
info={info}
onArchive={() => {
actions.archive(info.email);
}}
/>
)}
/>
)) || (
<div className="p-4 items-center border-gray-200 border-b border-t custom-text-gray">
Pending...
</div>
)
);
};
export default Contacts;
```
重新执行 `pnpm run dev`,重复刚才的操作,可以看到 Archives 列表能正常显示了:

:::note 注
useModel API 还可以设置 Selector,只连接这个 Model 定义的状态中的局部。
:::
---
> 本小节的代码可以在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/tutorials/c11/hello-modern)。