@modern-js/main-doc 0.0.0-next-20230301104416 → 0.0.0-next-20230301140540

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/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @modern-js/main-doc
2
2
 
3
- ## 0.0.0-next-20230301104416
3
+ ## 0.0.0-next-20230301140540
4
4
 
5
5
  ### Patch Changes
6
6
 
7
7
  - Updated dependencies [54caf4334]
8
- - @modern-js/builder-doc@0.0.0-next-20230301104416
8
+ - @modern-js/builder-doc@0.0.0-next-20230301140540
@@ -44,8 +44,9 @@ And it provides elegant degradation processing. Once the SSR request fails, it w
44
44
  However, developers still need to pay attention to the fallback of data, such as `null` values or data returns that do not as expect. Avoid React rendering errors or messy rendering results when SSR.
45
45
 
46
46
  :::info
47
- When using Data Loader, data fetching happens before rendering, Modern.js still supports fetching data when the component is rendered. See [Data Fetch](/guides/basic-features/data-fetch).
47
+ 1. When you request the page on client-side page transitions, Modern.js sends an API request to the server, which runs Data Loader function.
48
48
 
49
+ 2. When using Data Loader, data fetching happens before rendering, Modern.js still supports fetching data when the component is rendered. See [Data Fetch](/guides/basic-features/data-fetch).
49
50
  :::
50
51
 
51
52
  ## Keep Rendering Consistent
@@ -288,9 +289,9 @@ In addition, some backend interfaces, or general gateways, will verify according
288
289
 
289
290
  Be sure to filter the `host` field if you really need to pass through all request headers.
290
291
 
291
- ## Stream SSR
292
+ ## Streaming SSR
292
293
 
293
- Modern.js supports streaming rendering in React 18, the default rendering mode can be modified with the following configuration:
294
+ Modern.js supports streaming rendering in React 18. Opt in it with the following configuration:
294
295
 
295
296
  ```json
296
297
  {
@@ -302,7 +303,211 @@ Modern.js supports streaming rendering in React 18, the default rendering mode c
302
303
  }
303
304
  ```
304
305
 
305
- :::note
306
- At present Modern.js built-in data fetch does not support streaming rendering. If app need it, developers can build it according to the demo of React Stream SSR.
306
+ The streaming SSR of Modern.js is implemented based on React Router, and the main APIs involved are:
307
+
308
+ - [`defer`](https://reactrouter.com/en/main/utils/defer): This utility allows you to defer values returned from loaders by passing promises instead of resolved values.
309
+ - [`Await`](https://reactrouter.com/en/main/components/await): Used to render deferred values with automatic error handling.
310
+ - [`useAsyncValue`](https://reactrouter.com/en/main/hooks/use-async-value):Returns the resolved data from the nearest `<Await>` ancestor component.
311
+
312
+
313
+ ### Return async data
314
+
315
+ ```ts title='page.loader.ts'
316
+ import { defer, type LoaderFunctionArgs } from '@modern-js/runtime/router';
317
+
318
+ interface User {
319
+ name: string;
320
+ age: number;
321
+ }
322
+
323
+ export interface Data {
324
+ data: User;
325
+ }
326
+
327
+ export default ({ params }: LoaderFunctionArgs) => {
328
+ const userId = params.id;
329
+
330
+ const user = new Promise<User>(resolve => {
331
+ setTimeout(() => {
332
+ resolve({
333
+ name: `user-${userId}`,
334
+ age: 18,
335
+ });
336
+ }, 200);
337
+ });
338
+
339
+ return defer({ data: user });
340
+ };
341
+
342
+ ```
343
+
344
+ `user` is of `Promise` type, which means the data will be obtained asynchronously. Note that `defer` must accept an object type parameter,
345
+ therefore, the parameter passed to `defer` is `{data: user}`.
346
+
347
+ `defer` can also receive asynchronous data and synchronous data at the same time. For example:
348
+
349
+ ```ts title='page.loader.ts'
350
+
351
+ // skip some codes
352
+
353
+ export default ({ params }: LoaderFunctionArgs) => {
354
+ const userId = params.id;
355
+
356
+ const user = new Promise<User>(resolve => {
357
+ setTimeout(() => {
358
+ resolve({
359
+ name: `user-${userId}`,
360
+ age: 18,
361
+ });
362
+ }, 200);
363
+ });
364
+
365
+ const otherData = new Promise<string>(resolve => {
366
+ setTimeout(() => {
367
+ resolve('some sync data');
368
+ }, 200);
369
+ });
370
+
371
+ return defer({
372
+ data: user,
373
+ other: await otherData
374
+ });
375
+ };
376
+
377
+ ```
378
+
379
+ `await` is added before `otherData`, so the data is obtained synchronously. It can be passed to `defer` with the data `user` at the same time.
380
+
381
+
382
+ ### Render async data
383
+
384
+ Use the `Await` component to render the data returned asynchronously from the Data Loader. For example:
385
+
386
+ ```ts title='page.tsx'
387
+ import { Await, useLoaderData } from '@modern-js/runtime/router';
388
+ import { Suspense } from 'react';
389
+ import type { Data } from './page.loader';
390
+
391
+ const Page = () => {
392
+ const data = useLoaderData() as Data;
393
+
394
+ return (
395
+ <div>
396
+ User info:
397
+ <Suspense fallback={<div id="loading">loading user data ...</div>}>
398
+ <Await resolve={data.data}>
399
+ {(user) => {
400
+ return (
401
+ <div id="data">
402
+ name: {user.name}, age: {user.age}
403
+ </div>
404
+ );
405
+ }}
406
+ </Await>
407
+ </Suspense>
408
+ </div>
409
+ );
410
+ };
411
+
412
+ export default Page;
413
+ ```
414
+
415
+ `Await` needs to be wrapped inside the `Suspense` component. The `resolve` of `Await` passes in the data acquired asynchronously by the Data Loader. When the data acquisition is completed,
416
+ the obtained data is rendered through the [Render Props](https://reactjs.org/docs/render-props.html) mode. When the data acquisition is in pending status, the
417
+ content set by the `fallback` property of the `Suspense` component will display.
418
+
419
+
420
+ :::warning Warning
421
+ When importing a type from a Data Loader file, you need to use the `import type` syntax to ensure that only type information is imported, which can prevent the Data Loader code from being packaged into the client bundle.
422
+
423
+ So, here we import like this: `import type { Data } from './page.loader'`;
424
+ :::
425
+
426
+ You can also get the asynchronous data returned by Data Loader through `useAsyncValue`. For example:
427
+
428
+ ```
429
+ ```ts title='page.tsx'
430
+ import { useAsyncValue } from '@modern-js/runtime/router';
431
+
432
+ // skip some codes
433
+
434
+ const UserInfo = () => {
435
+ const user = useAsyncValue();
436
+
437
+ return (
438
+ <div>
439
+ name: {user.name}, age: {user.age}
440
+ </div>
441
+ )
442
+ }
443
+
444
+ const Page = () => {
445
+ const data = useLoaderData() as Data;
446
+
447
+ return (
448
+ <div>
449
+ User info:
450
+ <Suspense fallback={<div id="loading">loading user data ...</div>}>
451
+ <Await resolve={data.data}>
452
+ <UserInfo />
453
+ </Await>
454
+ </Suspense>
455
+ </div>
456
+ );
457
+ };
458
+
459
+ export default Page;
460
+ ```
461
+
462
+ ### Error handling
463
+
464
+ The `errorElement` property of the `Await` component can be used to handle errors thrown when the Data Loader executes or when a child component renders.
465
+ For example, we intentionally throw an error in the Data Loader function:
466
+
467
+ ```ts title='page.loader.ts'
468
+ import { defer } from '@modern-js/runtime/router';
469
+
470
+ export default () => {
471
+ const data = new Promise((resolve, reject) => {
472
+ setTimeout(() => {
473
+ reject(new Error('error occurs'));
474
+ }, 200);
475
+ });
476
+
477
+ return defer({ data });
478
+ };
479
+ ```
480
+
481
+ Then use `useAsyncError` to get the error, and assign the component used to render the error to the `errorElement` property of the `Await` component:
482
+
483
+ ```ts title='page.ts'
484
+ import { Await, useAsyncError, useLoaderData } from '@modern-js/runtime/router';
485
+ import { Suspense } from 'react';
486
+
487
+ export default function Page() {
488
+ const data = useLoaderData();
489
+
490
+ return (
491
+ <div>
492
+ Error page
493
+ <Suspense fallback={<div>loading ...</div>}>
494
+ <Await resolve={data.data} errorElement={<ErrorElement />}>
495
+ {(data: any) => {
496
+ return <div>never displayed</div>;
497
+ }}
498
+ </Await>
499
+ </Suspense>
500
+ </div>
501
+ );
502
+ }
503
+
504
+ function ErrorElement() {
505
+ const error = useAsyncError() as Error;
506
+ return <p>Something went wrong! {error.message}</p>;
507
+ }
508
+ ```
307
509
 
510
+ :::info More
511
+ 1. [Deferred Data](https://reactrouter.com/en/main/guides/deferred)
512
+ 2. [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37)
308
513
  :::
package/package.json CHANGED
@@ -11,13 +11,13 @@
11
11
  "modern",
12
12
  "modern.js"
13
13
  ],
14
- "version": "0.0.0-next-20230301104416",
14
+ "version": "0.0.0-next-20230301140540",
15
15
  "publishConfig": {
16
16
  "registry": "https://registry.npmjs.org/",
17
17
  "access": "public"
18
18
  },
19
19
  "peerDependencies": {
20
- "@modern-js/builder-doc": "0.0.0-next-20230301104416"
20
+ "@modern-js/builder-doc": "0.0.0-next-20230301140540"
21
21
  },
22
22
  "devDependencies": {
23
23
  "ts-node": "^10",
@@ -25,7 +25,7 @@
25
25
  "fs-extra": "^10",
26
26
  "@types/node": "^16",
27
27
  "@types/fs-extra": "^9",
28
- "@modern-js/builder-doc": "0.0.0-next-20230301104416"
28
+ "@modern-js/builder-doc": "0.0.0-next-20230301140540"
29
29
  },
30
30
  "scripts": {
31
31
  "build": "npx ts-node ./scripts/sync.ts"
@@ -43,10 +43,13 @@ Modern.js 打破传统的 SSR 开发模式,提供了用户无感的 SSR 开发
43
43
  不过,开发者仍然需要关注数据的兜底处理,例如 `null` 值或不符合预期的数据返回。避免在 SSR 时产生 React 渲染错误或是返回凌乱的渲染结果。
44
44
 
45
45
  :::info 补充信息
46
- 使用 Data Loader 时,数据获取发生在渲染前,Modern.js 也仍然支持在组件渲染时获取数据。更多相关内容可以查看[数据获取](/guides/basic-features/data-fetch)。
46
+ 1. 当以客户端路由的方式请求页面时,Modern.js 会发送一个 HTTP 请求,服务端接收到请求后执行页面对应的 Data Loader 函数,然后将执行结果作为请求的响应返回浏览器。
47
47
 
48
+ 2. 使用 Data Loader 时,数据获取发生在渲染前,Modern.js 也仍然支持在组件渲染时获取数据。更多相关内容可以查看[数据获取](/guides/basic-features/data-fetch)。
48
49
  :::
49
50
 
51
+
52
+
50
53
  ## 保持渲染一致
51
54
 
52
55
  有些业务中,通常需要根据当前的运行容器环境特征做不同的 UI 展示,例如 [UA](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) 信息。如果处理不够仔细,此时很有可能出现不符合预期的渲染结果。
@@ -277,7 +280,7 @@ export const loader = () => {
277
280
 
278
281
  ## 流式渲染
279
282
 
280
- Modern.js 支持了 React 18 的流式渲染,可以通过如下配置修改默认的渲染模式:
283
+ Modern.js 支持了 React 18 的流式渲染,可以通过如下配置启用:
281
284
 
282
285
  ```json
283
286
  {
@@ -289,7 +292,210 @@ Modern.js 支持了 React 18 的流式渲染,可以通过如下配置修改默
289
292
  }
290
293
  ```
291
294
 
292
- :::note
293
- 目前 Modern.js 内置的数据获取方式还未支持流式渲染,如迫切需要开发者可以按照 React Stream SSR 的 Demo 自建。
295
+ Modern.js 的流式渲染基于 React Router 实现,主要涉及 API 有:
296
+
297
+ - [`defer`](https://reactrouter.com/en/main/utils/defer):在 Data Loader 中使用,用于支持异步获取数据。
298
+ - [`Await`](https://reactrouter.com/en/main/components/await):用于渲染 Data Loader 返回的异步数据。
299
+ - [`useAsyncValue`](https://reactrouter.com/en/main/hooks/use-async-value):用于从最近的父级 `Await` 组件中获取数据。
300
+
301
+
302
+ ### 异步获取数据
303
+
304
+ ```ts title='page.loader.ts'
305
+ import { defer, type LoaderFunctionArgs } from '@modern-js/runtime/router';
306
+
307
+ interface User {
308
+ name: string;
309
+ age: number;
310
+ }
311
+
312
+ export interface Data {
313
+ data: User;
314
+ }
315
+
316
+ export default ({ params }: LoaderFunctionArgs) => {
317
+ const userId = params.id;
318
+
319
+ const user = new Promise<User>(resolve => {
320
+ setTimeout(() => {
321
+ resolve({
322
+ name: `user-${userId}`,
323
+ age: 18,
324
+ });
325
+ }, 200);
326
+ });
327
+
328
+ return defer({ data: user });
329
+ };
330
+
331
+ ```
332
+
333
+ `user` 是一个 Promise 类型的对象,表示需要异步获取的数据,通过 `defer` 处理需要异步获取的 `user`。注意,`defer` 必须接收一个对象类型的参数,
334
+ 因此, 传入 `defer` 的参数为 `{data: user}`。
335
+
336
+ `defer` 还可以同时接收异步数据和同步数据。例如:
337
+
338
+ ```ts title='page.loader.ts'
339
+
340
+ // 省略部分代码
341
+
342
+ export default ({ params }: LoaderFunctionArgs) => {
343
+ const userId = params.id;
344
+
345
+ const user = new Promise<User>(resolve => {
346
+ setTimeout(() => {
347
+ resolve({
348
+ name: `user-${userId}`,
349
+ age: 18,
350
+ });
351
+ }, 200);
352
+ });
353
+
354
+ const otherData = new Promise<string>(resolve => {
355
+ setTimeout(() => {
356
+ resolve('some sync data');
357
+ }, 200);
358
+ });
359
+
360
+ return defer({
361
+ data: user,
362
+ other: await otherData
363
+ });
364
+ };
365
+
366
+ ```
367
+
368
+ `otherData` 前加了 `await`,所以是同步获取的数据,它可以和异步获取的数据 `user` 同时传入 `defer`。
369
+
370
+
371
+ ### 渲染异步数据
372
+
373
+ 通过 `Await` 组件,可以获取到 Data Loader 中异步返回的数据,然后进行渲染。例如:
374
+
375
+ ```ts title='page.tsx'
376
+ import { Await, useLoaderData } from '@modern-js/runtime/router';
377
+ import { Suspense } from 'react';
378
+ import type { Data } from './page.loader';
379
+
380
+ const Page = () => {
381
+ const data = useLoaderData() as Data;
382
+
383
+ return (
384
+ <div>
385
+ User info:
386
+ <Suspense fallback={<div id="loading">loading user data ...</div>}>
387
+ <Await resolve={data.data}>
388
+ {(user) => {
389
+ return (
390
+ <div id="data">
391
+ name: {user.name}, age: {user.age}
392
+ </div>
393
+ );
394
+ }}
395
+ </Await>
396
+ </Suspense>
397
+ </div>
398
+ );
399
+ };
294
400
 
401
+ export default Page;
402
+ ```
403
+
404
+ `Await` 需要包裹在 `Suspense` 组件内部,`Await` 的 `resolve` 传入的是 Data Loader 异步获取的数据,当数据获取完成后,
405
+ 通过 [Render Props](https://reactjs.org/docs/render-props.html) 模式,渲染获取到的数据。在数据的获取阶段,将展示
406
+ `Suspense` 组件 `fallback` 属性设置的内容。
407
+
408
+ :::warning 注意
409
+ 从 Data Loader 文件导入类型时,需要使用 import type 语法,保证只导入类型信息,这样可以避免 Data Loader 的代码打包到前端产物的 bundle 文件中。
410
+
411
+ 所以,这里的导入方式为:`import type { Data } from './page.loader'`;
412
+ :::
413
+
414
+ 也可以通过 `useAsyncValue` 获取 Data Loader 返回的异步数据。例如:
415
+
416
+ ```
417
+ ```ts title='page.tsx'
418
+ import { useAsyncValue } from '@modern-js/runtime/router';
419
+
420
+ // 省略部分代码
421
+
422
+ const UserInfo = () => {
423
+ const user = useAsyncValue();
424
+
425
+ return (
426
+ <div>
427
+ name: {user.name}, age: {user.age}
428
+ </div>
429
+ )
430
+ }
431
+
432
+ const Page = () => {
433
+ const data = useLoaderData() as Data;
434
+
435
+ return (
436
+ <div>
437
+ User info:
438
+ <Suspense fallback={<div id="loading">loading user data ...</div>}>
439
+ <Await resolve={data.data}>
440
+ <UserInfo />
441
+ </Await>
442
+ </Suspense>
443
+ </div>
444
+ );
445
+ };
446
+
447
+ export default Page;
448
+ ```
449
+
450
+ ### 错误处理
451
+
452
+ `Await` 组件的 `errorElement` 属性,可以用来处理当 Data Loader 执行时,或者子组件渲染时抛出的错误。
453
+ 例如,我们故意在 Data Loader 函数中抛出错误:
454
+
455
+ ```ts title='page.loader.ts'
456
+ import { defer } from '@modern-js/runtime/router';
457
+
458
+ export default () => {
459
+ const data = new Promise((resolve, reject) => {
460
+ setTimeout(() => {
461
+ reject(new Error('error occurs'));
462
+ }, 200);
463
+ });
464
+
465
+ return defer({ data });
466
+ };
467
+ ```
468
+
469
+ 然后通过 `useAsyncError` 获取错误,并将用于渲染错误信息的组件赋值给 `Await` 组件的 `errorElement` 属性:
470
+
471
+ ```ts title='page.ts'
472
+ import { Await, useAsyncError, useLoaderData } from '@modern-js/runtime/router';
473
+ import { Suspense } from 'react';
474
+
475
+ export default function Page() {
476
+ const data = useLoaderData();
477
+
478
+ return (
479
+ <div>
480
+ Error page
481
+ <Suspense fallback={<div>loading ...</div>}>
482
+ <Await resolve={data.data} errorElement={<ErrorElement />}>
483
+ {(data: any) => {
484
+ return <div>never displayed</div>;
485
+ }}
486
+ </Await>
487
+ </Suspense>
488
+ </div>
489
+ );
490
+ }
491
+
492
+ function ErrorElement() {
493
+ const error = useAsyncError() as Error;
494
+ return <p>Something went wrong! {error.message}</p>;
495
+ }
496
+ ```
497
+
498
+ :::info 补充信息
499
+ 1. [Deferred Data](https://reactrouter.com/en/main/guides/deferred)
500
+ 2. [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37)
295
501
  :::