@modern-js/main-doc 2.0.0-beta.3 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/en/docusaurus-plugin-content-docs/current/apis/app/commands/inspect.md +0 -4
- package/en/docusaurus-plugin-content-docs/current/components/init-app.md +42 -0
- package/en/docusaurus-plugin-content-docs/current/configure/app/server/routes.md +2 -4
- package/en/docusaurus-plugin-content-docs/current/configure/app/tools/esbuild.md +16 -39
- package/en/docusaurus-plugin-content-docs/current/guides/basic-features/css/css-in-js.md +38 -0
- package/en/docusaurus-plugin-content-docs/current/guides/basic-features/css/css-modules.md +86 -0
- package/en/docusaurus-plugin-content-docs/current/guides/basic-features/css/less-sass.md +17 -0
- package/en/docusaurus-plugin-content-docs/current/guides/basic-features/css/postcss.md +81 -0
- package/en/docusaurus-plugin-content-docs/current/guides/basic-features/css/tailwindcss.md +95 -0
- package/en/docusaurus-plugin-content-docs/current/guides/basic-features/data-fetch.md +66 -0
- package/en/docusaurus-plugin-content-docs/current/guides/basic-features/routes.md +270 -0
- package/en/docusaurus-plugin-content-docs/current/guides/concept/entries.md +116 -0
- package/en/docusaurus-plugin-content-docs/current/guides/concept/lifecycle.md +15 -0
- package/en/docusaurus-plugin-content-docs/current/guides/get-started/quick-start.md +162 -0
- package/en/docusaurus-plugin-content-docs/current/guides/get-started/upgrade.md +78 -0
- package/{zh/tutorials/first-app → en/docusaurus-plugin-content-docs/current/guides}/overview.md +4 -4
- package/en/docusaurus-plugin-content-docs/current/tutorials/foundations/introduction.md +1 -1
- package/en/docusaurus-plugin-content-docs/current.json +11 -11
- package/package.json +3 -3
- package/zh/apis/app/commands/inspect.md +0 -4
- package/zh/apis/app/commands/new.md +1 -1
- package/zh/apis/app/hooks/src/index_.md +6 -5
- package/zh/components/debug-app.md +18 -0
- package/zh/components/global-proxy.md +28 -0
- package/zh/components/init-app.md +44 -0
- package/zh/components/prerequisites.md +19 -0
- package/zh/configure/app/server/routes.md +2 -4
- package/zh/configure/app/tools/esbuild.md +16 -39
- package/zh/guides/advanced-features/bff/bff-proxy.md +1 -1
- package/zh/guides/advanced-features/compatibility.md +2 -38
- package/zh/guides/advanced-features/custom-app.md +15 -17
- package/zh/guides/advanced-features/ssg.md +6 -6
- package/zh/guides/advanced-features/ssr.md +94 -51
- package/zh/guides/advanced-features/testing.md +33 -1
- package/zh/guides/advanced-features/web-server.md +2 -2
- package/zh/guides/basic-features/css/tailwindcss.md +2 -6
- package/zh/guides/basic-features/html.md +182 -0
- package/zh/guides/basic-features/mock.md +3 -9
- package/zh/guides/basic-features/proxy.md +2 -27
- package/zh/guides/concept/entries.md +4 -5
- package/zh/guides/get-started/quick-start.md +6 -78
- package/zh/guides/get-started/upgrade.md +8 -8
- package/zh/guides/topic-detail/model/quick-start.md +1 -1
- package/zh/guides/topic-detail/model/test-model.md +2 -2
- package/zh/guides/topic-detail/monorepo/intro.md +1 -1
- package/zh/guides/troubleshooting/dependencies.md +0 -69
- package/zh/tutorials/first-app/_category_.json +1 -1
- package/zh/tutorials/first-app/c01-start.md +94 -0
- package/zh/tutorials/first-app/{c05-component/5.1-use-ui-library.md → c02-component.md} +13 -15
- package/zh/tutorials/first-app/c03-css.md +305 -0
- package/zh/tutorials/first-app/{c08-client-side-routing/8.1-code-based-routing.md → c04-routes.md} +52 -39
- package/zh/tutorials/first-app/c05-loader.md +82 -0
- package/zh/tutorials/first-app/c06-model.md +256 -0
- package/zh/tutorials/first-app/c07-container.md +268 -0
- package/zh/tutorials/first-app/c08-entries.md +134 -0
- package/zh/tutorials/foundations/introduction.md +1 -1
- package/en/docusaurus-plugin-content-docs/current/configure/app/output/enable-modern-mode.md +0 -34
- package/zh/apis/generator/overview.md +0 -32
- package/zh/configure/app/output/enable-modern-mode.md +0 -34
- package/zh/guides/topic-detail/monorepo/deploy.md +0 -43
- package/zh/tutorials/first-app/c01-getting-started/1.1-prerequisites.md +0 -25
- package/zh/tutorials/first-app/c01-getting-started/1.2-minimal-mwa.md +0 -118
- package/zh/tutorials/first-app/c01-getting-started/1.3-dev-command.md +0 -29
- package/zh/tutorials/first-app/c01-getting-started/1.4-enable-ssr.md +0 -47
- package/zh/tutorials/first-app/c01-getting-started/1.5-start-command.md +0 -18
- package/zh/tutorials/first-app/c01-getting-started/1.6-create-repo.md +0 -31
- package/zh/tutorials/first-app/c01-getting-started/_category_.json +0 -3
- package/zh/tutorials/first-app/c02-generator-and-studio/2.1-generator.md +0 -79
- package/zh/tutorials/first-app/c02-generator-and-studio/2.2-boilerplates.md +0 -34
- package/zh/tutorials/first-app/c02-generator-and-studio/2.3-configuration.md +0 -19
- package/zh/tutorials/first-app/c02-generator-and-studio/_category_.json +0 -3
- package/zh/tutorials/first-app/c03-ide/3.1-setting-up.md +0 -55
- package/zh/tutorials/first-app/c03-ide/3.2-hints-in-ide.md +0 -60
- package/zh/tutorials/first-app/c03-ide/3.3-autofix-in-ide.md +0 -11
- package/zh/tutorials/first-app/c03-ide/3.4-autofix-in-cli.md +0 -63
- package/zh/tutorials/first-app/c03-ide/_category_.json +0 -3
- package/zh/tutorials/first-app/c04-es6-plus-and-ts/4.1-use-es6-plus.md +0 -54
- package/zh/tutorials/first-app/c04-es6-plus-and-ts/4.2-use-typescript.md +0 -135
- package/zh/tutorials/first-app/c04-es6-plus-and-ts/4.3-compatibility.md +0 -67
- package/zh/tutorials/first-app/c04-es6-plus-and-ts/_category_.json +0 -3
- package/zh/tutorials/first-app/c05-component/5.2-use-standalone-component.md +0 -72
- package/zh/tutorials/first-app/c05-component/_category_.json +0 -3
- package/zh/tutorials/first-app/c06-css-and-component/6.1-css-in-js.md +0 -110
- package/zh/tutorials/first-app/c06-css-and-component/6.2-utility-class.md +0 -143
- package/zh/tutorials/first-app/c06-css-and-component/6.3-postcss.md +0 -84
- package/zh/tutorials/first-app/c06-css-and-component/6.4-design-system.md +0 -83
- package/zh/tutorials/first-app/c06-css-and-component/6.5-storybook.md +0 -77
- package/zh/tutorials/first-app/c06-css-and-component/6.6-testing.md +0 -104
- package/zh/tutorials/first-app/c06-css-and-component/_category_.json +0 -3
- package/zh/tutorials/first-app/c07-app-entry/7.1-intro.md +0 -69
- package/zh/tutorials/first-app/c07-app-entry/7.2-add-entry-in-cli.md +0 -100
- package/zh/tutorials/first-app/c07-app-entry/7.3-manage-entries-by-hand.md +0 -69
- package/zh/tutorials/first-app/c07-app-entry/_category_.json +0 -3
- package/zh/tutorials/first-app/c08-client-side-routing/_category_.json +0 -3
- package/zh/tutorials/first-app/c09-bff/9.1-serverless.md +0 -30
- package/zh/tutorials/first-app/c09-bff/9.2-enable-bff.md +0 -95
- package/zh/tutorials/first-app/c09-bff/9.3-fetch-bff.md +0 -131
- package/zh/tutorials/first-app/c09-bff/_category_.json +0 -3
- package/zh/tutorials/first-app/c10-model/10.1-application-architecture.md +0 -21
- package/zh/tutorials/first-app/c10-model/10.2-add-model.md +0 -185
- package/zh/tutorials/first-app/c10-model/10.3-use-model.md +0 -55
- package/zh/tutorials/first-app/c10-model/10.4-testing.md +0 -69
- package/zh/tutorials/first-app/c10-model/_category_.json +0 -3
- package/zh/tutorials/first-app/c11-container/11.1-use-model-with-app-state.md +0 -240
- package/zh/tutorials/first-app/c11-container/11.2-add-container.md +0 -109
- package/zh/tutorials/first-app/c11-container/11.3-use-loader.md +0 -63
- package/zh/tutorials/first-app/c11-container/11.4-testing.md +0 -56
- package/zh/tutorials/first-app/c11-container/_category_.json +0 -3
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 启用 BFF
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
和之前章节一样,我们同样使用生成器来启用 BFF。
|
|
6
|
-
|
|
7
|
-
在项目根目录下执行 `pnpm run new` 命令:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
# 启用可选功能
|
|
11
|
-
❯ 启用「BFF」功能
|
|
12
|
-
...
|
|
13
|
-
|
|
14
|
-
# 请选择 BFF 类型
|
|
15
|
-
❯ 函数写法
|
|
16
|
-
框架写法
|
|
17
|
-
|
|
18
|
-
# 请选择运行时框
|
|
19
|
-
❯ Express
|
|
20
|
-
Koa
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
:::info 注
|
|
24
|
-
函数模式与框架模式都是基于 Modern.js 的 BFF 函数的,两种模式的主体都是函数,也都需要运行时框架的支持,这里的提问是为了提供不同的样板文件。更多相关内容可以查看[一体化 BFF 专题](/docs/guides/advanced-features/bff/function)。
|
|
25
|
-
:::
|
|
26
|
-
|
|
27
|
-
我们先选择【 函数写法 】,并选择运行时框架为 Express,项目结构将变成:
|
|
28
|
-
|
|
29
|
-
```md
|
|
30
|
-
.
|
|
31
|
-
├── .eslintrc.js
|
|
32
|
-
├── .gitignore
|
|
33
|
-
├── .husky
|
|
34
|
-
├── .npmrc
|
|
35
|
-
├── .nvmrc
|
|
36
|
-
├── .prettierrc
|
|
37
|
-
├── .vscode
|
|
38
|
-
├── README.md
|
|
39
|
-
├── api
|
|
40
|
-
│ ├── _app.ts
|
|
41
|
-
│ ├── index.ts
|
|
42
|
-
│ └── info
|
|
43
|
-
│ └── [type].ts
|
|
44
|
-
├── modern.config.ts
|
|
45
|
-
├── package.json
|
|
46
|
-
├── pnpm-lock.yaml
|
|
47
|
-
├── src
|
|
48
|
-
│ ├── .eslintrc.js
|
|
49
|
-
│ ├── contacts
|
|
50
|
-
│ │ ├── index.test.ts
|
|
51
|
-
│ │ └── routes
|
|
52
|
-
│ │ ├── archives
|
|
53
|
-
│ │ │ └── page.tsx
|
|
54
|
-
│ │ ├── components
|
|
55
|
-
│ │ │ ├── Avatar
|
|
56
|
-
│ │ │ │ ├── index.stories.tsx
|
|
57
|
-
│ │ │ │ └── index.tsx
|
|
58
|
-
│ │ │ └── Item
|
|
59
|
-
│ │ │ ├── index.test.tsx
|
|
60
|
-
│ │ │ └── index.tsx
|
|
61
|
-
│ │ ├── index.css
|
|
62
|
-
│ │ ├── layout.tsx
|
|
63
|
-
│ │ ├── page.tsx
|
|
64
|
-
│ │ └── styles
|
|
65
|
-
│ │ └── utils.css
|
|
66
|
-
│ ├── landing-page
|
|
67
|
-
│ │ └── routes
|
|
68
|
-
│ │ ├── index.css
|
|
69
|
-
│ │ ├── layout.tsx
|
|
70
|
-
│ │ └── page.tsx
|
|
71
|
-
│ └── modern-app-env.d.ts
|
|
72
|
-
└── tsconfig.json
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
新增的 `api/` 目录,跟 `src/` 目录一样是源代码目录,目录中有几个示范用法的样板文件
|
|
76
|
-
|
|
77
|
-
`package.json` 中则会增加 BFF 插件:
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
+ "@modern-js/plugin-bff"
|
|
81
|
-
+ "@modern-js/plugin-express"
|
|
82
|
-
+ "express"
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
在函数写法中,BFF 的 API 路由也遵循类似【 约定式路由 】的生成逻辑。
|
|
86
|
-
|
|
87
|
-
Modern.js Serverless BFF 的默认前缀为 `/api`,例如上述目录结构可以得到两个 API 路由:
|
|
88
|
-
|
|
89
|
-
`http://localhost:8080/api`
|
|
90
|
-
|
|
91
|
-
`http://localhost:8080/api/info/:type`
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
> 本小节的代码可以在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/tutorials/c09/hello-modern-2)。
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 从 BFF 获取数据
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
上一小节中,我们创建了【 函数写法 】的 BFF,这一小节中,我们将用该模式为联系人列表实现一个数据接口。
|
|
6
|
-
|
|
7
|
-
我们首先把不需要的样板文件清理掉,把 `index.ts` 重命名为 `contacts.ts`,执行以下命令:
|
|
8
|
-
|
|
9
|
-
import Tabs from '@theme/Tabs';
|
|
10
|
-
import TabItem from '@theme/TabItem';
|
|
11
|
-
|
|
12
|
-
<Tabs>
|
|
13
|
-
<TabItem value="macOS" label="macOS" default>
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
rm -r api/info api/_app.ts
|
|
17
|
-
mv api/index.ts api/contacts.ts
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
</TabItem>
|
|
21
|
-
<TabItem value="Windows" label="Windows">
|
|
22
|
-
|
|
23
|
-
```powershell
|
|
24
|
-
rm -r api/info
|
|
25
|
-
rm api/_app.ts
|
|
26
|
-
mv api/index.ts api/contacts.ts
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
</TabItem>
|
|
30
|
-
</Tabs>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
此时 API 路由将变为 `/api/contacts`。
|
|
34
|
-
|
|
35
|
-
我们使用 [faker](https://github.com/Marak/Faker.js) 来 mock 需要的数据,首先安装依赖:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
pnpm add faker@5.5.3
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
将 `api/contacts.ts` 内容改为:
|
|
42
|
-
|
|
43
|
-
```js
|
|
44
|
-
import { name, internet } from 'faker';
|
|
45
|
-
|
|
46
|
-
export const get = async () => {
|
|
47
|
-
const mockData = new Array(20).fill(0).map(() => {
|
|
48
|
-
const firstName = name.firstName();
|
|
49
|
-
return {
|
|
50
|
-
name: firstName,
|
|
51
|
-
avatar: `https://avatars.dicebear.com/api/identicon/${firstName}.svg`,
|
|
52
|
-
email: internet.email(),
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return { code: 200, data: mockData };
|
|
57
|
-
};
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
:::info 注
|
|
61
|
-
数据同样可以从远端接口获取,此处仅为了演示。
|
|
62
|
-
:::
|
|
63
|
-
|
|
64
|
-
执行 `pnpm run dev`,访问 `http://localhost:8080/api/contacts`:
|
|
65
|
-
|
|
66
|
-

|
|
67
|
-
|
|
68
|
-
接下来我们把 `src/routes/contacts/page.tsx` 里硬编码的 `getMockData` 改成从 BFF 加载:
|
|
69
|
-
|
|
70
|
-
```tsx
|
|
71
|
-
import { Helmet } from '@modern-js/runtime/head';
|
|
72
|
-
import { List } from 'antd';
|
|
73
|
-
import 'ladda/dist/ladda.min.css';
|
|
74
|
-
import 'tailwindcss/base.css';
|
|
75
|
-
import 'tailwindcss/components.css';
|
|
76
|
-
import 'tailwindcss/utilities.css';
|
|
77
|
-
import './styles/utils.css';
|
|
78
|
-
|
|
79
|
-
import { useState, useEffect } from 'react';
|
|
80
|
-
import { get as contacts } from '@api/contacts';
|
|
81
|
-
import Item from './components/Item';
|
|
82
|
-
|
|
83
|
-
function Index() {
|
|
84
|
-
const [list, setList] = useState(
|
|
85
|
-
[] as Array<{ name: string; email: string; avatar: string }>,
|
|
86
|
-
);
|
|
87
|
-
const loadMockData = async () => {
|
|
88
|
-
const { data } = await contacts();
|
|
89
|
-
setList(data);
|
|
90
|
-
};
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
if (!list.length) {
|
|
93
|
-
loadMockData();
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
return (
|
|
97
|
-
<div className="container lg mx-auto">
|
|
98
|
-
<Helmet>
|
|
99
|
-
<title>All</title>
|
|
100
|
-
</Helmet>
|
|
101
|
-
{(list.length && (
|
|
102
|
-
<List
|
|
103
|
-
dataSource={list}
|
|
104
|
-
renderItem={info => <Item key={info.name} info={info} />}
|
|
105
|
-
/>
|
|
106
|
-
)) || (
|
|
107
|
-
<div className="p-4 items-center border-gray-200 border-b border-t custom-text-gray">
|
|
108
|
-
Pending...
|
|
109
|
-
</div>
|
|
110
|
-
)}
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export default Index;
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
在 Modern.js 中,可以通过调用函数(前后端一体化)的方式,直接调用 BFF 接口,调用时无需关心域名、路径等。
|
|
119
|
-
|
|
120
|
-
:::info 注
|
|
121
|
-
在使用 `pnpm run new` 创建 BFF 时,Modern.js 已经默认在 `tsconig.json` 中注入了别名。这也是之前提到的,生成器在项目创建之后并不会被抛弃,仍旧可以在开发过程中不断为项目提供新的内容。
|
|
122
|
-
:::
|
|
123
|
-
|
|
124
|
-
执行 `pnpm run dev`,再打开页面`http://localhost:8000/contacts`,可以看到页面加载后,会先初始化联系人数据(显示 pending),之后每次切换到 All 列表,也会重新请求联系人数据:
|
|
125
|
-
|
|
126
|
-

|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
> 本小节的代码可以在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/tutorials/c09/hello-modern-3)。
|
|
131
|
-
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 应用架构
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
上一章节中,我们把硬编码的 `mockData` 改成从 BFF 加载,在 `pages.tsx` 组件里用 BFF 函数,获取到联系人数据之后,保存在 `Index` 的组件内部状态里,而 `archives/index` 组件暂时继续使用 mock 数据。
|
|
6
|
-
|
|
7
|
-
本章节中,为了进一步实现项目功能,我们需要让两个组件都从 BFF 获取数据,还要实现【 Archive 】按钮,点击按钮能把联系人归档。
|
|
8
|
-
|
|
9
|
-
业务逻辑变复杂之后,相关代码不可避免会变多,如果都写在组件里,都会让这个组件的可读性和可维护性变差,让做不同事情的代码混在一起。
|
|
10
|
-
|
|
11
|
-
原本可以跟 UI 完全解耦的业务逻辑(比如网络请求、纯数据的操作、状态的管理等)跟 UI 逻辑(比如联系人列表如何展现)混在一起,当需要修改 UI 的时候,不得不跟 UI 无关的业务逻辑代码打交道,反过来也一样,不符合【 [单一职责原则(SRP)](https://zh.wikipedia.org/wiki/%E5%8D%95%E4%B8%80%E5%8A%9F%E8%83%BD%E5%8E%9F%E5%88%99)】,也做不到【 [关注点分离(SoC)](https://zh.wikipedia.org/wiki/%E5%85%B3%E6%B3%A8%E7%82%B9%E5%88%86%E7%A6%BB)】。
|
|
12
|
-
|
|
13
|
-
从这里开始,我们发现如果这是个真实的项目,继续这样写下去会产生越来越多面条式代码,仅凭 React 组件这一种抽象方式,不足以让代码足够结构化,随着项目不断积累业务逻辑、复杂性和变更,技术债也会积累,最终可能变成没人愿意碰的祖传代码。
|
|
14
|
-
|
|
15
|
-
像这样的项目,需要更健全的**客户端应用架构**。
|
|
16
|
-
|
|
17
|
-
Modern.js 提供开箱即用的**应用架构**,支持几种扮演不同**角色(role)**、属于不同**分层(layer)**的代码模块类型,可以把业务逻辑解耦到不同类型的代码模块里,做到高[内聚](https://zh.wikipedia.org/wiki/%E5%85%A7%E8%81%9A%E6%80%A7_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8))低[耦合](https://zh.wikipedia.org/wiki/%E8%80%A6%E5%90%88%E6%80%A7_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8))。
|
|
18
|
-
|
|
19
|
-
当前项目里 `components/` 目录中的 React 组件,是一种可以称作【 视图组件 】的代码模块,负责 UI、交互上的界面展现。
|
|
20
|
-
|
|
21
|
-
本章节我们来把 `page.tsx` 中的 Index 组件中可以跟 UI 解耦的业务逻辑,移到一种叫【 Model(业务模型)】的代码模块里。
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 实现 Model
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
创建一个完整的 Model 首先需要定义**状态(state)**,包括状态中数据的名称和初始值。
|
|
6
|
-
|
|
7
|
-
我们使用 Model 来管理联系人列表的数据,因此定义如下数据状态:
|
|
8
|
-
|
|
9
|
-
```js
|
|
10
|
-
const state = {
|
|
11
|
-
items: [],
|
|
12
|
-
};
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
使用 TS 语法,可以定义更完整的类型信息,比如 items 里每个对象都应该有 `name`、`email` 字段;
|
|
16
|
-
|
|
17
|
-
为了实现归档功能,还需要创建 `archived` 字段保存这个联系人是否已被归档的状态;
|
|
18
|
-
|
|
19
|
-
我们还需要一个字段用来访问所有已归档的联系人,可以定义 **computed** 类型的字段,对已有的数据做转换:
|
|
20
|
-
|
|
21
|
-
```js
|
|
22
|
-
const computed = {
|
|
23
|
-
archived: ({ items }) => {
|
|
24
|
-
return items.filter(item => item.archived);
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
:::info 注
|
|
30
|
-
当前版本还未支持 computed,本章节后续部分会先使用其他方式实现 archived 列表,这里只做介绍。
|
|
31
|
-
:::
|
|
32
|
-
|
|
33
|
-
computed 类型字段的定义方式是函数,但使用时可以像普通字段一样通过 state 访问。
|
|
34
|
-
|
|
35
|
-
Modern.js 支持的 Model 模块跟 React 组件一样,是基于 FP(Functional Programming)而不是 OOP(Object-Oriented Programming)的,对状态的管理是基于**不可变数据**的,不会修改状态中的数据,只会从一个状态转移到另一个状态,这样的好处很多,比如保障程序的可靠性、方便调试、方便记录和还原状态等。
|
|
36
|
-
|
|
37
|
-
由于 JS 没有原生支持不可变数据,为了提高编写这种代码的效率,Modern.js 集成了 [Immer](https://immerjs.github.io/immer/),能够像操作 JS 中常规的可变数据一样,去写这种状态转移的逻辑。
|
|
38
|
-
|
|
39
|
-
实现 Archive 按钮时,我们需要一个 `archive` 函数,负责修改指定联系人的 `archived` 字段,我们把这种函数都叫作 **action**:
|
|
40
|
-
|
|
41
|
-
```js
|
|
42
|
-
const actions = {
|
|
43
|
-
archive(draft, payload) {
|
|
44
|
-
const target = draft.items.find(item => item.email === payload);
|
|
45
|
-
if (target) {
|
|
46
|
-
target.archived = true;
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
action 函数是一种**纯函数**,确定的输入得到确定的输出(转移后的状态),不应该有任何副作用。
|
|
53
|
-
|
|
54
|
-
函数的第一个参数是 Immer 提供的 Draft State,第二个参数是 action 被调用时传入的参数(后面会介绍怎么调用)。
|
|
55
|
-
|
|
56
|
-
Model 里也可以定义 Side Effect,比如我们需要从 BFF 加载这个联系人列表的数据,这段业务逻辑可以写成:
|
|
57
|
-
|
|
58
|
-
```js
|
|
59
|
-
const effects = {
|
|
60
|
-
async load() {
|
|
61
|
-
const { data } = await contacts();
|
|
62
|
-
return data;
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
一个 Side Effect 有多种实现方式,上面使用的是 Async 函数方式,这种方式是最简单直观的。Modern.js 会根据它返回的 Promise 对象的状态变化,自动触发不同的 action。
|
|
68
|
-
|
|
69
|
-
因此一个 effect 总共有三个 action,命名里会用 Side Effect 的名称作为命名空间,在这个例子里,分别是:
|
|
70
|
-
|
|
71
|
-
- `load.pending`:等待中
|
|
72
|
-
- `load.fulfilled`:成功,得到结果
|
|
73
|
-
- `load.rejected`:失败,得到错误信息
|
|
74
|
-
|
|
75
|
-
Modern.js 虽然会自动定义和触发这些 action,但默认不会为这些 action 实现具体的业务逻辑(action 直接返回原本的状态,不做任何转换)。
|
|
76
|
-
|
|
77
|
-
我们尝试自己实现它们:
|
|
78
|
-
|
|
79
|
-
```js
|
|
80
|
-
import _ from 'lodash';
|
|
81
|
-
|
|
82
|
-
const state = {
|
|
83
|
-
items: [],
|
|
84
|
-
pending: false,
|
|
85
|
-
error: null,
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const computed = {
|
|
89
|
-
archived: ({ items }) => {
|
|
90
|
-
return items.filter(item => item.archived);
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const actions = {
|
|
95
|
-
archive(draft, payload) {
|
|
96
|
-
const target = draft.items.find(item => item.email === payload);
|
|
97
|
-
if (target) {
|
|
98
|
-
target.archived = true;
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
load: {
|
|
102
|
-
pending(draft) {
|
|
103
|
-
draft.pending = true;
|
|
104
|
-
},
|
|
105
|
-
fulfilled(draft, payload) {
|
|
106
|
-
draft.pending = false;
|
|
107
|
-
_.merge(draft.items, payload);
|
|
108
|
-
},
|
|
109
|
-
rejected(draft, payload) {
|
|
110
|
-
draft.pending = false;
|
|
111
|
-
draft.error = payload;
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
上述实现里,成功时,payload 是 promise 的结果;失败时,payload 是错误信息。
|
|
118
|
-
|
|
119
|
-
从上面这个例子里可以看到,可以用嵌套的写法,实现 `load.pending` 这样名称中包含命名空间的 action。
|
|
120
|
-
|
|
121
|
-
为了做到高内聚低耦合,一个 Model 的 state、action、side effect 不应该分散在不同文件里。接下来我们把上面的代码连起来,放在同一个 Model 文件里,执行以下命令:
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
mkdir -p src/contacts/routes/models/
|
|
125
|
-
touch src/contacts/routes/models/contacts.ts
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
`src/contacts/routes/models/contacts.ts` 的内容:
|
|
129
|
-
|
|
130
|
-
```tsx
|
|
131
|
-
import { model } from '@modern-js/runtime/model';
|
|
132
|
-
import { get as contacts } from '@api/contacts';
|
|
133
|
-
|
|
134
|
-
type State = {
|
|
135
|
-
items: {
|
|
136
|
-
avatar: string;
|
|
137
|
-
name: string;
|
|
138
|
-
email: string;
|
|
139
|
-
archived?: boolean;
|
|
140
|
-
}[];
|
|
141
|
-
pending: boolean;
|
|
142
|
-
error: null | Error;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
export default model<State>('contacts').define({
|
|
146
|
-
state: {
|
|
147
|
-
items: [],
|
|
148
|
-
pending: false,
|
|
149
|
-
error: null,
|
|
150
|
-
},
|
|
151
|
-
computed: {
|
|
152
|
-
archived: ({ items }: State) => items.filter(item => item.archived),
|
|
153
|
-
},
|
|
154
|
-
actions: {
|
|
155
|
-
archive(draft, payload) {
|
|
156
|
-
const target = draft.items.find(item => item.email === payload)!;
|
|
157
|
-
if (target) {
|
|
158
|
-
target.archived = true;
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
load: {
|
|
162
|
-
pending(draft) {
|
|
163
|
-
draft.pending = true;
|
|
164
|
-
},
|
|
165
|
-
rejected(draft, payload) {
|
|
166
|
-
draft.pending = false;
|
|
167
|
-
draft.error = payload;
|
|
168
|
-
},
|
|
169
|
-
fulfilled(draft, p) {
|
|
170
|
-
draft.items = p;
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
effects: {
|
|
175
|
-
async load() {
|
|
176
|
-
const { data } = await contacts();
|
|
177
|
-
return data;
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
我们把一个包含 state,action 等要素的 plain object 称作【 Model Spec 】,Modern.js 提供了 [Model API](/docs/apis/app/runtime/model/model_),可以根据 Model Spec 生成【 Model 】。
|
|
184
|
-
|
|
185
|
-
本小节中,我们联系人列表项目需要的 Model 实现。下一小节我们将会学习如何使用 Model。
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 使用 Model
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
上一小节中,我们实现了 Model,已经把 `page.tsx` `Index` 组件中原有的 UI 无关业务逻辑解耦出来。
|
|
6
|
-
|
|
7
|
-
这一小节中,我们使用这个 Model 直接把 `page.tsx` `Index` 组件重新还原出来,实现变得更简洁清晰:
|
|
8
|
-
|
|
9
|
-
```js title="src/contacts/routes/page.tsx"
|
|
10
|
-
import { useEffect } from 'react';
|
|
11
|
-
import { useLocalModel } from '@modern-js/runtime/model';
|
|
12
|
-
import contacts from './models/contacts';
|
|
13
|
-
import Item from './components/Item';
|
|
14
|
-
|
|
15
|
-
function Index() {
|
|
16
|
-
const [{ items, error, pending }, actions] = useLocalModel(contacts);
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
if (!items.length && !error && !pending) {
|
|
20
|
-
actions.load();
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
return (
|
|
24
|
-
<div className="container lg mx-auto">
|
|
25
|
-
{(items.length && (
|
|
26
|
-
<List
|
|
27
|
-
dataSource={items}
|
|
28
|
-
renderItem={info => <Item key={info.name} info={info} />}
|
|
29
|
-
/>
|
|
30
|
-
)) || (
|
|
31
|
-
<div className="p-4 items-center border-gray-200 border-b border-t custom-text-gray">
|
|
32
|
-
Pending...
|
|
33
|
-
</div>
|
|
34
|
-
)}
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export default Index;
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
以上代码跟上一章节中 `Index` 组件的代码等价。
|
|
43
|
-
|
|
44
|
-
`useLocalModel` 是 Modern.js 提供的 hooks API,可以使用 Model,在组件中提供 Model 中定义的 state,或通过 actions 调用 Model 中定义的 side effect 与 action,从而改变 Model 的 state。
|
|
45
|
-
|
|
46
|
-
Modern.js 的 Model 能够这样使用的原因是 Model 是基于 FP 的,不像 OOP 的 Model 那样自身持有和封装了状态,对外部提供方法访问和修改自身内部的状态。
|
|
47
|
-
|
|
48
|
-
Model 是业务逻辑,是计算过程,本身不创建也不持有状态,只有在被组件用 hooks API 使用后,才在指定的地方(比如这个例子里,是组件内部的 state)创建状态。
|
|
49
|
-
|
|
50
|
-
执行 `pnpm run dev`,可以看到跟上一章节一样的效果。
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
> 本小节的代码可以在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/tutorials/c10/hello-modern-3)。
|
|
55
|
-
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 测试 Model
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
上一小节中我们学习了如何使用 Model。
|
|
6
|
-
|
|
7
|
-
这一小节里我们来测试 Model。
|
|
8
|
-
|
|
9
|
-
跟[测试组件](../c06-css-and-component/6.6-testing.md)中一样,不需要做任何配置,可以直接给 Model 写测试用例。
|
|
10
|
-
|
|
11
|
-
以 `models/contacts` 为例,我们创建对应的 `.test` 文件:
|
|
12
|
-
|
|
13
|
-
import Tabs from '@theme/Tabs';
|
|
14
|
-
import TabItem from '@theme/TabItem';
|
|
15
|
-
|
|
16
|
-
<Tabs>
|
|
17
|
-
<TabItem value="macOS" label="macOS" default>
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
touch src/contacts/routes/models/contacts.test.ts
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
</TabItem>
|
|
24
|
-
<TabItem value="Windows" label="Windows">
|
|
25
|
-
|
|
26
|
-
```powershell
|
|
27
|
-
ni src/contacts/routes/models/contacts.test.ts
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
</TabItem>
|
|
31
|
-
</Tabs>
|
|
32
|
-
|
|
33
|
-
在测试用例中可以使用 Modern.js 提供的 Model 模拟,可以调用这个对象的方法写断言。
|
|
34
|
-
|
|
35
|
-
我们直接使用 createStore API 来编写测试文件,示例内容如下:
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
import { createStore } from '@modern-js/runtime/testing';
|
|
39
|
-
import contactsModel from './contacts';
|
|
40
|
-
|
|
41
|
-
const mockItem = {
|
|
42
|
-
avatar: '',
|
|
43
|
-
name: '李华',
|
|
44
|
-
age: '16',
|
|
45
|
-
email: 'lihua@gmail.com',
|
|
46
|
-
archived: false,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
describe('test contracts model', () => {
|
|
50
|
-
it('actions works well', async () => {
|
|
51
|
-
const store = createStore();
|
|
52
|
-
const [state, actions] = store.use(contactsModel);
|
|
53
|
-
|
|
54
|
-
state.items.push(mockItem);
|
|
55
|
-
expect(store.use(contactsModel)[0].items.length).toBe(1);
|
|
56
|
-
expect(store.use(contactsModel)[0].items[0].archived).toBeFalsy();
|
|
57
|
-
|
|
58
|
-
await actions.archive('lihua@gmail.com');
|
|
59
|
-
expect(store.use(contactsModel)[0].items[0].archived).toBeTruthy();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
执行 `pnpm run test`,可以看到测试报告。
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
> 本小节的代码可以在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/tutorials/c10/hello-modern-4)。
|
|
69
|
-
|