Loader 管理
什么是 Loader
开发 SSR 应用,服务端预加载数据是一个必不可少的流程,它通常包含以下 3 个阶段:
- 服务端预先请求应用渲染所需要的数据,使用获取到的数据在服务端生成完整 HTML。
- 服务端返回 HTML 的同时,把请求到的数据同步到客户端(一般是通过将数据序列化,插入到返回给客户端的 HTML 的 Script 脚本中)。
- 客户端复用服务端预先请求到的数据,执行后续逻辑。
Modern.js 提出了 Loader 的概念,将上述行为进行抽象。通过 useLoader
这样一个同构的 API,处理 SSR 应用的数据请求,Modern.js 会自动处理上述三个流程。
简单的 useLoader 使用
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
,用于数据同步的标记。如下是一个编译后的产物示例:
var loader = async () => {
return fetch(url);
};
loader.id = 'id1';
const { data, loading, error } = useLoader(loader);
那么服务端运行完成后会产生如下结构的数据:
{
'id1': {
data: 'hello',
error: null,
loading: false
}
}
客户端重新运行 Loader 的时候,会根据自身的 id
从服务端预先加载的数据中去查找,如果找不到对应数据,客户端会重新执行 Loader。
useLoader 降级
在真实的业务场景中,服务端执行 useLoader
可能因为各种原因出现错误,此时就需要将失败了的 useLoader
降级到客户端重新执行。
当 useLoader
执行失败,返回的数据结构示例如下:
{
'id1': {
data: null,
error: 'I was failed',
loading: false
}
}
当客户端执行时发现 error
字段不为空,认为该 useLoader
在服务端失败,会降级到客户端重新执行。
结合 SSG 使用
使用 SSG
时,页面的请求会分为两种数据:
- 静态的,即不会变更的,我们希望在 SSG 编译阶段就加载这部分数据并生成到 HTML 中。
- 动态的,会频繁变更的,需要在客户端运行时再次请求这部分数据。
对于第一部分需要在 SSG 编译阶段使用的数据,我们只需要给 useLoader
加一个 static
的标识:
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:
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
的时候:
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 的方式,自动更新 和 手动更新。
自动更新
function Home({ username }) {
const { data, reloading } = useUserInfoLoader(username);
return ...;
}
我们改造 Home
组件,接收了一个 username
字段作为 props。那么当 username
更新的时候,对应的 UserInfoLoader
是会重新执行的,也就是说重新请求新的 username
对应的用户信息。useLoader
会把 params
参数认为是自身的依赖,当 params
序列化后的值更新了的时候,对应 Loader 才会重新执行,更新 Loader 数据,否则沿用之前旧的数据。
我们可以根据 reloading
的值来判断当前的 Loader 是否处于正在更新的状态中。
注
- 当一个 Loader 更新的时候,该 Loader 所在的其它组件也会响应 Loader 更新,进行重新渲染。
手动更新
function Home({ username }) {
const { data, reloading, reload } = useUserInfoLoader(username);
return <div>
<div>{JSON.stringify(data)}</div>
<button onClick={() => reload('kitten')}> reload </button>
</div>
}
我们点击 reload
按钮,执行 reload('kitten')
函数,此时会触发 Loader 的重新执行,'kitten'
会作为新的参数传给 Loader 函数。
注
- 当需要用之前参数更新 Loader 时,可直接执行
reload()
。
Dependent Loader
Loader 获取数据是异步的,当一个 Loader 的执行依赖另外一个 Loader 执行完成,我们可以使用 skip
参数实现。
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 便开始执行了。
补充信息
- 本章代码示例:use-loader。
- 更多介绍,请参考【useLoader API】。