---
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);
  },
};
```

:::info 注
当前版本还未支持 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。
