@modern-js/main-doc 0.0.0-next-20230104054903 → 0.0.0-next-20230104140639
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/CHANGELOG.md +2 -3
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/_category_.json +5 -0
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c01-start.md +99 -0
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c02-component.md +56 -0
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c03-css.md +324 -0
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c04-routes.md +169 -0
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c05-loader.md +82 -0
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c06-model.md +260 -0
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c07-container.md +283 -0
- package/en/docusaurus-plugin-content-docs/current/tutorials/first-app/c08-entries.md +137 -0
- package/package.json +3 -3
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
@@ -0,0 +1,99 @@
|
|
1
|
+
---
|
2
|
+
title: 创建项目
|
3
|
+
---
|
4
|
+
|
5
|
+
从这一章节开始,我们将进入实战教程部分。在实战教程中,我们将会从环境准备开始,从简单到复杂,一步一步搭建一个真实的项目。
|
6
|
+
|
7
|
+
## 环境准备
|
8
|
+
|
9
|
+
import Prerequisites from '@site-docs/components/prerequisites.md'
|
10
|
+
|
11
|
+
<Prerequisites />
|
12
|
+
|
13
|
+
## 初始化项目
|
14
|
+
|
15
|
+
我们创建新的目录,通过命令行工具初始化项目:
|
16
|
+
|
17
|
+
```bash
|
18
|
+
mkdir myapp && cd myapp
|
19
|
+
npx @modern-js/create
|
20
|
+
```
|
21
|
+
|
22
|
+
import InitApp from '@site-docs/components/init-app.md'
|
23
|
+
|
24
|
+
<InitApp />
|
25
|
+
|
26
|
+
## 调试项目
|
27
|
+
|
28
|
+
import DebugApp from '@site-docs/components/debug-app.md'
|
29
|
+
|
30
|
+
<DebugApp />
|
31
|
+
|
32
|
+
## 修改代码
|
33
|
+
|
34
|
+
我们将原本的示例代码删除,替换成一个简单的联系人列表:
|
35
|
+
|
36
|
+
```tsx title="src/routes/page.tsx"
|
37
|
+
const getAvatar = (users: Array<{ name: string; email: string }>) =>
|
38
|
+
users.map(user => ({
|
39
|
+
...user,
|
40
|
+
avatar: `https://avatars.dicebear.com/v2/identicon/${user.name}.svg`,
|
41
|
+
}));
|
42
|
+
|
43
|
+
const mockData = getAvatar([
|
44
|
+
{ name: 'Thomas', email: 'w.kccip@bllmfbgv.dm' },
|
45
|
+
{ name: 'Chow', email: 'f.lfqljnlk@ywoefljhc.af' },
|
46
|
+
{ name: 'Bradley', email: 'd.wfovsqyo@gpkcjwjgb.fr' },
|
47
|
+
{ name: 'Davis', email: '"t.kqkoj@utlkwnpwk.nu' },
|
48
|
+
]);
|
49
|
+
|
50
|
+
function App() {
|
51
|
+
return (
|
52
|
+
<ul>
|
53
|
+
{mockData.map(({ name, avatar, email }) => (
|
54
|
+
<li key={name}>
|
55
|
+
<img src={avatar} width={60} height={60} /> ---
|
56
|
+
<span>{name}</span> ---
|
57
|
+
<span>{email}</span>
|
58
|
+
</li>
|
59
|
+
))}
|
60
|
+
</ul>
|
61
|
+
);
|
62
|
+
}
|
63
|
+
|
64
|
+
export default App;
|
65
|
+
```
|
66
|
+
|
67
|
+
删除多余的 css 文件,保持目录没有多余的文件:
|
68
|
+
|
69
|
+
```bash
|
70
|
+
rm src/routes/index.css
|
71
|
+
```
|
72
|
+
|
73
|
+
因为框架默认支持 [HMR](https://webpack.js.org/concepts/hot-module-replacement/),可以看到 `http://localhost:8080/` 里的内容会自动更新为:
|
74
|
+
|
75
|
+

|
76
|
+
|
77
|
+
此刻的页面还没有样式。下一章节将展开这部分的内容。
|
78
|
+
|
79
|
+
## 开启 SSR
|
80
|
+
|
81
|
+
接下来,我们修改项目中的 `modern.config.ts`,开启 SSR 能力:
|
82
|
+
|
83
|
+
```ts
|
84
|
+
import AppToolsPlugin, { defineConfig } from '@modern-js/app-tools';
|
85
|
+
|
86
|
+
// https://modernjs.dev/docs/apis/app/config
|
87
|
+
export default defineConfig({
|
88
|
+
runtime: {
|
89
|
+
router: true,
|
90
|
+
state: true,
|
91
|
+
},
|
92
|
+
server: {
|
93
|
+
ssr: true,
|
94
|
+
},
|
95
|
+
plugins: [AppToolsPlugin()],
|
96
|
+
});
|
97
|
+
```
|
98
|
+
|
99
|
+
重新执行 `pnpm run dev`,可以发现项目已经在服务端完成了页面渲染。
|
@@ -0,0 +1,56 @@
|
|
1
|
+
---
|
2
|
+
title: 编写 UI 组件
|
3
|
+
---
|
4
|
+
|
5
|
+
上一章节中,我们学习了如何初始化项目,并使用配置修改 Modern.js 的默认行为。
|
6
|
+
|
7
|
+
这一章节中,我们继续沿用上一章节的项目代码,继续完善联系人列表。
|
8
|
+
|
9
|
+
为了做更好的 UI 展示和交互,我们引入组件库 [Antd](https://ant.design/index-cn) 来开发,使用 `<List>` 组件来代替原始的列表。先添加依赖:
|
10
|
+
|
11
|
+
```bash
|
12
|
+
pnpm add antd
|
13
|
+
```
|
14
|
+
|
15
|
+
修改 `src/routes/page.tsx`,在顶部导入组件:
|
16
|
+
|
17
|
+
```ts
|
18
|
+
import { List } from 'antd';
|
19
|
+
```
|
20
|
+
|
21
|
+
修改 `<App>` 组件的实现:
|
22
|
+
|
23
|
+
```tsx
|
24
|
+
function App() {
|
25
|
+
return (
|
26
|
+
<div>
|
27
|
+
<List
|
28
|
+
dataSource={mockData}
|
29
|
+
renderItem={({ name, email, avatar }) => (
|
30
|
+
<List.Item key={name}>
|
31
|
+
<List.Item.Meta
|
32
|
+
avatar={<img alt="avatar" src={avatar} width={60} height={60} />}
|
33
|
+
title={name}
|
34
|
+
description={email}
|
35
|
+
/>
|
36
|
+
</List.Item>
|
37
|
+
)}
|
38
|
+
/>
|
39
|
+
</div>
|
40
|
+
);
|
41
|
+
}
|
42
|
+
```
|
43
|
+
|
44
|
+
执行 `pnpm run dev`,查看运行结果:
|
45
|
+
|
46
|
+

|
47
|
+
|
48
|
+
可以看到 Ant Design 导出的组件,已经具备了完整的样式。
|
49
|
+
|
50
|
+
:::info 注
|
51
|
+
Modern.js 会[自动按需导入 Ant Design 组件需要的 CSS](https://github.com/ant-design/babel-plugin-import)。
|
52
|
+
:::
|
53
|
+
|
54
|
+
:::note
|
55
|
+
我们也可以使用其他组件库来实现同样的功能,例如 [Arco Design](https://arco.design/)。
|
56
|
+
:::
|
@@ -0,0 +1,324 @@
|
|
1
|
+
---
|
2
|
+
title: 添加样式
|
3
|
+
---
|
4
|
+
|
5
|
+
import Tabs from '@theme/Tabs';
|
6
|
+
import TabItem from '@theme/TabItem';
|
7
|
+
|
8
|
+
上一章节中,我们学习了如何使用使用三方库中的组件。
|
9
|
+
|
10
|
+
这一章节中,我们将学习如何实现 UI 组件。
|
11
|
+
|
12
|
+
## 使用 CSS 写 JS 组件
|
13
|
+
|
14
|
+
首先我们希望自己控制联系人头像的展示,实现这种设计稿:
|
15
|
+
|
16
|
+

|
17
|
+
|
18
|
+
假设没有现成的组件可以实现,那就需要自己写些 CSS 了,这里我们使用 [styled-components](https://styled-components.com/),来实现类似的需求。Modern.js 开箱即用的支持 styled-components,既不需要安装依赖,也不需要做任何配置。
|
19
|
+
|
20
|
+
styled-components 通过模块化的方式,避免了传统 CSS 写法上的诸多问题。例如直接在元素的 style 属性上写样式,UI 视觉上的细节也会跟 UI 结构上的细节和业务逻辑混在一起。或是 classname 需要避免全局空间重名,需要用到命名规范的问题。
|
21
|
+
|
22
|
+
在 `src/routes/page.tsx` 里修改顶部的代码:
|
23
|
+
|
24
|
+
```js
|
25
|
+
import styled from '@modern-js/runtime/styled';
|
26
|
+
```
|
27
|
+
|
28
|
+
添加以下代码:
|
29
|
+
|
30
|
+
```js
|
31
|
+
const Avatar = styled.img`
|
32
|
+
width: 50px;
|
33
|
+
height: 50px;
|
34
|
+
border: 4px solid #0ef;
|
35
|
+
border-radius: 50%;
|
36
|
+
`;
|
37
|
+
```
|
38
|
+
|
39
|
+
修改 `List.Item.Meta` 的代码:
|
40
|
+
|
41
|
+
```tsx
|
42
|
+
<List.Item.Meta
|
43
|
+
avatar={<Avatar src={avatar} />}
|
44
|
+
title={name}
|
45
|
+
description={email}
|
46
|
+
/>
|
47
|
+
```
|
48
|
+
|
49
|
+
执行 `pnpm run dev`,可以看到预期的运行结果:
|
50
|
+
|
51
|
+

|
52
|
+
|
53
|
+
接下来我们做一点重构,为了增强可读性,让代码更容易维护,可以把 Avatar 组件拆分出去。我们在终端执行以下命令,创建新的文件:
|
54
|
+
|
55
|
+
<Tabs>
|
56
|
+
<TabItem value="macOS" label="macOS" default>
|
57
|
+
|
58
|
+
```bash
|
59
|
+
mkdir -p src/components/Avatar
|
60
|
+
touch src/components/Avatar/index.tsx
|
61
|
+
```
|
62
|
+
|
63
|
+
</TabItem>
|
64
|
+
<TabItem value="Windows" label="Windows">
|
65
|
+
|
66
|
+
```powershell
|
67
|
+
mkdir -p src/components/Avatar
|
68
|
+
ni src/components/Avatar/index.tsx
|
69
|
+
```
|
70
|
+
|
71
|
+
</TabItem>
|
72
|
+
</Tabs>
|
73
|
+
|
74
|
+
把 `src/routes/page.tsx` 里的 `<Avatar>` 实现删掉,修改为:
|
75
|
+
|
76
|
+
```ts
|
77
|
+
import Avatar from '../components/Avatar';
|
78
|
+
```
|
79
|
+
|
80
|
+
`src/components/Avatar/index.tsx` 的内容,修改为:
|
81
|
+
|
82
|
+
```ts
|
83
|
+
import styled from '@modern-js/runtime/styled';
|
84
|
+
|
85
|
+
const Avatar = styled.img`
|
86
|
+
width: 50px;
|
87
|
+
height: 50px;
|
88
|
+
border: 4px solid #0ef;
|
89
|
+
border-radius: 50%;
|
90
|
+
`;
|
91
|
+
|
92
|
+
export default Avatar;
|
93
|
+
```
|
94
|
+
|
95
|
+
执行 `pnpm run dev`,运行结果应该是一样的。
|
96
|
+
|
97
|
+
:::info 注
|
98
|
+
采用目录形式 `Avatar/index.tsx` 而不是单文件形式 `Avatar.tsx` 的原因是,之后可以方便的在目录内部增加子文件,包括专用的资源(图片等)、专用子组件、CSS 文件等。
|
99
|
+
:::
|
100
|
+
|
101
|
+
|
102
|
+
## 使用 Utility
|
103
|
+
|
104
|
+
我们已经使用 style-components 实现 `<Avatar>` 组件,但当前的 UI 仍然不能让人满意,缺乏专业感,例如列表项内部的布局有点粗糙,很多地方没对齐。
|
105
|
+
|
106
|
+
现在,我们自己来实现一个更好的 `Item` 组件,实现这样的设计稿:
|
107
|
+
|
108
|
+

|
109
|
+
|
110
|
+
这次要实现的 UI 更复杂,有内部结构,但另一方面,并没有 `<Avatar>` 组件的**很粗的亮蓝色边框**这样很特殊的 UI,都是很常规的水平垂直布局、居中、字体样式等。这种情况下,其实根本没必要写 CSS,有更高效的、跟 styled-components 互补的实现方式:**Utility Class**。
|
111
|
+
|
112
|
+
Modern.js 集成了主流、轻量、通用的 Utility Class 工具库 [Tailwind CSS](https://tailwindcss.com/)。
|
113
|
+
|
114
|
+
执行 `pnpm run new`,进行如下选择,开启 Tailwind CSS:
|
115
|
+
|
116
|
+
```bash
|
117
|
+
? 请选择你想要的操作 启用可选功能
|
118
|
+
? 启用可选功能 启用 Tailwind CSS 支持
|
119
|
+
```
|
120
|
+
|
121
|
+
在 `modern.config.ts` 中注册 Tailwind 插件:
|
122
|
+
|
123
|
+
```ts title="modern.config.ts"
|
124
|
+
import AppToolsPlugin, { defineConfig } from '@modern-js/app-tools';
|
125
|
+
import TailwindCSSPlugin from '@modern-js/plugin-tailwindcss';
|
126
|
+
|
127
|
+
// https://modernjs.dev/docs/apis/app/config
|
128
|
+
export default defineConfig({
|
129
|
+
runtime: {
|
130
|
+
router: true,
|
131
|
+
state: true,
|
132
|
+
},
|
133
|
+
server: {
|
134
|
+
ssr: true,
|
135
|
+
},
|
136
|
+
plugins: [AppToolsPlugin(), TailwindCSSPlugin()],
|
137
|
+
});
|
138
|
+
```
|
139
|
+
|
140
|
+
在 `src/routes/page.tsx` 顶部引入 Tailwind CSS 的 css 文件,就可以开始快速实现专业的 UI:
|
141
|
+
|
142
|
+
```js
|
143
|
+
import 'tailwindcss/base.css';
|
144
|
+
import 'tailwindcss/components.css';
|
145
|
+
import 'tailwindcss/utilities.css';
|
146
|
+
```
|
147
|
+
|
148
|
+
先创建 Item 组件:
|
149
|
+
|
150
|
+
<Tabs>
|
151
|
+
<TabItem value="macOS" label="macOS" default>
|
152
|
+
|
153
|
+
```bash
|
154
|
+
mkdir -p src/components/Item
|
155
|
+
touch src/components/Item/index.tsx
|
156
|
+
```
|
157
|
+
|
158
|
+
</TabItem>
|
159
|
+
<TabItem value="Windows" label="Windows">
|
160
|
+
|
161
|
+
```powershell
|
162
|
+
mkdir -p src/components/Item
|
163
|
+
ni src/components/Item/index.tsx
|
164
|
+
```
|
165
|
+
|
166
|
+
</TabItem>
|
167
|
+
</Tabs>
|
168
|
+
|
169
|
+
修改 `src/routes/page.tsx`,把 `List` 的 `render` 实现交给 `Item` 组件:
|
170
|
+
|
171
|
+
```js
|
172
|
+
import { List } from 'antd';
|
173
|
+
import 'tailwindcss/base.css';
|
174
|
+
import 'tailwindcss/components.css';
|
175
|
+
import 'tailwindcss/utilities.css';
|
176
|
+
import Item from '../components/Item';
|
177
|
+
|
178
|
+
const getAvatar = (users: Array<{ name: string; email: string }>) =>
|
179
|
+
users.map(user => ({
|
180
|
+
...user,
|
181
|
+
avatar: `https://avatars.dicebear.com/v2/identicon/${user.name}.svg`,
|
182
|
+
}));
|
183
|
+
|
184
|
+
const mockData = getAvatar([
|
185
|
+
{ name: 'Thomas', email: 'w.kccip@bllmfbgv.dm' },
|
186
|
+
{ name: 'Chow', email: 'f.lfqljnlk@ywoefljhc.af' },
|
187
|
+
{ name: 'Bradley', email: 'd.wfovsqyo@gpkcjwjgb.fr' },
|
188
|
+
{ name: 'Davis', email: '"t.kqkoj@utlkwnpwk.nu' },
|
189
|
+
]);
|
190
|
+
|
191
|
+
function Index() {
|
192
|
+
return (
|
193
|
+
<div className="container lg mx-auto">
|
194
|
+
<List
|
195
|
+
dataSource={mockData}
|
196
|
+
renderItem={info => <Item key={info.name} info={info} />}
|
197
|
+
/>
|
198
|
+
</div>
|
199
|
+
);
|
200
|
+
}
|
201
|
+
|
202
|
+
export default Index;
|
203
|
+
```
|
204
|
+
|
205
|
+
在父容器的上使用了 [Utility Class](https://tailwindcss.com/docs/container) ,快速实现了最基本的最大宽度、居中等样式。
|
206
|
+
|
207
|
+
接下来实现 `src/components/Item/index.tsx`:
|
208
|
+
|
209
|
+
```tsx
|
210
|
+
import Avatar from '../Avatar';
|
211
|
+
|
212
|
+
type InfoProps = {
|
213
|
+
avatar: string;
|
214
|
+
name: string;
|
215
|
+
email: string;
|
216
|
+
archived?: boolean;
|
217
|
+
};
|
218
|
+
|
219
|
+
const Item = ({ info }: { info: InfoProps }) => {
|
220
|
+
const { avatar, name, email, archived } = info;
|
221
|
+
return (
|
222
|
+
<div className="flex p-4 items-center border-gray-200 border-b">
|
223
|
+
<Avatar src={avatar} />
|
224
|
+
<div className="ml-4 flex-1 flex justify-between">
|
225
|
+
<div className="flex-1">
|
226
|
+
<p>{name}</p>
|
227
|
+
<p>{email}</p>
|
228
|
+
</div>
|
229
|
+
<button
|
230
|
+
type="button"
|
231
|
+
disabled={archived}
|
232
|
+
className={`bg-blue-500 text-white font-bold
|
233
|
+
py-2 px-4 rounded-full hover:bg-blue-700`}
|
234
|
+
>
|
235
|
+
Archive
|
236
|
+
</button>
|
237
|
+
</div>
|
238
|
+
</div>
|
239
|
+
);
|
240
|
+
};
|
241
|
+
|
242
|
+
export default Item;
|
243
|
+
```
|
244
|
+
|
245
|
+
执行 `pnpm run dev`,可以看到预期的运行结果:
|
246
|
+
|
247
|
+

|
248
|
+
|
249
|
+
我们只使用了少量 Utility Class,比如 [Flex](https://tailwindcss.com/docs/display/)、[Padding](https://tailwindcss.com/docs/padding/)、[Margin](https://tailwindcss.com/docs/margin/)、[Text](https://tailwindcss.com/docs/text-color/)、[Font](https://tailwindcss.com/docs/font-weight/)、[Border](https://tailwindcss.com/docs/border-width),不写一行 CSS 就实现了符合设计稿的专业 UI。
|
250
|
+
|
251
|
+
|
252
|
+
## 自定义 Utility Class
|
253
|
+
|
254
|
+
我们也可以自己实现新的 Utility Class,方便在代码间复用。
|
255
|
+
|
256
|
+
Utility Class 本身也是一种**面向组件**的技术(将不同 class 用在一个组件上,等价于给这个组件设置了一些来自基类的属性),但 Utility Class 的 classname 是全局的(因为要用在任意组件/元素上),很适合用独立 CSS 文件来实现。
|
257
|
+
|
258
|
+
创建一个新的 CSS 文件:
|
259
|
+
|
260
|
+
<Tabs>
|
261
|
+
<TabItem value="macOS" label="macOS" default>
|
262
|
+
|
263
|
+
```bash
|
264
|
+
mkdir -p src/styles
|
265
|
+
touch src/styles/utils.css
|
266
|
+
```
|
267
|
+
|
268
|
+
</TabItem>
|
269
|
+
<TabItem value="Windows" label="Windows">
|
270
|
+
|
271
|
+
```powershell
|
272
|
+
mkdir -p src/styles
|
273
|
+
ni src/styles/utils.css
|
274
|
+
```
|
275
|
+
|
276
|
+
</TabItem>
|
277
|
+
</Tabs>
|
278
|
+
|
279
|
+
在 `src/routes/page.tsx` 里导入 `utils.css`:
|
280
|
+
|
281
|
+
```js
|
282
|
+
import '../styles/utils.css';
|
283
|
+
```
|
284
|
+
|
285
|
+
在 `src/routes/styles/utils.css` 里实现一个名为 `custom-text-gray` 的 Utility Class。
|
286
|
+
|
287
|
+
```css
|
288
|
+
:root {
|
289
|
+
--custom-text-color:rgb(113, 128, 150);
|
290
|
+
}
|
291
|
+
|
292
|
+
.custom-text-gray {
|
293
|
+
color: var(--custom-text-color);
|
294
|
+
}
|
295
|
+
```
|
296
|
+
|
297
|
+
:::info 注
|
298
|
+
Modern.js 集成了 [PostCSS](/docs/guides/basic-features/css/postcss),支持现代 CSS 语法特性,比如 [custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*)。
|
299
|
+
:::
|
300
|
+
|
301
|
+
在 `src/routes/components/Item/index.tsx` 里使用,把:
|
302
|
+
|
303
|
+
```js
|
304
|
+
<div className="ml-4 flex-1 flex justify-between">
|
305
|
+
```
|
306
|
+
|
307
|
+
改成:
|
308
|
+
|
309
|
+
```js
|
310
|
+
<div className="ml-4 custom-text-gray flex-1 flex justify-between">
|
311
|
+
```
|
312
|
+
|
313
|
+
执行 `pnpm run dev`,可以看到字体颜色改变了:
|
314
|
+
|
315
|
+

|
316
|
+
|
317
|
+
:::info 注
|
318
|
+
此处只是为了演示 Utility Class 用法。真实项目中,在有 Tailwind CSS 的情况下,这种 Utility Class 没什么价值,应该通过配置 Design System 的 [**theme**](https://tailwindcss.com/docs/customizing-colors) 来增加字体颜色。
|
319
|
+
|
320
|
+
`utils.css` 也可以写成 `utils.scss` 或 `utils.less`,Modern.js 对 SCSS 和 Less 同样提供开箱即用的支持。
|
321
|
+
|
322
|
+
不过在 PostCSS 的支持下,现代 CSS 应该足以满足这些开发需求,性能相较于预处理器也更好,建议优先用 .css 文件。
|
323
|
+
:::
|
324
|
+
|
@@ -0,0 +1,169 @@
|
|
1
|
+
---
|
2
|
+
title: 添加客户端路由
|
3
|
+
---
|
4
|
+
|
5
|
+
import Tabs from '@theme/Tabs';
|
6
|
+
import TabItem from '@theme/TabItem';
|
7
|
+
|
8
|
+
上一章节中,我们学习了如何为创建 UI 组件,并添加样式。
|
9
|
+
|
10
|
+
这一章节中,我们将会学习如何添加**客户端路由**。
|
11
|
+
|
12
|
+
之前我们已经为联系人列表增加了 Archive 按钮,接下来我们添加一个客户端路由 `/archives`,访问这个路由时,只显示已存档的联系人,而原有的 `/` 继续显示所有联系人。
|
13
|
+
|
14
|
+
新建 `src/routes/archives/page.tsx` 文件:
|
15
|
+
|
16
|
+
<Tabs>
|
17
|
+
<TabItem value="macOS" label="macOS" default>
|
18
|
+
|
19
|
+
```bash
|
20
|
+
mkdir -p src/routes/archives
|
21
|
+
touch src/routes/archives/page.tsx
|
22
|
+
```
|
23
|
+
|
24
|
+
</TabItem>
|
25
|
+
<TabItem value="Windows" label="Windows">
|
26
|
+
|
27
|
+
```powershell
|
28
|
+
mkdir -p src/routes/archives
|
29
|
+
ni src/routes/archives/page.tsx
|
30
|
+
```
|
31
|
+
|
32
|
+
</TabItem>
|
33
|
+
</Tabs>
|
34
|
+
|
35
|
+
添加如下代码:
|
36
|
+
|
37
|
+
```tsx title="src/archives/page.tsx"
|
38
|
+
import { List } from 'antd';
|
39
|
+
import { Helmet } from '@modern-js/runtime/head';
|
40
|
+
import Item from '../../components/Item';
|
41
|
+
|
42
|
+
const getAvatar = (users: Array<{ name: string; email: string }>) =>
|
43
|
+
users.map(user => ({
|
44
|
+
...user,
|
45
|
+
avatar: `https://avatars.dicebear.com/v2/identicon/${user.name}.svg`,
|
46
|
+
}));
|
47
|
+
|
48
|
+
const getMockArchivedData = () =>
|
49
|
+
getAvatar([
|
50
|
+
{ name: 'Thomas', email: 'w.kccip@bllmfbgv.dm' },
|
51
|
+
{ name: 'Chow', email: 'f.lfqljnlk@ywoefljhc.af' },
|
52
|
+
]);
|
53
|
+
function Index() {
|
54
|
+
return (
|
55
|
+
<div className="container lg mx-auto">
|
56
|
+
<Helmet>
|
57
|
+
<title>Archives</title>
|
58
|
+
</Helmet>
|
59
|
+
<List
|
60
|
+
dataSource={getMockArchivedData()}
|
61
|
+
renderItem={info => <Item key={info.name} info={info} />}
|
62
|
+
/>
|
63
|
+
</div>
|
64
|
+
);
|
65
|
+
}
|
66
|
+
|
67
|
+
export default Index;
|
68
|
+
```
|
69
|
+
|
70
|
+
这里使用了 [React Helmet](https://github.com/nfl/react-helmet) 的 `Helmet` 组件,在 `src/routes/page.tsx` 中也添加 Helmet 组件:
|
71
|
+
|
72
|
+
```tsx
|
73
|
+
import { Helmet } from '@modern-js/runtime/head';
|
74
|
+
|
75
|
+
function Index() {
|
76
|
+
return (
|
77
|
+
<div className="container lg mx-auto">
|
78
|
+
<Helmet>
|
79
|
+
<title>All</title>
|
80
|
+
</Helmet>
|
81
|
+
...
|
82
|
+
</div>
|
83
|
+
);
|
84
|
+
}
|
85
|
+
```
|
86
|
+
|
87
|
+
:::info 注
|
88
|
+
Modern.js 默认集成了 react-helmet,也可以结合 SSR 使用,满足 SEO 需求。
|
89
|
+
:::
|
90
|
+
|
91
|
+
因为现在有多个页面,都需要用到前面的 Utility Class,因此我们需要把样式文件移动到 `src/routes/layout.tsx`:
|
92
|
+
|
93
|
+
```tsx
|
94
|
+
import 'tailwindcss/base.css';
|
95
|
+
import 'tailwindcss/components.css';
|
96
|
+
import 'tailwindcss/utilities.css';
|
97
|
+
import '../styles/utils.css';
|
98
|
+
```
|
99
|
+
|
100
|
+
执行 `pnpm run dev`,访问 `http://localhost:8080`,可以看到完整的联系人,页面的标题是 All:
|
101
|
+
|
102
|
+

|
103
|
+
|
104
|
+
访问 `http://localhost:8080/archives`,只会看到已存档的联系人,页面的标题是 Archives:
|
105
|
+
|
106
|
+

|
107
|
+
|
108
|
+
查看页面 HTML 源码,可以看到两个页面的内容是一样,是在客户端针对不同 URL 渲染不同内容。
|
109
|
+
|
110
|
+
**接下来我们增加一个简单的导航栏,让用户能在两个列表之间切换**。
|
111
|
+
|
112
|
+
打开 `src/routes/layout.tsx`,在顶部导入 Radio 组件:
|
113
|
+
|
114
|
+
```tsx
|
115
|
+
import { Radio } from 'antd';
|
116
|
+
```
|
117
|
+
|
118
|
+
然后将 UI 最顶部进行修改,增加一组单选框
|
119
|
+
|
120
|
+
```tsx {4-9}
|
121
|
+
export default function Layout() {
|
122
|
+
return (
|
123
|
+
<div>
|
124
|
+
<div className="h-16 p-2 flex items-center justify-center">
|
125
|
+
<Radio.Group onChange={handleSetList} value={currentList}>
|
126
|
+
<Radio value="/">All</Radio>
|
127
|
+
<Radio value="/archives">Archives</Radio>
|
128
|
+
</Radio.Group>
|
129
|
+
</div>
|
130
|
+
<Outlet />
|
131
|
+
</div>
|
132
|
+
);
|
133
|
+
}
|
134
|
+
```
|
135
|
+
|
136
|
+
然后我们来实现 `currentList` 和 `handleSetList`。
|
137
|
+
|
138
|
+
引入三个 React Hook:`useState` 和 `useNavigate` 和 `useParams`,以及 Ant Design 的事件类型定义:
|
139
|
+
|
140
|
+
```js
|
141
|
+
import { useState } from 'react';
|
142
|
+
import { Radio, RadioChangeEvent } from 'antd';
|
143
|
+
import { Outlet, useLocation, useNavigate } from "@modern-js/runtime/router";
|
144
|
+
```
|
145
|
+
|
146
|
+
最后在 Layout 组件里增加局部状态和相关逻辑:
|
147
|
+
|
148
|
+
```tsx {2-9}
|
149
|
+
export default function Layout() {
|
150
|
+
const navigate = useNavigate();
|
151
|
+
const location = useLocation();
|
152
|
+
const [currentList, setList] = useState(location.pathname || '/');
|
153
|
+
const handleSetList = (e: RadioChangeEvent) => {
|
154
|
+
const { value } = e.target;
|
155
|
+
setList(value);
|
156
|
+
navigate(value);
|
157
|
+
};
|
158
|
+
return (
|
159
|
+
...
|
160
|
+
}
|
161
|
+
```
|
162
|
+
|
163
|
+
到这里就已经完成了页面导航栏实现,执行 `pnpm run dev` 查看效果:
|
164
|
+
|
165
|
+

|
166
|
+
|
167
|
+
点击导航栏中 Archives,可以看到单选框的选中状态和 URL 都会变化,页面没有刷新,只发生了 CSR。
|
168
|
+
|
169
|
+
通过 URL 访问两个页面,可以看到 HTML 内容是不同的,这是因为在 SSR 阶段页面就执行了客户端路由的逻辑,HTML 里已经包含了最终的渲染结果。
|