UNPKG

rerumaccusamus

Version:

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

186 lines (149 loc) 5.82 kB
--- title: 实现 Model --- 创建一个完整的 Model 首先需要定义**状态(state)**,包括状态中数据的名称和初始值。 我们使用 Model 来管理联系人列表的数据,因此定义如下数据状态: ```js const state = { items: [], }; ``` 使用 TS 语法,可以定义更完整的类型信息,比如 items 里每个对象都应该有 `name`、`email` 字段; 为了实现归档功能,还需要创建 `archived` 字段保存这个联系人是否已被归档的状态; 我们还需要一个字段用来访问所有已归档的联系人,可以定义 **computed** 类型的字段,对已有的数据做转换: ```js const computed = { archived: ({ items }) => { return items.filter(item => item.archived); }, }; ``` :::note 当前版本还未支持 computed,本章节后续部分会先使用其他方式实现 archived 列表,这里只做介绍。 ::: computed 类型字段的定义方式是函数,但使用时可以像普通字段一样通过 state 访问。 Modern.js 支持的 Model 模块跟 React 组件一样,是基于 FP(Functional Programming)而不是 OOP(Object-Oriented Programming)的,对状态的管理是基于**不可变数据**的,不会修改状态中的数据,只会从一个状态转移到另一个状态,这样的好处很多,比如保障程序的可靠性、方便调试、方便记录和还原状态等。 由于 JS 没有原生支持不可变数据,为了提高编写这种代码的效率,Modern.js 集成了 [Immer](https://immerjs.github.io/immer/),能够像操作 JS 中常规的可变数据一样,去写这种状态转移的逻辑。 实现 Archive 按钮时,我们需要一个 `archive` 函数,负责修改指定联系人的 `archived` 字段,我们把这种函数都叫作 **action**: ```js const actions = { archive(draft, payload) { const target = draft.items.find(item => item.email === payload); if (target) { target.archived = true; } }, }; ``` action 函数是一种**纯函数**,确定的输入得到确定的输出(转移后的状态),不应该有任何副作用。 函数的第一个参数是 Immer 提供的 Draft State,第二个参数是 action 被调用时传入的参数(后面会介绍怎么调用)。 Model 里也可以定义 Side Effect,比如我们需要从 BFF 加载这个联系人列表的数据,这段业务逻辑可以写成: ```js const effects = { async load() { const { data } = await concats(); return data; }, }; ``` 一个 Side Effect 有多种实现方式,上面使用的是 Async 函数方式,这种方式是最简单直观的。Modern.js 会根据它返回的 Promise 对象的状态变化,自动触发不同的 action。 因此一个 effect 总共有三个 action,命名里会用 Side Effect 的名称作为命名空间,在这个例子里,分别是: - `load.pending`:等待中 - `load.fulfilled`:成功,得到结果 - `load.rejected`:失败,得到错误信息 Modern.js 虽然会自动定义和触发这些 action,但默认不会为这些 action 实现具体的业务逻辑(action 直接返回原本的状态,不做任何转换)。 我们尝试自己实现它们: ```js import _ from 'lodash'; const state = { items: [], pending: false, error: null, }; const computed = { archived: ({ items }) => { return items.filter(item => item.archived); }, }; const actions = { archive(draft, payload) { const target = draft.items.find(item => item.email === payload); if (target) { target.archived = true; } }, load: { pending(draft) { draft.pending = true; }, fulfilled(draft, payload) { draft.pending = false; _.merge(draft.items, payload); }, rejected(draft, payload) { draft.pending = false; draft.error = payload; }, }, }; ``` 上述实现里,成功时,payload promise 的结果;失败时,payload 是错误信息。 从上面这个例子里可以看到,可以用嵌套的写法,实现 `load.pending` 这样名称中包含命名空间的 action。 为了做到高内聚低耦合,一个 Model state、action、side effect 不应该分散在不同文件里。接下来我们把上面的代码连起来,放在同一个 Model 文件里,执行以下命令: ```bash mkdir -p src/contacts/models/ touch src/contacts/models/contacts.ts ``` `src/contacts/models/contacts.ts` 的内容: ```tsx import { model } from '@modern-js/runtime/model'; import { get as concats } from '@api/contacts'; type State = { items: { avatar: string; name: string; email: string; archived?: boolean; }[]; pending: boolean; error: null | Error; }; export default model<State>('contacts').define({ state: { items: [], pending: false, error: null, }, computed: { archived: ({ items }: State) => items.filter(item => item.archived), }, actions: { archive(draft, payload) { const target = draft.items.find(item => item.email === payload)!; if (target) { target.archived = true; } }, load: { pending(draft) { draft.pending = true; }, rejected(draft, payload) { draft.pending = false; draft.error = payload; }, fulfilled(draft, p) { draft.items = p; }, }, }, effects: { async load() { const { data } = await concats(); return data; }, }, }); ``` 我们把一个包含 state,action 等要素的 plain object 称作【 Model Spec 】,Modern.js 提供了 [Model API](/docs/apis/runtime/model/model_),可以根据 Model Spec 生成【 Model 】。 本小节中,我们联系人列表项目需要的 Model 实现。下一小节我们将会学习如何使用 Model。