@modern-js/main-doc 2.0.0-canary.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/en/docusaurus-plugin-content-docs/current/apis/app/runtime/_category_.json +1 -1
  3. package/en/docusaurus-plugin-content-docs/current/apis/app/runtime/core/use-module-apps.md +62 -31
  4. package/en/docusaurus-plugin-content-docs/current/apis/app/runtime/router/router.md +174 -375
  5. package/en/docusaurus-plugin-content-docs/current/components/enable-bff.md +36 -0
  6. package/en/docusaurus-plugin-content-docs/current/components/enable-micro-frontend.md +13 -0
  7. package/en/docusaurus-plugin-content-docs/current/components/micro-master-manifest-config.md +15 -0
  8. package/en/docusaurus-plugin-content-docs/current/components/micro-runtime-config.md +18 -0
  9. package/en/docusaurus-plugin-content-docs/current/components/router-legacy-tip.md +1 -0
  10. package/en/docusaurus-plugin-content-docs/current/configure/app/auto-load-plugin.md +62 -0
  11. package/en/docusaurus-plugin-content-docs/current/configure/app/deploy/microFrontend.md +54 -0
  12. package/en/docusaurus-plugin-content-docs/current/configure/app/output/ssg.md +226 -0
  13. package/en/docusaurus-plugin-content-docs/current/configure/app/runtime/master-app.md +20 -39
  14. package/en/docusaurus-plugin-content-docs/current/configure/app/runtime/router.md +17 -4
  15. package/en/docusaurus-plugin-content-docs/current/configure/app/runtime/state.md +17 -4
  16. package/en/docusaurus-plugin-content-docs/current/configure/app/server/enable-framework-ext.md +47 -0
  17. package/en/docusaurus-plugin-content-docs/current/guides/advanced-features/bff/frameworks.md +2 -0
  18. package/en/docusaurus-plugin-content-docs/current/guides/advanced-features/bff/function.md +10 -6
  19. package/en/docusaurus-plugin-content-docs/current/guides/advanced-features/ssg.md +6 -2
  20. package/en/docusaurus-plugin-content-docs/current/guides/basic-features/css/_category_.json +4 -0
  21. package/en/docusaurus-plugin-content-docs/current/guides/basic-features/data-fetch.md +1 -1
  22. package/en/docusaurus-plugin-content-docs/current/guides/basic-features/routes.md +0 -2
  23. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/_category_.json +5 -0
  24. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/framework-plugin/extend.md +162 -0
  25. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/framework-plugin/hook-list.md +803 -0
  26. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/framework-plugin/hook.md +169 -0
  27. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/framework-plugin/implement.md +247 -0
  28. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/framework-plugin/introduction.md +49 -0
  29. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/framework-plugin/plugin-api.md +116 -0
  30. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/framework-plugin/relationship.md +118 -0
  31. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/generator/config/common.md +1 -1
  32. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/generator/config/module.md +3 -1
  33. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/generator/config/mwa.md +1 -9
  34. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/generator/project.md +2 -2
  35. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/micro-frontend/_category_.json +4 -0
  36. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/micro-frontend/c01-introduction.md +29 -0
  37. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/micro-frontend/c02-development.md +191 -0
  38. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/micro-frontend/c03-main-app.md +246 -0
  39. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/micro-frontend/c04-communicate.md +54 -0
  40. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/micro-frontend/c05-mixed-stack.md +24 -0
  41. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/_category_.json +4 -0
  42. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/auto-actions.md +90 -0
  43. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/computed-state.md +151 -0
  44. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/define-model.md +66 -0
  45. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/faq.md +43 -0
  46. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/manage-effects.md +259 -0
  47. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/model-communicate.md +219 -0
  48. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/performance.md +173 -0
  49. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/quick-start.md +116 -0
  50. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/redux-integration.md +21 -0
  51. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/test-model.md +43 -0
  52. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/typescript-best-practice.md +71 -0
  53. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/use-model.md +244 -0
  54. package/en/docusaurus-plugin-content-docs/current/guides/topic-detail/model/use-out-of-modernjs.md +51 -0
  55. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/_category_.json +5 -0
  56. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c01-start.md +99 -0
  57. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c02-component.md +56 -0
  58. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c03-css.md +324 -0
  59. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c04-routes.md +169 -0
  60. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c05-loader.md +82 -0
  61. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c06-model.md +260 -0
  62. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c07-container.md +283 -0
  63. package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c08-entries.md +137 -0
  64. package/en/docusaurus-plugin-content-docs/current/tutorials/foundations/_category_.json +1 -1
  65. package/en/docusaurus-plugin-content-docs/current/tutorials/foundations/introduction.md +5 -3
  66. package/package.json +4 -4
  67. package/zh/apis/app/runtime/core/use-module-apps.md +2 -0
  68. package/zh/apis/app/runtime/router/router.md +169 -371
  69. package/zh/components/enable-bff.md +36 -0
  70. package/zh/components/micro-master-manifest-config.md +15 -0
  71. package/zh/components/router-legacy-tip.md +1 -0
  72. package/zh/configure/app/auto-load-plugin.md +62 -0
  73. package/zh/configure/app/deploy/microFrontend.md +0 -10
  74. package/zh/configure/app/output/ssg.md +1 -5
  75. package/zh/configure/app/runtime/master-app.md +4 -18
  76. package/zh/configure/app/runtime/router.md +19 -4
  77. package/zh/configure/app/runtime/state.md +7 -7
  78. package/zh/configure/app/server/enable-framework-ext.md +47 -0
  79. package/zh/configure/app/server/port.md +1 -1
  80. package/zh/configure/app/tools/_category_.json +1 -1
  81. package/zh/guides/advanced-features/bff/frameworks.md +2 -0
  82. package/zh/guides/advanced-features/bff/function.md +7 -5
  83. package/zh/guides/advanced-features/eslint.md +2 -1
  84. package/zh/guides/advanced-features/ssg.md +4 -0
  85. package/zh/guides/basic-features/data-fetch.md +1 -1
  86. package/zh/guides/basic-features/env-vars.md +1 -1
  87. package/zh/guides/basic-features/routes.md +0 -3
  88. package/zh/guides/topic-detail/generator/config/module.md +3 -1
  89. package/zh/guides/topic-detail/generator/config/mwa.md +1 -9
  90. package/zh/guides/topic-detail/model/quick-start.md +1 -1
  91. package/zh/tutorials/first-app/c06-model.md +5 -1
  92. package/zh/tutorials/first-app/c08-entries.md +1 -1
  93. package/zh/tutorials/foundations/introduction.md +5 -3
  94. package/en/docusaurus-plugin-content-docs/current/apis/app/overview.md +0 -12
  95. package/en/docusaurus-plugin-content-docs/current/configure/app/bff/fetcher.md +0 -28
  96. package/en/docusaurus-plugin-content-docs/current/configure/app/dev/with-master-app.md +0 -31
  97. package/en/docusaurus-plugin-content-docs/current/guides/overview.md +0 -11
  98. package/en/docusaurus-plugin-content-docs/current/tutorials/foundations/basic.md +0 -8
  99. package/zh/apis/app/overview.md +0 -11
  100. package/zh/apis/monorepo/overview.md +0 -11
  101. package/zh/configure/app/bff/fetcher.md +0 -31
  102. package/zh/configure/app/dev/with-master-app.md +0 -32
  103. package/zh/guides/overview.md +0 -11
  104. package/zh/tutorials/foundations/basic.md +0 -8
@@ -0,0 +1,260 @@
1
+ ---
2
+ title: 添加业务模型(状态管理)
3
+ ---
4
+
5
+ 上一章节中,我们把硬编码的 `mockData` 改成从 Data Loader 中加载。
6
+
7
+ 这一章节中,我们会进一步实现项目的功能,例如实现 **Archive** 按钮的功能,把联系人归档。
8
+
9
+ 因此会开始编写一些跟 UI 完全无关的业务逻辑,如果继续写在组件代码中,会产生越来越多的面条式代码。为此,我们引入了一种叫做 **业务模型(Model)** 的代码模块,将这些业务逻辑和 UI 解耦。
10
+
11
+ :::info 注
12
+ 使用 Model API,需要先设置 [runtime.state](/docs/configure/app/runtime/state) 以启用状态管理插件。
13
+ :::
14
+
15
+ ## 实现 Model
16
+
17
+ 创建一个完整的 Model 首先需要定义**状态(state)**,包括状态中数据的名称和初始值。
18
+
19
+ 我们使用 Model 来管理联系人列表的数据,因此定义如下数据状态:
20
+
21
+ ```js
22
+ const state = {
23
+ items: [],
24
+ };
25
+ ```
26
+
27
+ 使用 TS 语法,可以定义更完整的类型信息,比如 items 里每个对象都应该有 `name`、`email` 字段。为了实现归档功能,还需要创建 `archived` 字段保存这个联系人是否已被归档的状态。
28
+
29
+ 我们还需要一个字段用来访问所有已归档的联系人,可以定义 **computed** 类型的字段,对已有的数据做转换:
30
+
31
+ ```js
32
+ const computed = {
33
+ archived: ({ items }) => {
34
+ return items.filter(item => item.archived);
35
+ },
36
+ };
37
+ ```
38
+
39
+ `computed` 类型字段的定义方式是函数,但使用时可以像普通字段一样通过 state 访问。
40
+
41
+ :::info
42
+ Modern.js 集成了 [Immer](https://immerjs.github.io/immer/),能够像操作 JS 中常规的可变数据一样,去写这种状态转移的逻辑。
43
+ :::
44
+
45
+ 实现 Archive 按钮时,我们需要一个 `archive` 函数,负责修改指定联系人的 `archived` 字段,我们把这种函数都叫作 **action**:
46
+
47
+ ```js
48
+ const actions = {
49
+ archive(draft, payload) {
50
+ const target = draft.items.find(item => item.email === payload);
51
+ if (target) {
52
+ target.archived = true;
53
+ }
54
+ },
55
+ };
56
+ ```
57
+
58
+ action 函数是一种**纯函数**,确定的输入得到确定的输出(转移后的状态),不应该有任何副作用。
59
+
60
+ 函数的第一个参数是 Immer 提供的 Draft State,第二个参数是 action 被调用时传入的参数(后面会介绍怎么调用)。
61
+
62
+ 我们尝试完整实现它们:
63
+
64
+ ```js
65
+ const state = {
66
+ items: [],
67
+ pending: false,
68
+ error: null,
69
+ };
70
+
71
+ const computed = {
72
+ archived: ({ items }) => {
73
+ return items.filter(item => item.archived);
74
+ },
75
+ };
76
+
77
+ const actions = {
78
+ archive(draft, payload) {
79
+ const target = draft.items.find(item => item.email === payload);
80
+ if (target) {
81
+ target.archived = true;
82
+ }
83
+ },
84
+ };
85
+ ```
86
+
87
+ 接下来我们把上面的代码连起来,放在同一个 Model 文件里。首先执行以下命令,创建新的文件目录:
88
+
89
+ ```bash
90
+ mkdir -p src/models/
91
+ touch src/models/contacts.ts
92
+ ```
93
+
94
+ 添加 `src/models/contacts.ts` 的内容:
95
+
96
+ ```tsx
97
+ import { model } from '@modern-js/runtime/model';
98
+
99
+ type State = {
100
+ items: {
101
+ avatar: string;
102
+ name: string;
103
+ email: string;
104
+ archived?: boolean;
105
+ }[];
106
+ pending: boolean;
107
+ error: null | Error;
108
+ };
109
+
110
+ export default model<State>('contacts').define({
111
+ state: {
112
+ items: [],
113
+ pending: false,
114
+ error: null,
115
+ },
116
+ computed: {
117
+ archived: ({ items }: State) => items.filter(item => item.archived),
118
+ },
119
+ actions: {
120
+ archive(draft, payload) {
121
+ const target = draft.items.find(item => item.email === payload)!;
122
+ if (target) {
123
+ target.archived = true;
124
+ }
125
+ },
126
+ },
127
+ });
128
+ ```
129
+
130
+ 我们把一个包含 state,action 等要素的 plain object 称作 **Model Spec**,Modern.js 提供了 [Model API](/docs/apis/app/runtime/model/model_),可以根据 Model Spec 生成 **Model**。
131
+
132
+ ## 使用 Model
133
+
134
+ 现在我们直接使用这个 Model,把项目的逻辑补充起来。
135
+
136
+ 首先修改 `src/components/Item/index.tsx`,添加 **Archive 按钮**的 UI 和交互,内容如下:
137
+
138
+ ```tsx
139
+ import Avatar from '../Avatar';
140
+
141
+ type InfoProps = {
142
+ avatar: string;
143
+ name: string;
144
+ email: string;
145
+ archived?: boolean;
146
+ };
147
+
148
+ const Item = ({
149
+ info,
150
+ onArchive,
151
+ }: {
152
+ info: InfoProps;
153
+ onArchive?: () => void;
154
+ }) => {
155
+ const { avatar, name, email, archived } = info;
156
+ return (
157
+ <div className="flex p-4 items-center border-gray-200 border-b">
158
+ <Avatar src={avatar} />
159
+ <div className="ml-4 custom-text-gray flex-1 flex justify-between">
160
+ <div className="flex-1">
161
+ <p>{name}</p>
162
+ <p>{email}</p>
163
+ </div>
164
+ <button
165
+ type="button"
166
+ disabled={archived}
167
+ onClick={onArchive}
168
+ className={`text-white font-bold py-2 px-4 rounded-full ${
169
+ archived
170
+ ? 'bg-gray-400 cursor-default'
171
+ : 'bg-blue-500 hover:bg-blue-700'
172
+ }`}
173
+ >
174
+ {archived ? 'Archived' : 'Archive'}
175
+ </button>
176
+ </div>
177
+ </div>
178
+ );
179
+ };
180
+
181
+ export default Item;
182
+ ```
183
+
184
+ 接下来,我们修改 `src/routes/page.tsx`,为 `<Item>` 组件传递更多参数:
185
+
186
+ ```tsx
187
+ import { Helmet } from '@modern-js/runtime/head';
188
+ import { useModel } from '@modern-js/runtime/model';
189
+ import { useLoaderData } from '@modern-js/runtime/router';
190
+ import { List } from 'antd';
191
+ import { name, internet } from 'faker';
192
+ import Item from '../components/Item';
193
+ import contacts from '../models/contacts';
194
+
195
+ type LoaderData = {
196
+ code: number;
197
+ data: {
198
+ name: string;
199
+ avatar: string;
200
+ email: string;
201
+ }[];
202
+ };
203
+
204
+ export const loader = async (): Promise<LoaderData> => {
205
+ const data = new Array(20).fill(0).map(() => {
206
+ const firstName = name.firstName();
207
+ return {
208
+ name: firstName,
209
+ avatar: `https://avatars.dicebear.com/api/identicon/${firstName}.svg`,
210
+ email: internet.email(),
211
+ archived: false,
212
+ };
213
+ });
214
+
215
+ return {
216
+ code: 200,
217
+ data,
218
+ };
219
+ };
220
+
221
+ function Index() {
222
+ const { data } = useLoaderData() as LoaderData;
223
+ const [{ items }, { archive, setItems }] = useModel(contacts);
224
+ if (items.length === 0) {
225
+ setItems(data);
226
+ }
227
+
228
+ return (
229
+ <div className="container lg mx-auto">
230
+ <Helmet>
231
+ <title>All</title>
232
+ </Helmet>
233
+ <List
234
+ dataSource={items}
235
+ renderItem={info => (
236
+ <Item
237
+ key={info.name}
238
+ info={info}
239
+ onArchive={() => {
240
+ archive(info.email);
241
+ }}
242
+ />
243
+ )}
244
+ />
245
+ </div>
246
+ );
247
+ }
248
+
249
+ export default Index;
250
+ ```
251
+
252
+ `useModel` 是 Modern.js 提供的 hooks API。可以在组件中提供 Model 中定义的 state,或通过 actions 调用 Model 中定义的 side effect 与 action,从而改变 Model 的 state。
253
+
254
+ Model 是业务逻辑,是计算过程,本身不创建也不持有状态。只有在被组件用 hooks API 使用后,才在指定的地方创建状态。
255
+
256
+ 执行 `pnpm run dev`,点击 **Archive 按钮**,可以看到页面 UI 发生了变化。
257
+
258
+ :::note
259
+ 上述例子中,`useLoaderData` 其实在每次切换路由时都会执行。因为我们在 Data Loader 里使用了 fake 数据,每次返回的数据是不同的。但我们优先使用了 Model 中的数据,因此切换路由时数据没有发生改变。
260
+ :::
@@ -0,0 +1,283 @@
1
+ ---
2
+ title: 添加容器组件
3
+ ---
4
+
5
+ import Tabs from '@theme/Tabs';
6
+ import TabItem from '@theme/TabItem';
7
+
8
+ 上一章节中,我们初步引入**业务模型**,从 UI 组件中拆分出这部分逻辑。`page.tsx` 中不再包含 UI 无关的业务逻辑实现细节,只需要使用 Model,就能实现同样的功能。
9
+
10
+ 这一章节中,我们要进一步利用 Model 中实现的业务逻辑,让 `page.tsx` 和 `archived/page.tsx` 获取同一份数据。并实现 Archive 按钮,点击按钮能把联系人归档,只显示在 Archives 列表里,不显示在 All 列表里。
11
+
12
+ ## 使用完整 Model
13
+
14
+ 因为两个页面需要共用同一套状态(联系人列表数据、联系人是否被归档),都需要包含加载初始数据的逻辑,因此我们需要在更上一层完成数据获取。
15
+
16
+ Modern.js 支持在 `layout.tsx` 通过 Data Loader 获取数据,我们先数据获取这部分代码移动到 `src/routes/layout.tsx` 中:
17
+
18
+ ```tsx
19
+ import { name, internet } from 'faker';
20
+ import {
21
+ Outlet,
22
+ useLoaderData,
23
+ useLocation,
24
+ useNavigate,
25
+ } from '@modern-js/runtime/router';
26
+ import { useState } from 'react';
27
+ import { Radio, RadioChangeEvent } from 'antd';
28
+ import { useModel } from '@modern-js/runtime/model';
29
+ import contacts from '../models/contacts';
30
+ import 'tailwindcss/base.css';
31
+ import 'tailwindcss/components.css';
32
+ import 'tailwindcss/utilities.css';
33
+ import '../styles/utils.css';
34
+
35
+ type LoaderData = {
36
+ code: number;
37
+ data: {
38
+ name: string;
39
+ avatar: string;
40
+ email: string;
41
+ }[];
42
+ };
43
+
44
+ export const loader = async (): Promise<LoaderData> => {
45
+ const data = new Array(20).fill(0).map(() => {
46
+ const firstName = name.firstName();
47
+ return {
48
+ name: firstName,
49
+ avatar: `https://avatars.dicebear.com/api/identicon/${firstName}.svg`,
50
+ email: internet.email(),
51
+ };
52
+ });
53
+
54
+ return {
55
+ code: 200,
56
+ data,
57
+ };
58
+ };
59
+
60
+ export default function Layout() {
61
+ const { data } = useLoaderData() as LoaderData;
62
+ const [{ items }, { setItems }] = useModel(contacts);
63
+ if (items.length === 0) {
64
+ setItems(data);
65
+ }
66
+
67
+ const navigate = useNavigate();
68
+ ...
69
+ }
70
+ ```
71
+
72
+ 在 `src/routes/page.tsx` 中,直接使用 Model,获取数据:
73
+
74
+ ```tsx
75
+ import { Helmet } from '@modern-js/runtime/head';
76
+ import { useModel } from '@modern-js/runtime/model';
77
+ import { List } from 'antd';
78
+ import Item from '../components/Item';
79
+ import contacts from '../models/contacts';
80
+
81
+ function Index() {
82
+ const [{ items }, { archive }] = useModel(contacts);
83
+
84
+ return (
85
+ <div className="container lg mx-auto">
86
+ <Helmet>
87
+ <title>All</title>
88
+ </Helmet>
89
+ <List
90
+ dataSource={items}
91
+ renderItem={info => (
92
+ <Item
93
+ key={info.name}
94
+ info={info}
95
+ onArchive={() => {
96
+ archive(info.email);
97
+ }}
98
+ />
99
+ )}
100
+ />
101
+ </div>
102
+ );
103
+ }
104
+
105
+ export default Index;
106
+ ```
107
+
108
+ 同样在 `archived/page.tsx` 中,删除原本的 `mockData` 逻辑,使用 Model 中 computed 的 `archived` 值作为数据源:
109
+
110
+ ```tsx
111
+ import { Helmet } from '@modern-js/runtime/head';
112
+ import { useModel } from '@modern-js/runtime/model';
113
+ import { List } from 'antd';
114
+ import Item from '../../components/Item';
115
+ import contacts from '../../models/contacts';
116
+
117
+ function Index() {
118
+ const [{ archived }, { archive }] = useModel(contacts);
119
+
120
+ return (
121
+ <div className="container lg mx-auto">
122
+ <Helmet>
123
+ <title>Archives</title>
124
+ </Helmet>
125
+ <List
126
+ dataSource={archived}
127
+ renderItem={info => (
128
+ <Item
129
+ key={info.name}
130
+ info={info}
131
+ onArchive={() => {
132
+ archive(info.email);
133
+ }}
134
+ />
135
+ )}
136
+ />
137
+ </div>
138
+ );
139
+ }
140
+
141
+ export default Index;
142
+ ```
143
+
144
+ 执行 `pnpm run dev`,访问 `http://localhost:8080/`,点击 Archive 按钮后,可以看到按钮置灰:
145
+
146
+ ![display](https://lf3-static.bytednsdoc.com/obj/eden-cn/nuvjhpqnuvr/modern-website/tutorials/c07-contacts-all.png)
147
+
148
+ 接下来点击顶部导航,切换到 Archives 列表,可以发现刚才 **Archive** 的联系人已经出现在列表当中:
149
+
150
+ ![display](https://lf3-static.bytednsdoc.com/obj/eden-cn/nuvjhpqnuvr/modern-website/tutorials/c07-contacts-archives.png)
151
+
152
+ ## 抽离容器组件
153
+
154
+ 前面章节中,我们把项目中的业务逻辑拆分成了两个 layer,一个是**视图组件**,另一个是**业务模块**。前者负责 UI 展示、交互等,后者负责实现 UI 无关的业务逻辑,专门管理状态。
155
+
156
+ 像 `src/routes/page.tsx` 和 `src/routes/archives/page.tsx` 这样使用了 `useModel` API 的组件,负责把 View 和 Model 这两个 layer 连接起来,类似传统 MVC 架构中 Controller 的角色,在 Modern.js 里我们沿用习惯,把它们称作**容器组件(Container)**。
157
+
158
+ 容器组件推荐放在专门的 `containers/` 目录里,我们执行以下命令,创建新的文件:
159
+
160
+ <Tabs>
161
+ <TabItem value="macOS" label="macOS" default>
162
+
163
+ ```bash
164
+ mkdir -p src/containers
165
+ touch src/containers/Contacts.tsx
166
+ ```
167
+
168
+ </TabItem>
169
+ <TabItem value="Windows" label="Windows">
170
+
171
+ ```powershell
172
+ mkdir -p src/containers
173
+ ni src/containers/Contacts.tsx
174
+ ```
175
+
176
+ </TabItem>
177
+ </Tabs>
178
+
179
+ 我们将原本两个 `page.tsx` 中公共的部分抽离出来,`src/containers/Contacts.tsx` 的代码如下:
180
+
181
+ ```tsx
182
+ import { Helmet } from "@modern-js/runtime/head";
183
+ import { useModel } from "@modern-js/runtime/model";
184
+ import { List } from "antd";
185
+ import Item from "../components/Item";
186
+ import { Helmet } from '@modern-js/runtime/head';
187
+ import { useModel } from '@modern-js/runtime/model';
188
+ import { List } from 'antd';
189
+ import Item from '../components/Item';
190
+ import contacts from '../models/contacts';
191
+
192
+ function Contacts({
193
+ title,
194
+ source,
195
+ }: {
196
+ title: string;
197
+ source: 'items' | 'archived';
198
+ }) {
199
+ const [state, { archive }] = useModel(contacts);
200
+
201
+ return (
202
+ <div className="container lg mx-auto">
203
+ <Helmet>
204
+ <title>{title}</title>
205
+ </Helmet>
206
+ <List
207
+ dataSource={state[source]}
208
+ renderItem={info => (
209
+ <Item
210
+ key={info.name}
211
+ info={info}
212
+ onArchive={() => {
213
+ archive(info.email);
214
+ }}
215
+ />
216
+ )}
217
+ />
218
+ </div>
219
+ );
220
+ }
221
+
222
+ export default Contacts;
223
+ ```
224
+
225
+ 修改 `src/routes/page.tsx` 和 `src/routes/archives/page.tsx` 的代码:
226
+
227
+ ```tsx title="src/routes/page.tsx"
228
+ import Contacts from '../containers/Contacts';
229
+
230
+ function Index() {
231
+ return <Contacts title="All" source="items" />;
232
+ }
233
+
234
+ export default Index;
235
+ ```
236
+
237
+ ```tsx title="src/routes/archives/page.tsx"
238
+ import Contacts from '../../containers/Contacts';
239
+
240
+ function Index() {
241
+ return <Contacts title="Archives" source="archived" />;
242
+ }
243
+
244
+ export default Index;
245
+ ```
246
+
247
+ 重构完成,现在的项目结构是:
248
+
249
+ ```md
250
+ .
251
+ ├── README.md
252
+ ├── dist
253
+ ├── modern.config.ts
254
+ ├── node_modules
255
+ ├── package.json
256
+ ├── pnpm-lock.yaml
257
+ ├── src
258
+ │   ├── components
259
+ │   │   ├── Avatar
260
+ │   │   │   └── index.tsx
261
+ │   │   └── Item
262
+ │   │   └── index.tsx
263
+ │   ├── containers
264
+ │   │   └── Contacts.tsx
265
+ │   ├── models
266
+ │   │   └── contacts.ts
267
+ │   ├── modern-app-env.d.ts
268
+ │   ├── routes
269
+ │   │   ├── archives
270
+ │   │   │   └── page.tsx
271
+ │   │   ├── layout.tsx
272
+ │   │   └── page.tsx
273
+ │   └── styles
274
+ │   └── utils.css
275
+ └── tsconfig.json
276
+ ```
277
+
278
+ `components/` 里的**视图组件**,都是目录形式,如 `Avatar/index.tsx`。而 `containers/` 里的**容器组件**,都是单文件形式,如 `contacts.tsx`。**这是我们推荐的一种最佳实践**。
279
+
280
+ 在​ [添加 UI 组件(Component)](./c02-component.md) 章节提到过,视图组件用目录形式,是因为视图组件负责实现 UI 展示和交互细节,可以演变的复杂。用目录形式,可以方便增加子文件,包括专用的资源(图片等)、专用子组件、CSS 文件等。在这个目录内部可以随意重构,只考虑最小局部。
281
+
282
+ 而容器组件只负责连接,是一个胶水层,复杂的业务逻辑和实现细节都交给 View 层和 Model 层去实现。容器组件自己应该保持简单清晰,不应该包含复杂实现细节,所以不应该有内部结构。采用单文件形式不但更简洁,也能起到约束作用,提醒开发者不要把容器组件写复杂。
283
+
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: 添加应用入口
3
+ ---
4
+
5
+ 上一个章节中,我们基本完成了联系人列表应用的开发,介绍了 Modern.js 中部分功能的用法,以及推荐的最佳实践。
6
+
7
+ 这一章节中,我们将介绍如何为应用添加新的入口。
8
+
9
+ ## 新建入口
10
+
11
+ 一个完整的项目可能需要多个入口,Modern.js 支持自动创建新入口,前面的章节中提到过,`pnpm run new` 可以启用可选功能。
12
+
13
+ 我们也可以通过它来创建新的工程元素,在项目根目录下执行 `pnpm run new`:
14
+
15
+ ```bash
16
+ ? 请选择你想要的操作 创建工程元素
17
+ ? 创建工程元素 新建「应用入口」
18
+ ? 请填写入口名称 (entry) landing-page
19
+ ```
20
+
21
+ 创建完成,项目会变成这样:
22
+
23
+ ```md
24
+ .
25
+ ├── README.md
26
+ ├── modern.config.ts
27
+ ├── node_modules
28
+ ├── package.json
29
+ ├── pnpm-lock.yaml
30
+ ├── src
31
+ │   ├── modern-app-env.d.ts
32
+ │   ├── landing-page
33
+ │   │   └── routes
34
+ │   │   ├── index.css
35
+ │   │   ├── layout.tsx
36
+ │   │   └── page.tsx
37
+ │   └── myapp
38
+ │   ├── components
39
+ │   │   ├── Avatar
40
+ │   │   │   └── index.tsx
41
+ │   │   └── Item
42
+ │   │   └── index.tsx
43
+ │   ├── containers
44
+ │   │   └── Contacts.tsx
45
+ │   ├── models
46
+ │   │   └── contacts.ts
47
+ │   ├── routes
48
+ │   │   ├── archives
49
+ │   │   │   └── page.tsx
50
+ │   │   ├── layout.tsx
51
+ │   │   └── page.tsx
52
+ │   └── styles
53
+ │   └── utils.css
54
+ └── tsconfig.json
55
+
56
+ ```
57
+
58
+ 可以看到联系人列表应用的文件,都被自动重构到 `src/myapp/` 里。
59
+
60
+ 同时新建了一个 `src/landing-page/`,里面同样有 `routes/*`(`pnpm run new` 命令只做了这些事,所以你也可以很容易的手动创建新入口或修改入口)
61
+
62
+ 执行 `pnpm run dev`,显示:
63
+
64
+ ![design](https://lf3-static.bytednsdoc.com/obj/eden-cn/nuvjhpqnuvr/modern-website/tutorials/c08-entries-myapp.png)
65
+
66
+ 访问 `http://localhost:8080/`,可以像之前一样看到应用程序。
67
+
68
+ 访问 `http://localhost:8080/landing-page`,可以看到刚创建的新入口 `landing-page` 的页面(Modern.js 自动生成的默认页面)。
69
+
70
+ Modern.js 框架的设计原则之一是【[约定优于配置(Convention over Configuration)](https://en.wikipedia.org/wiki/Convention_over_configuration)】,多数情况下可以按约定直接写代码,不需要做任何配置,这里 `src/` 中的目录结构就是一种约定:
71
+
72
+ `src/myapp/` 和 `src/landing-page/` 被自动识别为两个应用入口:myapp 和 landing-page。
73
+
74
+ 其中 `src/myapp/` 的目录名跟项目名(`package.json` 里的 `name`)一致,会被认为是项目**主入口**,项目 URL 的根路径(开发环境里默认是 `http://localhost:8080/`)会自动指向主入口。
75
+
76
+ 其他入口的 URL,是在根路径后追加入口名,比如 `http://localhost:8080/landing-page`。
77
+
78
+ 接下来,我们把 `src/myapp/` 重命名为 `src/contacts/`:
79
+
80
+ ```bash
81
+ mv src/myapp src/contacts
82
+ ```
83
+
84
+ 再次执行 `pnpm run dev`,结果变成:
85
+
86
+ ![design](https://lf3-static.bytednsdoc.com/obj/eden-cn/nuvjhpqnuvr/modern-website/tutorials/c08-entries-contacts.png)
87
+
88
+ 现在不再有主入口,联系人列表现在是一个普通入口,需要用 `http://localhost:8080/contacts` 访问。
89
+
90
+
91
+ ## 按入口修改配置
92
+
93
+ 我们可以在 Modern.js 配置文件里,自己写代码来控制项目的配置。
94
+
95
+ 现在,修改 `modern.config.ts` 里面添加内容:
96
+
97
+ ```typescript
98
+ import AppToolsPlugin, { defineConfig } from '@modern-js/app-tools';
99
+ import TailwindCSSPlugin from '@modern-js/plugin-tailwindcss';
100
+
101
+ // https://modernjs.dev/docs/apis/app/config
102
+ export default defineConfig({
103
+ runtime: {
104
+ router: true,
105
+ state: true,
106
+ },
107
+ server: {
108
+ ssr: true,
109
+ ssrByEntries: {
110
+ 'landing-page': false,
111
+ },
112
+ },
113
+ plugins: [AppToolsPlugin(), TailwindCSSPlugin()],
114
+ });
115
+ ```
116
+
117
+ 执行 `pnpm run dev`,再用浏览器打开 `view-source:http://localhost:8080/landing-page`,可以看到 `landing-page` 网页内容是通过 js 动态加载的,且此页面的 SSR 功能被关闭。
118
+
119
+ 如果注释掉 `ssrByEntries` 和它的值,landing-page 的 SSR 功能就恢复开启了。
120
+
121
+ 还有一些时候,需要一些更复杂的逻辑来做设置,比如需要 JS 变量、表达式、导入模块等,例如在只在开发环境里开启 SSR:
122
+
123
+ ```js
124
+ export default defineConfig({
125
+ server: {
126
+ ssrByEntries: {
127
+ 'landing-page': process.env.NODE_ENV !== 'production',
128
+ },
129
+ },
130
+ };
131
+ ```
132
+
133
+ 到底为止,我们的联系人列表应用的雏形就基本完成了 👏👏👏。
134
+
135
+ ## 下一步
136
+
137
+ 接下来你可以通过了解[指南](/docs/guides/get-started/quick-start)、[配置](/docs/configure/app/usage) 等更多教程,进一步完善你的应用。
@@ -1,5 +1,5 @@
1
1
  {
2
- "label": "基础",
2
+ "label": "Basic",
3
3
  "position": 1,
4
4
  "collapsed": false
5
5
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Modern.js Introduce
2
+ title: Introduce
3
3
  sidebar_position: 1
4
4
  ---
5
5
 
@@ -38,6 +38,8 @@ It mainly contains the following features:
38
38
 
39
39
  ## Next
40
40
 
41
- If you are a front-end beginner, you may find these concepts a bit complicated. We provide some [JavaScript and React](/docs/tutorials/foundations/basic) learning materials, you can do some understanding of them first。
41
+ <!-- If you are a front-end beginner, you may find these concepts a bit complicated. We provide some [JavaScript and React](/docs/tutorials/foundations/basic) learning materials, you can do some understanding of them first。
42
42
 
43
- If you are an experienced developer and want to know how to use Modern.js, you can try [Create your first app](/docs/tutorials/first-app/c01-start), or read [Guide](/docs/guides/overview).
43
+ If you are an experienced developer and want to know how to use Modern.js, you can try [Create your first app](/docs/tutorials/first-app/c01-start), or read [Guide](/docs/guides/get-started/quick-start). -->
44
+
45
+ If you want to know how to use Modern.js, you can try [Create your first app](/docs/tutorials/first-app/c01-start), or read [Guide](/docs/guides/get-started/quick-start).