@modern-js/main-doc 0.0.0-nightly-20231007160533 → 0.0.0-nightly-20231009160555
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/docs/en/apis/app/hooks/src/routes.mdx +1 -1
- package/docs/en/guides/advanced-features/rspack-start.mdx +1 -1
- package/docs/en/guides/advanced-features/ssr.mdx +17 -17
- package/docs/en/guides/basic-features/data/_category_.json +4 -0
- package/docs/en/guides/basic-features/{data-fetch.mdx → data/data-fetch.mdx} +35 -27
- package/docs/en/guides/basic-features/data/data-write.mdx +241 -0
- package/docs/en/guides/basic-features/routes.mdx +7 -7
- package/docs/en/tutorials/first-app/c05-loader.mdx +2 -2
- package/docs/en/tutorials/first-app/c06-model.mdx +4 -4
- package/docs/en/tutorials/first-app/c07-container.mdx +3 -3
- package/docs/zh/apis/app/hooks/src/routes.mdx +1 -1
- package/docs/zh/guides/advanced-features/rspack-start.mdx +1 -1
- package/docs/zh/guides/advanced-features/ssr.mdx +16 -16
- package/docs/zh/guides/basic-features/data/_category_.json +4 -0
- package/docs/zh/guides/basic-features/{data-fetch.mdx → data/data-fetch.md} +31 -27
- package/docs/zh/guides/basic-features/data/data-write.mdx +236 -0
- package/docs/zh/guides/basic-features/routes.mdx +7 -7
- package/docs/zh/tutorials/first-app/c05-loader.mdx +2 -2
- package/docs/zh/tutorials/first-app/c06-model.mdx +3 -3
- package/docs/zh/tutorials/first-app/c07-container.mdx +3 -3
- package/package.json +5 -5
@@ -15,7 +15,7 @@ Because the two pages need to share the same set of state (point of contact tabu
|
|
15
15
|
|
16
16
|
Modern.js support obtaining data through Data Loader in `layout.tsx`, we first move the data acquisition part of the code to `src/routes/layout.tsx`:
|
17
17
|
|
18
|
-
```ts title="src/routes/layout.
|
18
|
+
```ts title="src/routes/layout.data.ts"
|
19
19
|
export type LoaderData = {
|
20
20
|
code: number;
|
21
21
|
data: {
|
@@ -25,7 +25,7 @@ export type LoaderData = {
|
|
25
25
|
}[];
|
26
26
|
};
|
27
27
|
|
28
|
-
export
|
28
|
+
export const loader = async (): Promise<LoaderData> => {
|
29
29
|
const data = new Array(20).fill(0).map(() => {
|
30
30
|
const firstName = name.firstName();
|
31
31
|
return {
|
@@ -58,7 +58,7 @@ import 'tailwindcss/base.css';
|
|
58
58
|
import 'tailwindcss/components.css';
|
59
59
|
import 'tailwindcss/utilities.css';
|
60
60
|
import '../styles/utils.css';
|
61
|
-
import type { LoaderData } from './layout.
|
61
|
+
import type { LoaderData } from './layout.data';
|
62
62
|
|
63
63
|
export default function Layout() {
|
64
64
|
const { data } = useLoaderData() as LoaderData;
|
@@ -54,7 +54,7 @@ sidebar_position: 2
|
|
54
54
|
|
55
55
|
在组件中,可以通过 [useParams](/apis/app/runtime/router/router#useparams) 获取对应命名的参数。
|
56
56
|
|
57
|
-
在使用 [loader](/guides/basic-features/data-fetch#loader-函数) 函数获取数据时,`params` 会作为 `loader` 函数的入参,通过 `params` 的属性可以获取到对应的参数。
|
57
|
+
在使用 [loader](/guides/basic-features/data/data-fetch#loader-函数) 函数获取数据时,`params` 会作为 `loader` 函数的入参,通过 `params` 的属性可以获取到对应的参数。
|
58
58
|
|
59
59
|
## 布局组件
|
60
60
|
|
@@ -26,7 +26,7 @@ import InitRspackApp from '@site-docs/components/init-rspack-app';
|
|
26
26
|
在使用 Rspack 作为打包工具时,由于部分能力尚在开发中,以下 features 暂时无法使用,我们将在未来提供支持:
|
27
27
|
|
28
28
|
- Storybook 调试
|
29
|
-
- 客户端渲染(CSR)使用 [useLoader](/guides/basic-features/data-fetch.html)
|
29
|
+
- 客户端渲染(CSR)使用 [useLoader](/guides/basic-features/data/data-fetch.html)
|
30
30
|
|
31
31
|
:::
|
32
32
|
|
@@ -22,8 +22,8 @@ export default defineConfig({
|
|
22
22
|
|
23
23
|
Modern.js 中提供了 Data Loader,方便开发者在 SSR、CSR 下同构的获取数据。每个路由模块,如 `layout.tsx` 和 `page.tsx` 都可以定义自己的 Data Loader:
|
24
24
|
|
25
|
-
```ts title="src/routes/page.
|
26
|
-
export
|
25
|
+
```ts title="src/routes/page.data.ts"
|
26
|
+
export const loader = () => {
|
27
27
|
return {
|
28
28
|
message: 'Hello World',
|
29
29
|
};
|
@@ -48,7 +48,7 @@ Modern.js 打破传统的 SSR 开发模式,提供了用户无感的 SSR 开发
|
|
48
48
|
|
49
49
|
1. 当以客户端路由的方式请求页面时,Modern.js 会发送一个 HTTP 请求,服务端接收到请求后执行页面对应的 Data Loader 函数,然后将执行结果作为请求的响应返回浏览器。
|
50
50
|
|
51
|
-
2. 使用 Data Loader 时,数据获取发生在渲染前,Modern.js 也仍然支持在组件渲染时获取数据。更多相关内容可以查看[数据获取](/guides/basic-features/data-fetch)。
|
51
|
+
2. 使用 Data Loader 时,数据获取发生在渲染前,Modern.js 也仍然支持在组件渲染时获取数据。更多相关内容可以查看[数据获取](/guides/basic-features/data/data-fetch)。
|
52
52
|
|
53
53
|
:::
|
54
54
|
|
@@ -147,8 +147,8 @@ SPR 利用预渲染与缓存技术,为 SSR 页面提供静态 Web 的响应性
|
|
147
147
|
|
148
148
|
这里模拟一个使用 `useLoaderData` API 的组件,Data Loader 中的请求需要消耗 2s 时间。
|
149
149
|
|
150
|
-
```tsx title="page.
|
151
|
-
export
|
150
|
+
```tsx title="page.data.ts"
|
151
|
+
export const loader = async () => {
|
152
152
|
await new Promise((resolve, reject) => {
|
153
153
|
setTimeout(() => {
|
154
154
|
resolve(null);
|
@@ -297,9 +297,9 @@ export const loader = () => {
|
|
297
297
|
|
298
298
|
上述两种方式,都会为开发者带来一些心智负担。在真实的业务中,我们发现大多数的 Node / Web 代码混用都出现在数据请求中。
|
299
299
|
|
300
|
-
因此,Modern.js 基于[嵌套路由](/guides/basic-features/routes)开发设计了[更简单的方案](/guides/basic-features/data-fetch)来分离 CSR 和 SSR 的代码。
|
300
|
+
因此,Modern.js 基于[嵌套路由](/guides/basic-features/routes)开发设计了[更简单的方案](/guides/basic-features/data/data-fetch)来分离 CSR 和 SSR 的代码。
|
301
301
|
|
302
|
-
我们可以通过独立文件来分离**数据请求**与**组件代码**。在 `routes/page.tsx` 中编写组件逻辑,在 `routes/page.
|
302
|
+
我们可以通过独立文件来分离**数据请求**与**组件代码**。在 `routes/page.tsx` 中编写组件逻辑,在 `routes/page.data.ts` 中编写数据请求逻辑。
|
303
303
|
|
304
304
|
```ts title="routes/page.tsx"
|
305
305
|
export default Page = () => {
|
@@ -307,9 +307,9 @@ export default Page = () => {
|
|
307
307
|
}
|
308
308
|
```
|
309
309
|
|
310
|
-
```ts title="routes/page.
|
310
|
+
```ts title="routes/page.data.tsx"
|
311
311
|
import fse from 'fs-extra';
|
312
|
-
export
|
312
|
+
export const loader = () => {
|
313
313
|
const file = fse.readFileSync('./myfile');
|
314
314
|
return {
|
315
315
|
...
|
@@ -319,7 +319,7 @@ export default () => {
|
|
319
319
|
|
320
320
|
## 接口请求
|
321
321
|
|
322
|
-
在 SSR 中发起接口请求时,开发者有时自己封装了同构的请求工具。部分接口需要传递用户 Cookie,开发者可以通过 [`useRuntimeContext`](/guides/basic-features/data-fetch#route-loader) API 获取到请求头来实现。
|
322
|
+
在 SSR 中发起接口请求时,开发者有时自己封装了同构的请求工具。部分接口需要传递用户 Cookie,开发者可以通过 [`useRuntimeContext`](/guides/basic-features/data/data-fetch#route-loader) API 获取到请求头来实现。
|
323
323
|
|
324
324
|
需要注意的是,此时获取到的是 HTML 请求的请求头,不一定适用于接口请求,因此**千万不能**透传所有请求头。并且,一些后端接口,或是通用网关,会根据请求头中的信息做校验,全量透传容易出现各种难以排查的问题,推荐**按需透传**。
|
325
325
|
|
@@ -349,7 +349,7 @@ Modern.js 的流式渲染基于 React Router 实现,主要涉及 API 有:
|
|
349
349
|
|
350
350
|
### 异步获取数据
|
351
351
|
|
352
|
-
```ts title="page.
|
352
|
+
```ts title="page.data.ts"
|
353
353
|
import { defer, type LoaderFunctionArgs } from '@modern-js/runtime/router';
|
354
354
|
|
355
355
|
interface User {
|
@@ -361,7 +361,7 @@ export interface Data {
|
|
361
361
|
data: User;
|
362
362
|
}
|
363
363
|
|
364
|
-
export
|
364
|
+
export const loader = ({ params }: LoaderFunctionArgs) => {
|
365
365
|
const userId = params.id;
|
366
366
|
|
367
367
|
const user = new Promise<User>(resolve => {
|
@@ -382,10 +382,10 @@ export default ({ params }: LoaderFunctionArgs) => {
|
|
382
382
|
|
383
383
|
`defer` 还可以同时接收异步数据和同步数据。例如:
|
384
384
|
|
385
|
-
```ts title="page.
|
385
|
+
```ts title="page.data.ts"
|
386
386
|
// 省略部分代码
|
387
387
|
|
388
|
-
export
|
388
|
+
export const loader = ({ params }: LoaderFunctionArgs) => {
|
389
389
|
const userId = params.id;
|
390
390
|
|
391
391
|
const user = new Promise<User>(resolve => {
|
@@ -419,7 +419,7 @@ export default ({ params }: LoaderFunctionArgs) => {
|
|
419
419
|
```tsx title="page.tsx"
|
420
420
|
import { Await, useLoaderData } from '@modern-js/runtime/router';
|
421
421
|
import { Suspense } from 'react';
|
422
|
-
import type { Data } from './page.
|
422
|
+
import type { Data } from './page.data';
|
423
423
|
|
424
424
|
const Page = () => {
|
425
425
|
const data = useLoaderData() as Data;
|
@@ -452,7 +452,7 @@ export default Page;
|
|
452
452
|
:::warning 注意
|
453
453
|
从 Data Loader 文件导入类型时,需要使用 import type 语法,保证只导入类型信息,这样可以避免 Data Loader 的代码打包到前端产物的 bundle 文件中。
|
454
454
|
|
455
|
-
所以,这里的导入方式为:`import type { Data } from './page.
|
455
|
+
所以,这里的导入方式为:`import type { Data } from './page.data'`;
|
456
456
|
|
457
457
|
:::
|
458
458
|
|
@@ -11,16 +11,22 @@ Modern.js 中提供了开箱即用的数据获取能力,开发者可以通过
|
|
11
11
|
|
12
12
|
## Data Loader(推荐)
|
13
13
|
|
14
|
-
Modern.js 推荐使用约定式路由做路由的管理,通过 Modern.js 的[约定式(嵌套)路由](/guides/basic-features/routes#约定式路由),每个路由组件(`layout.ts` 或 `page.ts`)可以有一个同名的 `
|
14
|
+
Modern.js 推荐使用约定式路由做路由的管理,通过 Modern.js 的[约定式(嵌套)路由](/guides/basic-features/routes#约定式路由),每个路由组件(`layout.ts` 或 `page.ts`)可以有一个同名的 `data` 文件,该 `data` 文件可以导出一个 `loader` 函数,函数会在组件渲染之前执行,为路由组件提供数据。
|
15
15
|
|
16
16
|
:::info
|
17
17
|
Modern.js v1 支持通过 [useLoader](#useloader(旧版)) 获取数据,这已经不是我们推荐的用法,除迁移过程外,不推荐两者混用。
|
18
18
|
|
19
19
|
:::
|
20
20
|
|
21
|
+
:::warning
|
22
|
+
- 在之前的版本中,Modern.js Data Loader 是定义在 `loader` 文件中的,在之后的版本中,我们推荐定义在 `data` 文件中,同时我们会保持对 `loader` 文件的兼容。
|
23
|
+
- 在 `data` 文件中,对应的 `loader` 需要具名导出。
|
24
|
+
|
25
|
+
:::
|
26
|
+
|
21
27
|
### 基础示例
|
22
28
|
|
23
|
-
路由组件如 `layout.ts` 或 `page.ts`,可以定义同名的 `
|
29
|
+
路由组件如 `layout.ts` 或 `page.ts`,可以定义同名的 `data` 文件,`data` 文件中导出一个 `loader` 函数,该函数提供组件所需的数据,然后在路由组件中通过 `useLoaderData` 函数获取数据,如下面示例:
|
24
30
|
|
25
31
|
```bash
|
26
32
|
.
|
@@ -28,16 +34,16 @@ Modern.js v1 支持通过 [useLoader](#useloader(旧版)) 获取数据,这
|
|
28
34
|
├── layout.tsx
|
29
35
|
└── user
|
30
36
|
├── layout.tsx
|
31
|
-
├── layout.
|
37
|
+
├── layout.data.ts
|
32
38
|
├── page.tsx
|
33
|
-
└── page.
|
39
|
+
└── page.data.ts
|
34
40
|
```
|
35
41
|
|
36
42
|
在文件中定义以下代码:
|
37
43
|
|
38
44
|
```ts title="routes/user/page.tsx"
|
39
45
|
import { useLoaderData } from '@modern-js/runtime/router';
|
40
|
-
import type { ProfileData } from './page.
|
46
|
+
import type { ProfileData } from './page.data.ts';
|
41
47
|
|
42
48
|
export default function UserPage() {
|
43
49
|
const profileData = useLoaderData() as ProfileData;
|
@@ -45,19 +51,19 @@ export default function UserPage() {
|
|
45
51
|
}
|
46
52
|
```
|
47
53
|
|
48
|
-
```ts title="routes/user/page.
|
54
|
+
```ts title="routes/user/page.data.ts"
|
49
55
|
export type ProfileData = {
|
50
56
|
/* some types */
|
51
57
|
};
|
52
58
|
|
53
|
-
export
|
59
|
+
export const loader = async (): Promise<ProfileData> => {
|
54
60
|
const res = await fetch('https://api/user/profile');
|
55
61
|
return await res.json();
|
56
62
|
};
|
57
63
|
```
|
58
64
|
|
59
65
|
:::caution
|
60
|
-
这里路由组件和 `
|
66
|
+
这里路由组件和 `data` 文件共享类型,要使用 `import type` 语法。
|
61
67
|
|
62
68
|
:::
|
63
69
|
|
@@ -81,10 +87,10 @@ export default async (): Promise<ProfileData> => {
|
|
81
87
|
当路由文件通过 `[]` 时,会作为[动态路由](/guides/basic-features/routes#动态路由),动态路由片段会作为参数传入 `loader` 函数:
|
82
88
|
|
83
89
|
```tsx
|
84
|
-
// routes/user/[id]/page.
|
90
|
+
// routes/user/[id]/page.data.ts
|
85
91
|
import { LoaderFunctionArgs } from '@modern-js/runtime/router';
|
86
92
|
|
87
|
-
export
|
93
|
+
export const loader = async ({ params }: LoaderFunctionArgs) => {
|
88
94
|
const { id } = params;
|
89
95
|
const res = await fetch(`https://api/user/${id}`);
|
90
96
|
return res.json();
|
@@ -100,10 +106,10 @@ export default async ({ params }: LoaderFunctionArgs) => {
|
|
100
106
|
一个常见的使用场景是通过 `request` 获取查询参数:
|
101
107
|
|
102
108
|
```tsx
|
103
|
-
// routes/user/[id]/page.
|
109
|
+
// routes/user/[id]/page.data.ts
|
104
110
|
import { LoaderFunctionArgs } from '@modern-js/runtime/router';
|
105
111
|
|
106
|
-
export
|
112
|
+
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
107
113
|
const url = new URL(request.url);
|
108
114
|
const userId = url.searchParams.get('id');
|
109
115
|
return queryUser(userId);
|
@@ -142,7 +148,7 @@ const loader = async (): Promise<ProfileData> => {
|
|
142
148
|
Modern.js 对 `fetch` API 做了 polyfill, 用于发起请求,该 API 与浏览器的 `fetch` API 一致,但是在服务端也能使用该 API 发起请求,这意味着不管是 CSR 还是 SSR,都可以使用统一的 `fetch` API 进行数据获取:
|
143
149
|
|
144
150
|
```tsx
|
145
|
-
async function loader() {
|
151
|
+
export async function loader() {
|
146
152
|
const res = await fetch('https://api/user/profile');
|
147
153
|
}
|
148
154
|
```
|
@@ -152,8 +158,8 @@ async function loader() {
|
|
152
158
|
在 `loader` 函数中,可以通过 `throw error` 或者 `throw response` 的方式处理错误,当 `loader` 函数中有错误被抛出时,Modern.js 会停止执行当前 `loader` 中的代码,并将前端 UI 切换到定义的 [`ErrorBoundary`](/guides/basic-features/routes#错误处理) 组件:
|
153
159
|
|
154
160
|
```tsx
|
155
|
-
// routes/user/profile/page.
|
156
|
-
export
|
161
|
+
// routes/user/profile/page.data.ts
|
162
|
+
export async function loader() {
|
157
163
|
const res = await fetch('https://api/user/profile');
|
158
164
|
if (!res.ok) {
|
159
165
|
throw res;
|
@@ -184,8 +190,8 @@ export default ErrorBoundary;
|
|
184
190
|
// routes/user/profile/page.tsx
|
185
191
|
import { useRouteLoaderData } from '@modern-js/runtime/router';
|
186
192
|
|
187
|
-
export
|
188
|
-
// 获取 routes/user/layout.
|
193
|
+
export function UserLayout() {
|
194
|
+
// 获取 routes/user/layout.data.ts 中 `loader` 返回的数据
|
189
195
|
const data = useRouteLoaderData('user/layout');
|
190
196
|
return (
|
191
197
|
<div>
|
@@ -220,12 +226,12 @@ export default function UserLayout() {
|
|
220
226
|
|
221
227
|
:::
|
222
228
|
|
223
|
-
创建 `user/layout.
|
229
|
+
创建 `user/layout.data.ts`,并添加以下代码:
|
224
230
|
|
225
|
-
```ts title="routes/user/layout.
|
231
|
+
```ts title="routes/user/layout.data.ts"
|
226
232
|
import { defer } from '@modern-js/runtime/router';
|
227
233
|
|
228
|
-
const loader = () =>
|
234
|
+
export const loader = () =>
|
229
235
|
defer({
|
230
236
|
userInfo: new Promise(resolve => {
|
231
237
|
setTimeout(() => {
|
@@ -236,8 +242,6 @@ const loader = () =>
|
|
236
242
|
}, 1000);
|
237
243
|
}),
|
238
244
|
});
|
239
|
-
|
240
|
-
export default loader;
|
241
245
|
```
|
242
246
|
|
243
247
|
在 `user/layout.tsx` 中添加以下代码:
|
@@ -298,12 +302,12 @@ export default () => {
|
|
298
302
|
|
299
303
|
```ts
|
300
304
|
// This won't work!
|
301
|
-
export
|
305
|
+
export const loader = async () => {
|
302
306
|
const res = fetch('https://api/user/profile');
|
303
307
|
return res.json();
|
304
308
|
};
|
305
309
|
|
306
|
-
import loader from './page.
|
310
|
+
import { loader } from './page.data.ts';
|
307
311
|
export default function RouteComp() {
|
308
312
|
const data = loader();
|
309
313
|
}
|
@@ -315,7 +319,7 @@ export default function RouteComp() {
|
|
315
319
|
// Not allowed
|
316
320
|
// routes/layout.tsx
|
317
321
|
import { useLoaderData } from '@modern-js/runtime/router';
|
318
|
-
import { ProfileData } from './page.
|
322
|
+
import { ProfileData } from './page.data.ts'; // should use "import type" instead
|
319
323
|
|
320
324
|
export const fetch = wrapFetch(fetch);
|
321
325
|
|
@@ -324,13 +328,13 @@ export default function UserPage() {
|
|
324
328
|
return <div>{profileData}</div>;
|
325
329
|
}
|
326
330
|
|
327
|
-
// routes/layout.
|
331
|
+
// routes/layout.data.ts
|
328
332
|
import { fetch } from './layout.tsx'; // should not be imported from the routing component
|
329
333
|
export type ProfileData = {
|
330
334
|
/* some types */
|
331
335
|
};
|
332
336
|
|
333
|
-
export
|
337
|
+
export const loader = async (): Promise<ProfileData> => {
|
334
338
|
const res = await fetch('https://api/user/profile');
|
335
339
|
return await res.json();
|
336
340
|
};
|
@@ -0,0 +1,236 @@
|
|
1
|
+
---
|
2
|
+
title: 数据写入
|
3
|
+
sidebar_position: 4
|
4
|
+
---
|
5
|
+
|
6
|
+
# 数据写入
|
7
|
+
|
8
|
+
在 Data Loader 章节中,介绍了 Modern.js 获取数据的方式,你可能会遇到两个问题:
|
9
|
+
1. 如何更新 Data Loader 中的数据?
|
10
|
+
2. 如何将新的数据传递到服务器?
|
11
|
+
|
12
|
+
EdenX 对此的解决方案是 DataAction。
|
13
|
+
|
14
|
+
## 基本示例
|
15
|
+
|
16
|
+
Data Action 和 Data Loader 一样,也是基于约定式路由的,通过 Modern.js 的[约定式(嵌套)路由](/guides/basic-features/routes#约定式路由),每个路由组件(`layout.ts`,`page.ts` 或 `$.tsx`)可以有一个同名的 `data` 文件,`data` 文件中可以导出一个命名为 `action` 的函数。
|
17
|
+
```bash
|
18
|
+
.
|
19
|
+
└── routes
|
20
|
+
└── user
|
21
|
+
├── layout.tsx
|
22
|
+
└── layout.data.ts
|
23
|
+
```
|
24
|
+
在文件中定义以下代码:
|
25
|
+
```ts title="routes/user/layout.data.ts"
|
26
|
+
import type { ActionFunction } from '@modern-js/runtime/router';
|
27
|
+
|
28
|
+
export const action: ActionFunction = ({ request }) => {
|
29
|
+
const newUser = await request.json();
|
30
|
+
const name = newUser.name;
|
31
|
+
return updateUserProfile(name);
|
32
|
+
}
|
33
|
+
```
|
34
|
+
|
35
|
+
```tsx title="routes/user/layout.tsx"
|
36
|
+
import {
|
37
|
+
useFetcher,
|
38
|
+
useLoaderData,
|
39
|
+
useParams,
|
40
|
+
Outlet
|
41
|
+
} from '@modern-js/runtime/router';
|
42
|
+
|
43
|
+
export default () => {
|
44
|
+
const userInfo = useLoaderData();
|
45
|
+
const { submit } = useFetcher();
|
46
|
+
const editUser = () => {
|
47
|
+
const newUser = {
|
48
|
+
name: 'Modern.js'
|
49
|
+
}
|
50
|
+
return submit(newUser, {
|
51
|
+
method: 'post',
|
52
|
+
encType: 'application/json',
|
53
|
+
})
|
54
|
+
}
|
55
|
+
return (
|
56
|
+
<div>
|
57
|
+
<button onClick={editUser}>edit user</button>
|
58
|
+
<div className="user-profile">
|
59
|
+
{userInfo}
|
60
|
+
</div>
|
61
|
+
<Outlet context={userInfo}></Outlet>
|
62
|
+
</div>
|
63
|
+
)
|
64
|
+
}
|
65
|
+
```
|
66
|
+
|
67
|
+
这里当执行 submit 后,会触发定义的 action 函数;在 action 函数中,可以通过 request (request.json,request.formData)获取提交的数据,获取数据后,再发送数据到服务端。
|
68
|
+
|
69
|
+
而 action 函数执行完,会执行 loader 函数代码,并更新对应的数据和视图。
|
70
|
+
|
71
|
+

|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
## 为什么要提供 Data Action
|
76
|
+
|
77
|
+
Modern.js 中提供 Data Action 主要是为了使 UI 和服务器的状态能保持同步,通过这种方式可以减少状态管理的负担,
|
78
|
+
传统的状态管理方式,会在客户端和远程分别持有状态:
|
79
|
+
|
80
|
+

|
81
|
+
|
82
|
+
而在 Modern.js 中,我们希望通过 Loader 和 Action 帮助开发者自动的同步客户端和服务端的状态:
|
83
|
+
|
84
|
+

|
85
|
+
|
86
|
+
如果项目中组件共享的数据主要服务端的状态,则无需在项目引入客户端状态管理库,使用 Data Loader 请求数据,通过 [`useRouteLoaderData`](/guides/basic-features/data/data-fetch.md) 在子组件中共享数据,
|
87
|
+
通过 Data Actino 修改和同步服务端的状态。
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
## `action` 函数
|
92
|
+
|
93
|
+
与 `loader` 函数一样,`action` 函数有两个入参,`params` 和 `request`:
|
94
|
+
|
95
|
+
### `params`
|
96
|
+
|
97
|
+
当路由文件通过 `[]` 时,会作为[动态路由](/guides/basic-features/routes#动态路由),动态路由片段会作为参数传入 `action` 函数:
|
98
|
+
|
99
|
+
```tsx
|
100
|
+
// routes/user/[id]/page.data.ts
|
101
|
+
import { ActionFunctionArgs } from '@modern-js/runtime/router';
|
102
|
+
|
103
|
+
export const action = async ({ params }: ActionFunctionArgs) => {
|
104
|
+
const { id } = params;
|
105
|
+
const res = await fetch(`https://api/user/${id}`);
|
106
|
+
return res.json();
|
107
|
+
};
|
108
|
+
```
|
109
|
+
|
110
|
+
当访问 `/user/123` 时,`action` 函数的参数为 `{ params: { id: '123' } }`。
|
111
|
+
|
112
|
+
|
113
|
+
### `request`
|
114
|
+
|
115
|
+
`request` 是一个 [Fetch Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) 实例。
|
116
|
+
|
117
|
+
通过 `request`,可以在 action 函数中获取到客户端提交的数据,如 `request.json()`,`request.formData()`,`request.json()` 等,
|
118
|
+
具体应该使用哪个 API,请参考[数据类型](#数据类型)。
|
119
|
+
|
120
|
+
```tsx
|
121
|
+
// routes/user/[id]/page.data.ts
|
122
|
+
import { ActionFunctionArgs } from '@modern-js/runtime/router';
|
123
|
+
|
124
|
+
export const action = async ({ request }: ActionFunctionArgs) => {
|
125
|
+
const newUser = await request.json();
|
126
|
+
return updateUser(newUser);
|
127
|
+
};
|
128
|
+
```
|
129
|
+
|
130
|
+
### 返回值
|
131
|
+
|
132
|
+
`action` 函数的返回值可以是任何可序列化的内容,也可以是一个 [Fetch Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) 实例,
|
133
|
+
可以通过 [`useActionData`](https://reactrouter.com/en/main/hooks/use-action-data) 访问 response 中内容。
|
134
|
+
|
135
|
+
|
136
|
+
## useSubmit 和 useFetcher
|
137
|
+
|
138
|
+
### 区别
|
139
|
+
|
140
|
+
你可以通过 [`useSubmit`](https://reactrouter.com/en/main/hooks/use-submit) 或 [`useFetcher`](https://reactrouter.com/en/main/hooks/use-fetcher) 调用 action,它们的区别是通过
|
141
|
+
`useSubmit` 调用 action,会触发浏览器的导航,通过 `useFetcher` 则不会触发浏览器的导航。
|
142
|
+
|
143
|
+
useSubmit:
|
144
|
+
|
145
|
+
```ts
|
146
|
+
const submit = useSubmit();
|
147
|
+
submit(null, { method: "post", action: "/logout" });
|
148
|
+
```
|
149
|
+
|
150
|
+
useFetcher:
|
151
|
+
```ts
|
152
|
+
const { submit } = useFetcher();
|
153
|
+
submit(null, { method: "post", action: "/logout" });
|
154
|
+
```
|
155
|
+
|
156
|
+
`submit` 函数有两个入参,`method` 和 `action`,`method` 相当于表单提交时的 `method`,大部分写入数据的场景下,`method` 可以传入 `post`,
|
157
|
+
`action` 用来指定触发哪个路由组件的 `action`,如果未传入 `action` 入参,默认会触发当前路由组件的 action,即 `user/page.tsx` 组件或子组件中执行 submit,
|
158
|
+
会触发 `user/page.data.ts` 中定义的 action。
|
159
|
+
|
160
|
+
:::info
|
161
|
+
这两个 API 更多的信息可参考相关文档:
|
162
|
+
- [`useSubmit`](https://reactrouter.com/en/main/hooks/use-submit)
|
163
|
+
- [`useFetcher`](https://reactrouter.com/en/main/hooks/use-fetcher)
|
164
|
+
|
165
|
+
:::
|
166
|
+
|
167
|
+
|
168
|
+
### 数据类型
|
169
|
+
|
170
|
+
`submit` 函数的第一个入参,可以接受不同类型的值。
|
171
|
+
如 `FormData`:
|
172
|
+
```ts
|
173
|
+
let formData = new FormData();
|
174
|
+
formData.append("cheese", "gouda");
|
175
|
+
submit(formData);
|
176
|
+
// In the action, you can get the data by request.json
|
177
|
+
```
|
178
|
+
|
179
|
+
或 `URLSearchParams` 类型的值:
|
180
|
+
```ts
|
181
|
+
let searchParams = new URLSearchParams();
|
182
|
+
searchParams.append("cheese", "gouda");
|
183
|
+
submit(searchParams);
|
184
|
+
// In the action, you can get the data by request.json
|
185
|
+
```
|
186
|
+
|
187
|
+
或任意 `URLSearchParams` 构造函数可接受的值
|
188
|
+
```ts
|
189
|
+
submit("cheese=gouda&toasted=yes");
|
190
|
+
submit([
|
191
|
+
["cheese", "gouda"],
|
192
|
+
["toasted", "yes"],
|
193
|
+
]);
|
194
|
+
// In the action, you can get the data by request.json
|
195
|
+
```
|
196
|
+
|
197
|
+
默认情况下,如果 `submit` 函数中的第一个入参是一个对象,对应的数据会被 encode 为 `formData`:
|
198
|
+
|
199
|
+
```ts
|
200
|
+
submit(
|
201
|
+
{ key: "value" },
|
202
|
+
{
|
203
|
+
method: "post",
|
204
|
+
encType: "application/x-www-form-urlencoded",
|
205
|
+
}
|
206
|
+
);
|
207
|
+
|
208
|
+
// In the action, you can get the data by request.formData
|
209
|
+
```
|
210
|
+
|
211
|
+
也可以指定为 json 编码:
|
212
|
+
|
213
|
+
```tsx
|
214
|
+
submit(
|
215
|
+
{ key: "value" },
|
216
|
+
{ method: "post", encType: "application/json" }
|
217
|
+
);
|
218
|
+
|
219
|
+
submit('{"key":"value"}', {
|
220
|
+
method: "post",
|
221
|
+
encType: "application/json",
|
222
|
+
});
|
223
|
+
|
224
|
+
// In the action, you can get the data by request.json
|
225
|
+
```
|
226
|
+
|
227
|
+
或提交纯文本:
|
228
|
+
```ts
|
229
|
+
submit("value", { method: "post", encType: "text/plain" });
|
230
|
+
// In the action, you can get the data by request.text
|
231
|
+
```
|
232
|
+
|
233
|
+
|
234
|
+
## CSR 和 SSR
|
235
|
+
|
236
|
+
与 Data Loader 一样,SSR 项目中,Data Action 是在服务端执行的(框架会自动发请求触发 Data Action),而在 CSR 项目中,Data Action 是在客户端执行的。
|
@@ -164,7 +164,7 @@ export const handle = {
|
|
164
164
|
|
165
165
|
```ts title="routes/layout.ts"
|
166
166
|
export default () => {
|
167
|
-
const matches = useMatches;
|
167
|
+
const matches = useMatches();
|
168
168
|
const breadcrumbs = matches.map(
|
169
169
|
matchedRoute => matchedRoute?.handle?.breadcrumbName,
|
170
170
|
);
|
@@ -189,7 +189,7 @@ export default () => {
|
|
189
189
|
|
190
190
|
在组件中,可以通过 [useParams](/apis/app/runtime/router/router#useparams) 获取对应命名的参数。
|
191
191
|
|
192
|
-
在 loader 中,params 会作为 [loader](/guides/basic-features/data-fetch#loader-函数) 的入参,通过 `params.xxx` 可以获取。
|
192
|
+
在 loader 中,params 会作为 [loader](/guides/basic-features/data/data-fetch#loader-函数) 的入参,通过 `params.xxx` 可以获取。
|
193
193
|
|
194
194
|
### 动态可选路由
|
195
195
|
|
@@ -209,7 +209,7 @@ export default () => {
|
|
209
209
|
|
210
210
|
在组件中,可以通过 [useParams](/apis/app/runtime/router/router#useparams) 获取对应命名的参数。
|
211
211
|
|
212
|
-
在 loader 中,params 会作为 [loader](/guides/basic-features/data-fetch#loader-函数) 的入参,通过 `params.xxx` 可以获取。
|
212
|
+
在 loader 中,params 会作为 [loader](/guides/basic-features/data/data-fetch#loader-函数) 的入参,通过 `params.xxx` 可以获取。
|
213
213
|
|
214
214
|
### 通配路由
|
215
215
|
|
@@ -352,12 +352,12 @@ Modern.js 建议必须有根 Layout 和根 Loading。
|
|
352
352
|
|
353
353
|
### 路由重定向
|
354
354
|
|
355
|
-
可以通过创建 [`Data Loader`](/guides/basic-features/data-fetch) 文件做路由的重定向,如有文件 `routes/user/page.tsx`,想对这个文件对应的路由做重定向,可以创建 `routes/user/page.
|
355
|
+
可以通过创建 [`Data Loader`](/guides/basic-features/data/data-fetch) 文件做路由的重定向,如有文件 `routes/user/page.tsx`,想对这个文件对应的路由做重定向,可以创建 `routes/user/page.data.ts` 文件:
|
356
356
|
|
357
|
-
```ts title="routes/user/page.
|
357
|
+
```ts title="routes/user/page.data.ts"
|
358
358
|
import { redirect } from '@modern-js/runtime/router';
|
359
359
|
|
360
|
-
export
|
360
|
+
export const loader () => {
|
361
361
|
const user = await getUser();
|
362
362
|
if (!user) {
|
363
363
|
return redirect('/login');
|
@@ -504,7 +504,7 @@ export const init = (context: RuntimeContext) => {
|
|
504
504
|
:::info
|
505
505
|
|
506
506
|
- 该功能目前仅在 Webpack 项目中支持,Rspack 项目暂不支持。
|
507
|
-
- 对数据的预加载目前只会预加载 SSR 项目中 [Data Loader](/guides/basic-features/data-fetch) 中返回的数据。
|
507
|
+
- 对数据的预加载目前只会预加载 SSR 项目中 [Data Loader](/guides/basic-features/data/data-fetch) 中返回的数据。
|
508
508
|
|
509
509
|
:::
|
510
510
|
|
@@ -18,7 +18,7 @@ pnpm add faker@5
|
|
18
18
|
pnpm add @types/faker@5 -D
|
19
19
|
```
|
20
20
|
|
21
|
-
创建 `src/routes/page.
|
21
|
+
创建 `src/routes/page.data.ts`:
|
22
22
|
|
23
23
|
```tsx
|
24
24
|
import { name, internet } from 'faker';
|
@@ -32,7 +32,7 @@ type LoaderData = {
|
|
32
32
|
}[];
|
33
33
|
};
|
34
34
|
|
35
|
-
export
|
35
|
+
export const loader = async (): Promise<LoaderData> => {
|
36
36
|
const data = new Array(20).fill(0).map(() => {
|
37
37
|
const firstName = name.firstName();
|
38
38
|
return {
|
@@ -192,9 +192,9 @@ const Item = ({
|
|
192
192
|
export default Item;
|
193
193
|
```
|
194
194
|
|
195
|
-
接下来,我们修改 `src/routes/page.tsx` 和 `src/routes/page.
|
195
|
+
接下来,我们修改 `src/routes/page.tsx` 和 `src/routes/page.data.ts`,为 `<Item>` 组件传递更多参数:
|
196
196
|
|
197
|
-
```ts title="src/routes/page.
|
197
|
+
```ts title="src/routes/page.data.ts"
|
198
198
|
export type LoaderData = {
|
199
199
|
code: number;
|
200
200
|
data: {
|
@@ -204,7 +204,7 @@ export type LoaderData = {
|
|
204
204
|
}[];
|
205
205
|
};
|
206
206
|
|
207
|
-
export
|
207
|
+
export const loader async (): Promise<LoaderData> => {
|
208
208
|
const data = new Array(20).fill(0).map(() => {
|
209
209
|
const firstName = name.firstName();
|
210
210
|
return {
|