@modern-js/main-doc 2.58.3 → 2.59.0
Sign up to get free protection for your applications and to get access to all the features.
- package/docs/en/apis/app/runtime/core/use-loader.mdx +1 -1
- package/docs/en/components/init-app.mdx +0 -1
- package/docs/en/components/init-rspack-app.mdx +0 -1
- package/docs/en/components/ssr-monitor.mdx +3 -0
- package/docs/en/configure/app/output/ssg.mdx +52 -141
- package/docs/en/configure/app/tools/tailwindcss.mdx +1 -1
- package/docs/en/guides/advanced-features/_meta.json +0 -8
- package/docs/en/guides/advanced-features/rsbuild-plugin.mdx +2 -2
- package/docs/en/guides/advanced-features/rspack-start.mdx +6 -14
- package/docs/en/guides/basic-features/_meta.json +31 -9
- package/docs/en/guides/basic-features/css/_meta.json +1 -0
- package/docs/en/guides/basic-features/css/css-in-js.mdx +34 -0
- package/docs/en/guides/basic-features/{css-modules.mdx → css/css-modules.mdx} +0 -4
- package/docs/en/guides/basic-features/css/css.mdx +25 -0
- package/docs/en/guides/basic-features/{css.mdx → css/tailwindcss.mdx} +5 -66
- package/docs/en/guides/basic-features/data/data-fetch.mdx +134 -235
- package/docs/en/guides/basic-features/data/data-write.mdx +66 -77
- package/docs/en/guides/basic-features/debug/_meta.json +1 -0
- package/docs/en/guides/basic-features/debug/rsdoctor.mdx +57 -0
- package/docs/en/guides/{advanced-features → basic-features/debug}/using-storybook.mdx +2 -0
- package/docs/en/guides/basic-features/render/_meta.json +1 -0
- package/docs/en/guides/basic-features/render/ssg.mdx +208 -0
- package/docs/en/guides/{advanced-features/ssr/cache.mdx → basic-features/render/ssr-cache.mdx} +38 -50
- package/docs/en/guides/basic-features/render/ssr.mdx +301 -0
- package/docs/en/guides/basic-features/render/streaming-ssr.mdx +230 -0
- package/docs/en/guides/basic-features/routes.mdx +275 -263
- package/docs/en/guides/basic-features/static-assets/_meta.json +1 -0
- package/docs/en/guides/basic-features/static-assets.mdx +1 -1
- package/docs/en/guides/basic-features/testing/_meta.json +1 -0
- package/docs/en/guides/basic-features/testing/cypress.mdx +95 -0
- package/docs/en/guides/basic-features/testing/jest.mdx +148 -0
- package/docs/en/guides/basic-features/testing/playwright.mdx +111 -0
- package/docs/en/guides/basic-features/testing/vitest.mdx +100 -0
- package/docs/en/guides/concept/entries.mdx +9 -2
- package/docs/en/guides/get-started/quick-start.mdx +1 -1
- package/docs/en/guides/get-started/tech-stack.mdx +4 -4
- package/docs/en/guides/topic-detail/generator/create/config.mdx +0 -10
- package/docs/en/guides/topic-detail/generator/create/use.mdx +0 -1
- package/docs/en/tutorials/first-app/c03-css.mdx +1 -1
- package/docs/zh/apis/app/runtime/core/use-loader.mdx +1 -1
- package/docs/zh/components/init-app.mdx +0 -1
- package/docs/zh/components/init-rspack-app.mdx +0 -1
- package/docs/zh/components/ssr-monitor.mdx +3 -0
- package/docs/zh/configure/app/output/ssg.mdx +49 -139
- package/docs/zh/configure/app/tools/tailwindcss.mdx +1 -1
- package/docs/zh/guides/advanced-features/_meta.json +0 -8
- package/docs/zh/guides/advanced-features/rsbuild-plugin.mdx +2 -2
- package/docs/zh/guides/advanced-features/rspack-start.mdx +7 -16
- package/docs/zh/guides/basic-features/_meta.json +31 -9
- package/docs/zh/guides/basic-features/css/_meta.json +1 -0
- package/docs/zh/guides/basic-features/css/css-in-js.mdx +34 -0
- package/docs/zh/guides/basic-features/css/css.mdx +25 -0
- package/docs/zh/guides/basic-features/{css.mdx → css/tailwindcss.mdx} +3 -64
- package/docs/zh/guides/basic-features/data/data-fetch.mdx +96 -211
- package/docs/zh/guides/basic-features/data/data-write.mdx +54 -55
- package/docs/zh/guides/basic-features/debug/_meta.json +1 -0
- package/docs/zh/guides/basic-features/debug/rsdoctor.mdx +57 -0
- package/docs/zh/guides/{advanced-features → basic-features/debug}/using-storybook.mdx +1 -1
- package/docs/zh/guides/basic-features/render/_meta.json +1 -0
- package/docs/zh/guides/basic-features/render/ssg.mdx +210 -0
- package/docs/zh/guides/{advanced-features/ssr/cache.mdx → basic-features/render/ssr-cache.mdx} +16 -26
- package/docs/zh/guides/basic-features/render/ssr.mdx +309 -0
- package/docs/zh/guides/{advanced-features/ssr/stream.mdx → basic-features/render/streaming-ssr.mdx} +22 -37
- package/docs/zh/guides/basic-features/routes.mdx +252 -237
- package/docs/zh/guides/basic-features/static-assets/_meta.json +1 -0
- package/docs/zh/guides/basic-features/static-assets.mdx +2 -6
- package/docs/zh/guides/basic-features/testing/_meta.json +1 -0
- package/docs/zh/guides/basic-features/testing/cypress.mdx +95 -0
- package/docs/zh/guides/basic-features/testing/jest.mdx +148 -0
- package/docs/zh/guides/basic-features/testing/playwright.mdx +112 -0
- package/docs/zh/guides/basic-features/testing/vitest.mdx +100 -0
- package/docs/zh/guides/concept/entries.mdx +6 -3
- package/docs/zh/guides/get-started/quick-start.mdx +1 -1
- package/docs/zh/guides/get-started/tech-stack.mdx +8 -8
- package/docs/zh/guides/topic-detail/generator/create/config.mdx +0 -10
- package/docs/zh/guides/topic-detail/generator/create/use.mdx +0 -1
- package/docs/zh/tutorials/first-app/c03-css.mdx +1 -1
- package/i18n.json +16 -4
- package/package.json +6 -6
- package/docs/en/apis/app/hooks/config/storybook.mdx +0 -37
- package/docs/en/guides/advanced-features/ssg.mdx +0 -116
- package/docs/en/guides/advanced-features/ssr/_meta.json +0 -1
- package/docs/en/guides/advanced-features/ssr/index.mdx +0 -23
- package/docs/en/guides/advanced-features/ssr/stream.mdx +0 -248
- package/docs/en/guides/advanced-features/ssr/usage.mdx +0 -341
- package/docs/en/guides/advanced-features/ssr.mdx +0 -555
- package/docs/zh/apis/app/hooks/config/storybook.mdx +0 -38
- package/docs/zh/guides/advanced-features/ssg.mdx +0 -116
- package/docs/zh/guides/advanced-features/ssr/_meta.json +0 -1
- package/docs/zh/guides/advanced-features/ssr/index.mdx +0 -23
- package/docs/zh/guides/advanced-features/ssr/usage.mdx +0 -329
- /package/docs/en/guides/basic-features/{mock.mdx → debug/mock.mdx} +0 -0
- /package/docs/en/guides/basic-features/{proxy.mdx → debug/proxy.mdx} +0 -0
- /package/docs/en/guides/basic-features/{json-files.mdx → static-assets/json-files.mdx} +0 -0
- /package/docs/en/guides/basic-features/{svg-assets.mdx → static-assets/svg-assets.mdx} +0 -0
- /package/docs/en/guides/basic-features/{wasm-assets.mdx → static-assets/wasm-assets.mdx} +0 -0
- /package/docs/zh/guides/basic-features/{css-modules.mdx → css/css-modules.mdx} +0 -0
- /package/docs/zh/guides/basic-features/{mock.mdx → debug/mock.mdx} +0 -0
- /package/docs/zh/guides/basic-features/{proxy.mdx → debug/proxy.mdx} +0 -0
- /package/docs/zh/guides/basic-features/{json-files.mdx → static-assets/json-files.mdx} +0 -0
- /package/docs/zh/guides/basic-features/{svg-assets.mdx → static-assets/svg-assets.mdx} +0 -0
- /package/docs/zh/guides/basic-features/{wasm-assets.mdx → static-assets/wasm-assets.mdx} +0 -0
@@ -0,0 +1,309 @@
|
|
1
|
+
---
|
2
|
+
title: 服务端渲染(SSR)
|
3
|
+
---
|
4
|
+
|
5
|
+
# 服务端渲染(SSR)
|
6
|
+
|
7
|
+
通过在服务器端将网页的 HTML 内容渲染成完整的网页(Server-Side Rendering,简称 SSR),然后将生成的网页发送到浏览器端,浏览器端只需要显示网页即可,不需要再进行额外的渲染。
|
8
|
+
|
9
|
+
它主要的优势在于:
|
10
|
+
|
11
|
+
- 提高首屏加载速度:SSR 可以在服务器端生成完整的网页,浏览器端只需要下载网页内容即可,不需要再进行额外的渲染,从而提高了首屏加载速度。
|
12
|
+
- 有利于 SEO:SSR 可以生成完整的 HTML 内容,搜索引擎可以直接索引 HTML 内容,从而提高网站的排名。
|
13
|
+
|
14
|
+
如果你有以下场景的需求,开发者可以考虑使用 SSR 来渲染你的页面:
|
15
|
+
|
16
|
+
1. 对首屏加载速度要求较高的网站,如电商网站、新闻网站等。
|
17
|
+
2. 对用户体验要求较高的网站,如社交网站、游戏网站等。
|
18
|
+
3. 对 SEO 要求较高的网站,如企业官网、博客等。
|
19
|
+
|
20
|
+
在 Modern.js 中,SSR 也是开箱即用的。开发者无需为 SSR 编写复杂的服务端逻辑,也无需关心 SSR 的运维,或是创建单独的服务。
|
21
|
+
|
22
|
+
除了开箱即用的 SSR 服务,为了保证开发者的开发体验,Modern.js 还具备:
|
23
|
+
|
24
|
+
- 完备的 SSR 降级策略,保证页面能够安全运行。
|
25
|
+
- 自动分割子路由,按需加载,减少首屏资源体积。
|
26
|
+
- 内置缓存系统,解决服务端负载高的问题。
|
27
|
+
|
28
|
+
## 开启 SSR
|
29
|
+
|
30
|
+
在 Modenr.js 启用 SSR 非常简单,只需要设置 [`server.ssr`](/configure/app/server/ssr) 为 `true` 即可:
|
31
|
+
|
32
|
+
```ts title="modern.config.ts"
|
33
|
+
import { defineConfig } from '@modern-js/app-tools';
|
34
|
+
|
35
|
+
export default defineConfig({
|
36
|
+
server: {
|
37
|
+
ssr: true,
|
38
|
+
},
|
39
|
+
});
|
40
|
+
```
|
41
|
+
|
42
|
+
## 数据获取
|
43
|
+
|
44
|
+
:::tip 前置阅读
|
45
|
+
如果你还不了解 Data Loader 如何使用或 Client Loader 的概念,请先阅读[数据获取](/guides/basic-features/data/data-fetch)。
|
46
|
+
:::
|
47
|
+
|
48
|
+
### 基本用法
|
49
|
+
|
50
|
+
Modern.js 中提供了 Data Loader,方便开发者在 SSR、CSR 下同构地获取数据。每个路由模块,如 `layout.tsx` 和 `page.tsx` 都可以定义自己的 Data Loader:
|
51
|
+
|
52
|
+
```ts title="src/routes/page.data.ts"
|
53
|
+
export const loader = () => {
|
54
|
+
return {
|
55
|
+
message: 'Hello World',
|
56
|
+
};
|
57
|
+
};
|
58
|
+
```
|
59
|
+
|
60
|
+
在组件中可以通过 Hooks API 的方式获取 `loader` 函数返回的数据:
|
61
|
+
|
62
|
+
```tsx
|
63
|
+
import { useLoaderData } from '@modern-js/runtime/router';
|
64
|
+
export default () => {
|
65
|
+
const data = useLoaderData();
|
66
|
+
return <div>{data.message}</div>;
|
67
|
+
};
|
68
|
+
```
|
69
|
+
|
70
|
+
|
71
|
+
### 使用 Client Loader
|
72
|
+
|
73
|
+
:::info
|
74
|
+
该功能需要 x.36.0 以上版本,推荐使用框架最新版本。
|
75
|
+
:::
|
76
|
+
|
77
|
+
默认情况下,在 SSR 应用中,`loader` 函数只会在服务端执行。但有些场景下,开发者可能期望在浏览器端发送的请求不经过 SSR 服务,直接请求数据源,例如:
|
78
|
+
|
79
|
+
1. 在浏览器端希望减少网络消耗,直接请求数据源。
|
80
|
+
2. 应用在浏览器端有数据缓存,不希望请求 SSR 服务获取数据。
|
81
|
+
|
82
|
+
Modern.js 支持在 SSR 应用中额外添加 `.data.client` 文件,同样具名导出 `loader`。此时 SSR 应用在服务端执行 Data Loader 报错降级,或浏览器端切换路由时,会像 CSR 应用一样在浏览器端执行该 `loader` 函数,而不是再向 SSR 服务发送数据请求。
|
83
|
+
|
84
|
+
```ts title="page.data.client.ts"
|
85
|
+
import cache from 'my-cache';
|
86
|
+
|
87
|
+
export async function loader({ params }) {
|
88
|
+
if (cache.has(params.id)) {
|
89
|
+
return cache.get(params.id);
|
90
|
+
}
|
91
|
+
const res = await fetch('URL_ADDRESS?id={params.id}');
|
92
|
+
return {
|
93
|
+
message: res.message,
|
94
|
+
}
|
95
|
+
}
|
96
|
+
```
|
97
|
+
|
98
|
+
:::warning
|
99
|
+
要使用 Client Loader,必须有对应的 Server Loader,且 Server Loader 必须是 `.data` 文件约定,不能是 `.loader` 文件约定。
|
100
|
+
:::
|
101
|
+
|
102
|
+
## SSR 降级
|
103
|
+
|
104
|
+
在 Modern.js 中,如果应用在 SSR 过程中出现异常,Modern.js 会自动降级到 CSR 模式,并在 CSR 重新发起数据请求,保证页面能够正常展示。SSR 降级的原因主要分为两种:
|
105
|
+
|
106
|
+
1. Data Loader 执行报错
|
107
|
+
2. React 组件在服务端渲染报错
|
108
|
+
|
109
|
+
### Data Loader 执行报错
|
110
|
+
|
111
|
+
默认情况下,如果路由对应的 `loader` 函数执行报错,框架会在服务端直接渲染 `<ErrorBoundary>` 组件,并展示错误信息,这也是社区内大多数框架的默认行为。
|
112
|
+
|
113
|
+
Modern.js 也支持通过 [`server.ssr`](/configure/app/server/ssr) 配置项中的 `loaderFailureMode` 字段,自定义降级策略。当该字段被配置为 `clientRender` 时,会直接降级到 CSR 模式,并重新发起数据请求。
|
114
|
+
|
115
|
+
此时,如果路由中定义了 Client Loader,则会优先使用 Client Loader 发起数据请求。如果重新渲染仍然出错,再展示 `<ErrorBoundary>` 组件。
|
116
|
+
|
117
|
+
{/* Todo 补个图 */}
|
118
|
+
|
119
|
+
### 组件渲染报错
|
120
|
+
|
121
|
+
当组件渲染报错时,Modern.js 会自动降级到 CSR 模式,并重新发起数据请求。如果重新渲染仍然出错,则展示 `<ErrorBoundary>` 组件。
|
122
|
+
|
123
|
+
:::tip
|
124
|
+
组件渲染报错的行为,不会受到 `loaderFailureMode` 的影响,也不会在浏览器端执行 Client Loader。
|
125
|
+
:::
|
126
|
+
|
127
|
+
{/* Todo 补个图 */}
|
128
|
+
|
129
|
+
## 日志与监控
|
130
|
+
|
131
|
+
import Monitor from '@site-docs/components/ssr-monitor';
|
132
|
+
|
133
|
+
<Monitor />
|
134
|
+
|
135
|
+
## 页面缓存
|
136
|
+
|
137
|
+
Modern.js 中内置了缓存的能力,详细请参考[渲染缓存](/guides/basic-features/render/ssr-cache)。
|
138
|
+
|
139
|
+
## 运行环境差异
|
140
|
+
|
141
|
+
SSR 应用会同时运行在服务端和浏览器端,两者在运行环境上不完全相同,存在 Web API 和 Node API 的差异。
|
142
|
+
|
143
|
+
开启 SSR 时,Modern.js 会用相同的入口,构建出 SSR Bundle 和 CSR Bundle 两份产物。因此,在 SSR Bundle 中存在 Web API,或是在 CSR Bundle 中存在 Node API 时,都可能导致运行出错。出现这类问题的场景主要是两类:
|
144
|
+
|
145
|
+
- 应用自身代码存在问题
|
146
|
+
- 应用依赖的包中存在副作用
|
147
|
+
|
148
|
+
### 自身代码问题
|
149
|
+
|
150
|
+
这种场景通常出现在应用从 CSR 迁移到 SSR,CSR 应用通常会在代码中引入 Web API。例如应用希望做全局的事件监听:
|
151
|
+
|
152
|
+
```tsx
|
153
|
+
document.addEventListener('load', () => {
|
154
|
+
console.log('document load');
|
155
|
+
});
|
156
|
+
const App = () => {
|
157
|
+
return <div>Hello World</div>;
|
158
|
+
};
|
159
|
+
export default App;
|
160
|
+
```
|
161
|
+
|
162
|
+
对于这种场景,你可以直接使用 Modern.js 内置的环境变量 `MODERN_TARGET` 进行判断,在构建时删除无用代码:
|
163
|
+
|
164
|
+
```ts
|
165
|
+
if (process.env.MODERN_TARGET === 'browser') {
|
166
|
+
document.addEventListener('load', () => {
|
167
|
+
console.log('document load');
|
168
|
+
});
|
169
|
+
}
|
170
|
+
```
|
171
|
+
|
172
|
+
开发环境打包后,SSR 产物和 CSR 产物会被编译成以下内容。因此 SSR 环境中不会再因为 Web API 报错:
|
173
|
+
|
174
|
+
```ts
|
175
|
+
// SSR 产物
|
176
|
+
if (false) {
|
177
|
+
}
|
178
|
+
|
179
|
+
// CSR 产物
|
180
|
+
if (true) {
|
181
|
+
document.addEventListener('load', () => {
|
182
|
+
console.log('document load');
|
183
|
+
});
|
184
|
+
}
|
185
|
+
```
|
186
|
+
|
187
|
+
:::note
|
188
|
+
更多内容可以查看[环境变量](/guides/basic-features/env-vars)。
|
189
|
+
:::
|
190
|
+
|
191
|
+
### 依赖中的副作用
|
192
|
+
|
193
|
+
这类场景是在 SSR 应用中随时可能出现的,因为社区中的包并不都支持在两个运行环境中运行,有些包也无需在两个环境中运行。例如在代码中引入了包 A,它内部有使用了 Web API 的副作用:
|
194
|
+
|
195
|
+
```ts title="packageA"
|
196
|
+
document.addEventListener('load', () => {
|
197
|
+
console.log('document load');
|
198
|
+
});
|
199
|
+
|
200
|
+
export const doSomething = () => {}
|
201
|
+
```
|
202
|
+
|
203
|
+
如果直接引用到组件中,会造成 CSR 加载报错,即使你已经使用环境变量进行判断,但仍然无法移除依赖中副作用的执行。
|
204
|
+
|
205
|
+
```tsx title="routes/page.tsx"
|
206
|
+
import { doSomething } from 'packageA';
|
207
|
+
|
208
|
+
export const Page = () => {
|
209
|
+
if (process.env.MODERN_TARGET === 'browser') {
|
210
|
+
doSomething();
|
211
|
+
}
|
212
|
+
return <div>Hello World</div>
|
213
|
+
}
|
214
|
+
```
|
215
|
+
|
216
|
+
Modern.js 也支持通过 `.server.` 后缀的文件来区分 SSR Bundle 和 CSR Bundle 产物的打包文件。可以创建同名的 `.ts` 和 `.server.ts` 文件做一层代理:
|
217
|
+
|
218
|
+
```ts title="a.ts"
|
219
|
+
export { doSomething } from 'packageA';
|
220
|
+
```
|
221
|
+
|
222
|
+
```ts title="a.server.ts"
|
223
|
+
export const doSomething: any = () => {};
|
224
|
+
```
|
225
|
+
|
226
|
+
在文件中直接引入 `./a`,此时 SSR 打包下会优先使用 `.server.ts` 后缀的文件,CSR 打包下会使用 `.ts` 后缀的文件。
|
227
|
+
|
228
|
+
```tsx title="routes/page.tsx"
|
229
|
+
import { doSomething } from './a'
|
230
|
+
|
231
|
+
export const Page = () => {
|
232
|
+
doSomething();
|
233
|
+
return <div>Hello World</div>
|
234
|
+
}
|
235
|
+
```
|
236
|
+
|
237
|
+
## 常见问题
|
238
|
+
|
239
|
+
### 保持渲染一致
|
240
|
+
|
241
|
+
SSR 业务需要保证在服务端渲染时的结果和浏览器端 Hydrate 的结果一致,否则很有可能出现不符合预期的渲染结果。这里通过一个例子,演示当 SSR 与 CSR 渲染不一致时出现的问题,在组件中添加以下代码:
|
242
|
+
|
243
|
+
```tsx
|
244
|
+
{
|
245
|
+
typeof window !== 'undefined' ? <div>browser content</div> : null;
|
246
|
+
}
|
247
|
+
```
|
248
|
+
|
249
|
+
启动应用后,访问页面,会发现浏览器控制台抛出警告信息:
|
250
|
+
|
251
|
+
```sh
|
252
|
+
Warning: Expected server HTML to contain a matching <div> in <div>.
|
253
|
+
```
|
254
|
+
|
255
|
+
这是 React hydrate 结果与 SSR 渲染结果不一致造成的。虽然当前页面表现正常,但在复杂应用中,很有可能因此出现 DOM 层级混乱、样式混乱等问题。
|
256
|
+
|
257
|
+
:::info
|
258
|
+
关于 React hydrate 逻辑请参考[这里](https://zh-hans.react.dev/reference/react-dom/hydrate)。
|
259
|
+
|
260
|
+
:::
|
261
|
+
|
262
|
+
应用需要保持 SSR 与 CSR 渲染结果的一致性,如果存在不一致的情况,说明这部分内容无需在 SSR 中进行渲染。Modern.js 为这类在 SSR 中不需要渲染的内容提供 [`<NoSSR>` 工具组件](/apis/app/runtime/core/use-runtime-context):
|
263
|
+
|
264
|
+
```ts
|
265
|
+
import { NoSSR } from '@modern-js/runtime/ssr';
|
266
|
+
```
|
267
|
+
|
268
|
+
在不需要进行 SSR 的元素外部,用 `NoSSR` 组件包裹:
|
269
|
+
|
270
|
+
```tsx
|
271
|
+
<NoSSR>
|
272
|
+
<div>browser content</div>
|
273
|
+
</NoSSR>
|
274
|
+
```
|
275
|
+
|
276
|
+
修改代码后,刷新页发现之前的 Waring 消失。打开浏览器开发者工具的 Network 窗口,查看返回的 HTML 文档是不包含 `NoSSR` 组件包裹的内容的。
|
277
|
+
|
278
|
+
在实际场景中,有些应用的 UI 展示会和用户设备有关,例如 [UA](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) 信息。Modern.js 也提供了
|
279
|
+
[`useRuntimeContext`](/apis/app/runtime/core/use-runtime-context) 这类 API,可以在组件中获取完整的请求信息,利用它保证 SSR 与 CSR 的渲染结果一致。
|
280
|
+
|
281
|
+
### 关注内存泄漏
|
282
|
+
|
283
|
+
:::warning 警告
|
284
|
+
在 SSR 场景下,开发者需要特别关注内存泄露问题,即使是微小的内存泄露,在大量的访问后也会对服务造成影响。
|
285
|
+
|
286
|
+
:::
|
287
|
+
|
288
|
+
SSR 时,浏览器的每次请求,都会触发服务端重新执行一次组件渲染逻辑。所以,需要避免在全局定义任何可能不断增长的数据结构,或在全局进行事件订阅,或创建不会被销毁的流。
|
289
|
+
|
290
|
+
例如以下代码,使用 [redux-observable](https://redux-observable.js.org/) 时,习惯了 CSR 的开发者通常会在组件中这样编码:
|
291
|
+
|
292
|
+
```tsx
|
293
|
+
/* 代码仅作为示例,不可运行 */
|
294
|
+
import { createEpicMiddleware, combineEpics } from 'redux-observable';
|
295
|
+
|
296
|
+
const epicMiddleware = createEpicMiddleware();
|
297
|
+
const rootEpic = combineEpics();
|
298
|
+
|
299
|
+
export default function Test() {
|
300
|
+
epicMiddleware.run(rootEpic);
|
301
|
+
return <div>Hello Modern.js</div>;
|
302
|
+
}
|
303
|
+
```
|
304
|
+
|
305
|
+
在组件外层创建 Middleware 实例 `epicMiddleware`,并在组件内部调用 `epicMiddleware.run`。
|
306
|
+
|
307
|
+
在浏览器端,这段代码不会造成任何问题,但是在 SSR 时,Middleware 实例会一直无法被销毁。每次渲染组件,调用 `epicMiddleware.run(rootEpic)` 时,都会在内部添加新的事件绑定,导致整个对象不断变大,最终对应用性能造成影响。
|
308
|
+
|
309
|
+
CSR 中这类问题不易被发觉,因此从 CSR 切换到 SSR 时,如果不确定应用是否存在这类隐患,可以对应用进行压测。
|
package/docs/zh/guides/{advanced-features/ssr/stream.mdx → basic-features/render/streaming-ssr.mdx}
RENAMED
@@ -1,11 +1,8 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
title: 流式渲染
|
2
|
+
title: 服务端流式渲染(Streaming SSR)
|
4
3
|
---
|
5
4
|
|
6
|
-
#
|
7
|
-
|
8
|
-
## 概述
|
5
|
+
# 服务端流式渲染(Streaming SSR)
|
9
6
|
|
10
7
|
流式渲染是一种新的渲染方式,它可以在用户与页面交互时,实时地更新页面内容,从而提高用户体验。
|
11
8
|
|
@@ -18,7 +15,7 @@ title: 流式渲染
|
|
18
15
|
- 拥有更好的性能控制:流式渲染可以让开发者更好地控制页面加载的优先级和顺序,从而更好地优化性能和用户体验。
|
19
16
|
- 更好的适应性:流式渲染可以更好地适应不同网络速度和设备性能,使得页面在各种环境下都能有更好的表现。
|
20
17
|
|
21
|
-
##
|
18
|
+
## 开启流式渲染
|
22
19
|
|
23
20
|
Modern.js 支持了 React 18 的流式渲染,可以通过如下配置启用:
|
24
21
|
|
@@ -40,9 +37,9 @@ Modern.js 的流式渲染基于 React Router 实现,主要涉及 API 有:
|
|
40
37
|
- [`Await`](https://reactrouter.com/en/main/components/await):用于渲染 Data Loader 返回的异步数据。
|
41
38
|
- [`useAsyncValue`](https://reactrouter.com/en/main/hooks/use-async-value):用于从最近的父级 `Await` 组件中获取数据。
|
42
39
|
|
43
|
-
|
40
|
+
## 获取数据
|
44
41
|
|
45
|
-
```ts title="page.data.ts"
|
42
|
+
```ts title="user/[id]/page.data.ts"
|
46
43
|
import { defer, type LoaderFunctionArgs } from '@modern-js/runtime/router';
|
47
44
|
|
48
45
|
interface User {
|
@@ -70,14 +67,11 @@ export const loader = ({ params }: LoaderFunctionArgs) => {
|
|
70
67
|
};
|
71
68
|
```
|
72
69
|
|
73
|
-
`user` 是一个 Promise 类型的对象,表示需要异步获取的数据,通过 `defer` 处理需要异步获取的 `user`。注意,`defer`
|
74
|
-
因此, 传入 `defer` 的参数为:`{ data: user }`
|
75
|
-
|
76
|
-
`defer` 还可以同时接收异步数据和同步数据。例如:
|
70
|
+
`user` 是一个 Promise 类型的对象,表示需要异步获取的数据,通过 `defer` 处理需要异步获取的 `user`。注意,`defer` 必须接收一个对象类型的参数,不能直接传递 Promise 对象。
|
77
71
|
|
78
|
-
|
79
|
-
// 省略部分代码
|
72
|
+
另外,`defer` 还可以同时接收异步数据和同步数据。在下述例子中,我们等待部分耗时较短的请求,在响应后通过对象数据返回,而耗时较长时间的请求,则通过 Promise 返回:
|
80
73
|
|
74
|
+
```ts title="user/[id]/page.data.ts"
|
81
75
|
export const loader = ({ params }: LoaderFunctionArgs) => {
|
82
76
|
const userId = params.id;
|
83
77
|
|
@@ -87,7 +81,7 @@ export const loader = ({ params }: LoaderFunctionArgs) => {
|
|
87
81
|
name: `user-${userId}`,
|
88
82
|
age: 18,
|
89
83
|
});
|
90
|
-
},
|
84
|
+
}, 2000);
|
91
85
|
});
|
92
86
|
|
93
87
|
const otherData = new Promise<string>(resolve => {
|
@@ -103,13 +97,13 @@ export const loader = ({ params }: LoaderFunctionArgs) => {
|
|
103
97
|
};
|
104
98
|
```
|
105
99
|
|
106
|
-
|
100
|
+
这样,应用无需等待最耗时的数据请求响应后才展示页面内容,可以优先展示部分有数据的页面内容
|
107
101
|
|
108
|
-
|
102
|
+
## 渲染数据
|
109
103
|
|
110
104
|
通过 `Await` 组件,可以获取到 Data Loader 中异步返回的数据,然后进行渲染。例如:
|
111
105
|
|
112
|
-
```tsx title="page.tsx"
|
106
|
+
```tsx title="user/[id]/page.tsx"
|
113
107
|
import { Await, useLoaderData } from '@modern-js/runtime/router';
|
114
108
|
import { Suspense } from 'react';
|
115
109
|
import type { Data } from './page.data';
|
@@ -143,22 +137,16 @@ export default Page;
|
|
143
137
|
`Suspense` 组件 `fallback` 属性设置的内容。
|
144
138
|
|
145
139
|
:::warning 注意
|
146
|
-
从
|
147
|
-
|
148
|
-
所以,这里的导入方式为:`import type { Data } from './page.data'`;
|
149
|
-
|
140
|
+
从 `page.data.ts` 文件导入类型时,需要使用 `import type` 语法,保证只导入类型信息,避免 Data Loader 的代码打包到前端产物中。
|
150
141
|
:::
|
151
142
|
|
152
|
-
|
143
|
+
在组件中,你也可以通过 `useAsyncValue` 获取 Data Loader 返回的异步数据。例如:
|
153
144
|
|
154
145
|
```tsx title='page.tsx'
|
155
146
|
import { useAsyncValue } from '@modern-js/runtime/router';
|
156
147
|
|
157
|
-
// 省略部分代码
|
158
|
-
|
159
148
|
const UserInfo = () => {
|
160
149
|
const user = useAsyncValue();
|
161
|
-
|
162
150
|
return (
|
163
151
|
<div>
|
164
152
|
name: {user.name}, age: {user.age}
|
@@ -168,7 +156,6 @@ const UserInfo = () => {
|
|
168
156
|
|
169
157
|
const Page = () => {
|
170
158
|
const data = useLoaderData() as Data;
|
171
|
-
|
172
159
|
return (
|
173
160
|
<div>
|
174
161
|
User info:
|
@@ -184,10 +171,9 @@ const Page = () => {
|
|
184
171
|
export default Page;
|
185
172
|
```
|
186
173
|
|
187
|
-
|
174
|
+
## 错误处理
|
188
175
|
|
189
|
-
`Await` 组件的 `errorElement`
|
190
|
-
例如,我们故意在 Data Loader 函数中抛出错误:
|
176
|
+
`Await` 组件的 `errorElement` 属性,可以用来处理 Data Loader 或者子组件渲染时的报错。例如,我们故意在 Data Loader 函数中抛出错误:
|
191
177
|
|
192
178
|
```ts title="page.loader.ts"
|
193
179
|
import { defer } from '@modern-js/runtime/router';
|
@@ -238,15 +224,14 @@ function ErrorElement() {
|
|
238
224
|
|
239
225
|
然而,当一个爬虫访问该页面时,它可能需要先加载所有内容,直接输出整个 HTML,而不是渐进式地加载它。
|
240
226
|
|
241
|
-
Modern.js 使用 [isbot](https://www.npmjs.com/package/isbot) 对请求的 `uesr-agent
|
227
|
+
Modern.js 使用 [isbot](https://www.npmjs.com/package/isbot) 对请求的 `uesr-agent`,以判断请求是否来自爬虫。
|
242
228
|
|
243
|
-
:::info 补充信息
|
244
|
-
|
245
|
-
1. [Deferred Data](https://reactrouter.com/en/main/guides/deferred)
|
246
|
-
2. [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37)
|
247
|
-
|
248
|
-
:::
|
249
229
|
|
250
230
|
import StreamSSRPerformance from '@site-docs/components/stream-ssr-performance';
|
251
231
|
|
252
232
|
<StreamSSRPerformance />
|
233
|
+
|
234
|
+
## 相关文档
|
235
|
+
|
236
|
+
1. [Deferred Data](https://reactrouter.com/en/main/guides/deferred)
|
237
|
+
2. [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37)
|