utquidem
Version:
The meta-framework suite designed from scratch for frontend-focused modern web development.
222 lines (155 loc) • 7.4 kB
Markdown
---
sidebar_position: 1
title: Loader 管理
---
## 什么是 Loader
开发 SSR 应用,服务端预加载数据是一个必不可少的流程,它通常包含以下 3 个阶段:
1. 服务端预先请求应用渲染所需要的数据,使用获取到的数据在服务端生成完整 HTML。
2. 服务端返回 HTML 的同时,把请求到的数据同步到客户端(一般是通过将数据序列化,插入到返回给客户端的 HTML 的 Script 脚本中)。
3. 客户端复用服务端预先请求到的数据,执行后续逻辑。
Modern.js 提出了 Loader 的概念,将上述行为进行抽象。通过 `useLoader` 这样一个同构的 API,处理 SSR 应用的数据请求,Modern.js 会自动处理上述三个流程。
## 简单的 useLoader 使用
```ts
function App() {
const { data, loading, error } = useLoader(async () => {
return fetch(url);
});
return ...;
}
```
`useLoader` 返回: `data`(成功时返回的数据)、`loading`(Loader 是否正在加载中)、`error`(失败时返回的错误信息)。
## 如何做服务端数据的同步
`useLoader` 是一个 Hook 函数,可以在任意 React 组件中使用,那么 `Modern.js` 是怎么去同步服务端预加载的数据到客户端呢?
服务端很容易把所有加载到的 Loader 执行一遍,并拿到返回的数据,难点在于如何在客户端运行的时候同步这份数据。
`Modern.js` 编译的时候会给每一个 Loader 生成一个唯一的 `id` ,用于数据同步的标记。如下是一个编译后的产物示例:
```ts
var loader = async () => {
return fetch(url);
};
loader.id = 'id1';
const { data, loading, error } = useLoader(loader);
```
那么服务端运行完成后会产生如下结构的数据:
```ts
{
'id1': {
data: 'hello',
error: null,
loading: false
}
}
```
客户端重新运行 Loader 的时候,会根据自身的 `id` 从服务端预先加载的数据中去查找,如果找不到对应数据,客户端会重新执行 Loader。
## useLoader 降级
在真实的业务场景中,服务端执行 `useLoader` 可能因为各种原因出现错误,此时就需要将失败了的 `useLoader` 降级到客户端重新执行。
当 `useLoader` 执行失败,返回的数据结构示例如下:
```ts
{
'id1': {
data: null,
error: 'I was failed',
loading: false
}
}
```
当客户端执行时发现 `error` 字段不为空,认为该 `useLoader` 在服务端失败,会降级到客户端重新执行。
## 结合 SSG 使用
使用 [`SSG`](/docs/guides/features/server-side/web/ssg) 时,页面的请求会分为两种数据:
- 静态的,即不会变更的,我们希望在 SSG 编译阶段就加载这部分数据并生成到 HTML 中。
- 动态的,会频繁变更的,需要在客户端运行时再次请求这部分数据。
对于第一部分需要在 SSG 编译阶段使用的数据,我们只需要给 `useLoader` 加一个 `static` 的标识:
```tsx
function Home() {
const { data: staticData } = useLoader(async () => {
return fetch(url1);
}, { static: true })
const { data: dynamicData } = useLoader(async () => {
return fetch(url2);
})
return ...
}
```
第一个 Loader 使用的静态数据 `staticData` 在编译阶段就会被加载加载,第二个 Loader 使用的动态数据 `dynamicData` 在客户端运行时才会被请求加载。
## Loader 缓存、重试
### 缓存
在实际业务场景中,一个同样功能的 `useLoader` 可能在多个不同的组件中使用。 Modern.js 支持 Loader 的缓存和去重能力,避免额外重复的 Loader 执行。
例如,我们有一个获取用户信息的 Loader:
```ts
function useUserInfoLoader(username) {
return useLoader(
async(context, _username) => {
return fetch(userUrl, { params: { _username } })
},
{
params: username
} // 第二个参数,起到 Loader ID 作用
);
}
```
注意我们将 `username` 传给了 `useLoader` 的第二个参数的 `params`,这里的 `params` 起到 Loader ID 的作用。Modern.js 内部会将 `params` 序列化成一个 ID,作为 Loader 的唯一标识。`params` 作为 ID 的优先级高于编译时生成的 Loader ID。
当我们在不同组件中使用 `useUserInfoLoader` 的时候:
```tsx
function Home() {
const { data } = useUserInfoLoader('bob');
return ...;
}
function About() {
const { data } = useUserInfoLoader('bob');
const { data: data1 } = useUserInfoLoader('tom');
return ...;
}
function App() {
return <div>
<Home />
<About />
</div>
}
```
`Home` 组件和 `About` 组件都执行了 `useUserInfoLoader('bob')`,根据上文我们知道 `'bob'` 最终会作为 Loader 的 ID,因为 ID 是相等的,所以 Modern.js 内部只会保留一个 Loader 实例,即只会请求一次 `'bob'` 的用户信息。
### 重试
Loader 过期之后,我们需要重新执行 Loader 以更新数据。
Modern.js 中提供了两种更新 Loader 的方式,**自动更新** 和 **手动更新**。
#### 自动更新
```tsx
function Home({ username }) {
const { data, reloading } = useUserInfoLoader(username);
return ...;
}
```
我们改造 `Home` 组件,接收了一个 `username` 字段作为 props。那么当 `username` 更新的时候,对应的 `UserInfoLoader` 是会重新执行的,也就是说重新请求新的 `username` 对应的用户信息。`useLoader` 会把 `params` 参数认为是自身的依赖,当 `params` 序列化后的值更新了的时候,对应 Loader 才会重新执行,更新 Loader 数据,否则沿用之前旧的数据。
我们可以根据 `reloading` 的值来判断当前的 Loader 是否处于正在更新的状态中。
:::info 注
- 当一个 Loader 更新的时候,该 Loader 所在的其它组件也会响应 Loader 更新,进行重新渲染。
:::
#### 手动更新
```tsx
function Home({ username }) {
const { data, reloading, reload } = useUserInfoLoader(username);
return <div>
<div>{JSON.stringify(data)}</div>
<button onClick={() => reload('kitten')}> relaod </button>
</div>
}
```
我们点击 `reload` 按钮,执行 `reload('kitten')` 函数,此时会触发 Loader 的重新执行,`'kitten'` 会作为新的参数传给 Loader 函数。
:::info 注
- 当需要用之前参数更新 Loader 时,可直接执行 `reload()`。
:::
## Dependent Loader
Loader 获取数据是异步的,当一个 Loader 的执行依赖另外一个 Loader 执行完成,我们可以使用 `skip` 参数实现。
```tsx
function Home() {
const { data, loading } = useLoader(async () => {
return new Promise(resolve => setTimeout(resolve, 1000))
});
const res = useLoader(async (context, data) => {
return fetch(url, { body: data });
}, { params: data, skip: loading || !data });
}
```
当第一个 Loader 没有返回的时候,`loading` 的值为 `true`,`data` 为空。我们通过 `loading || !data` 跳过第二个 Loader 的执行。
当第一个 Loader 成功执行完后,`loading || !data` 值转变为 `true`,第二个 Loader 便开始执行了。
:::info 补充信息
- 本章代码示例:[use-loader](https://github.com/modern-js-dev/modern-js-examples/tree/main/series/tutorials/runtime-api/use-loader)。
- 更多介绍,请参考【[useLoader API](/docs/apis/runtime/container/use-loader)】。
:::