UNPKG

rerumaccusamus

Version:

The meta-framework suite designed from scratch for frontend-focused modern web development.

275 lines (225 loc) 8.5 kB
--- 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 按钮后,可以看到按钮置灰: ![display](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/11/display.png) 接下来点击顶部导航,切换到 Archives 列表,我们预期的时候能看到列表里显示刚才归档的联系人,但实际上列表是空的: ![display7](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/11/display7.png) 出现这个问题的原因是,我们继续沿用了上一节的 `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 列表能正常显示了: ![display1](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/11/display1.png) :::note 注 useModel API 还可以设置 Selector,只连接这个 Model 定义的状态中的局部。 ::: --- > 本小节的代码可以在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/tutorials/c11/hello-modern)。