跳转到主文档

Loader 管理

什么是 Loader

开发 SSR 应用,服务端预加载数据是一个必不可少的流程,它通常包含以下 3 个阶段:

  1. 服务端预先请求应用渲染所需要的数据,使用获取到的数据在服务端生成完整 HTML。
  2. 服务端返回 HTML 的同时,把请求到的数据同步到客户端(一般是通过将数据序列化,插入到返回给客户端的 HTML 的 Script 脚本中)。
  3. 客户端复用服务端预先请求到的数据,执行后续逻辑。

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 的值为 truedata 为空。我们通过 loading || !data 跳过第二个 Loader 的执行。 当第一个 Loader 成功执行完后,loading || !data 值转变为 true,第二个 Loader 便开始执行了。

补充信息