TL;DR: Use React Loadable.

在 server-side rendering (SSR) 的 React 项目中实现对 dynamic import 的支持是一件比较困难的事情,多数项目只实现了比较简陋的 dynamic import 支持,比如只支持路由级别的 dynamic import,或者没有支持 client 端 async chunks 与其他资源的并行加载。

不像 client-only 的 React 项目,在 SSR 项目中支持 dynamic import 需要引入额外的 webpack 配置,这也是为什么目前这方面的库比较少的原因。React Loadable 可能是目前这方面唯一有全功能支持的库。

这篇文章会参照 React Loadable 的设计,来讲解一下具体的实现细节。

使用方式

实现函数 dynamic(...),返回一个 React 组件,约定调用方式如下:

const AsyncComponent = dynamic(() => import("./AsyncComponent"));

Server 端渲染

众所周知,import(...) 返回的是一个 Promise,而 Promise 并不能同步地拿到里面的值,即使这个 Promise 已经是 fulfilled 状态。

在这种情况下,ReactDOMServer.renderToString(...) 可能并不能同步地渲染出 dynamic import 的组件。

常见的解决方案有两种:

  1. 在 server 启动之前 preload 所有 dynamic import 并保存到缓存里;
  2. 通过 webpack 内部变量查找 dynamic import 对应的值。

前者不仅可以在 webpack 项目中使用,还能在后端只经过 Babel 编译而不经过 webpack 打包的情况下使用。

而后者依赖 webpack 的内部变量去查找对应模块,所以只能在 webpack 打包的项目内使用,实现代码大致如下:

const load = id => {
  if (__webpack_modules__[id]) {
    return __webpack_require__(id);
  }
};

const AsyncComponent = load(require.resolveWeak("./AsyncComponent"));

因为这种方式依赖 require.resolveWeak(...) 返回的模块 id,dynamic 函数的调用方式需要对应更改为:

const AsyncComponent = dynamic(() => import("./AsyncComponent"), {
  id: require.resolveWeak("./AsyncComponent"),
});

可以使用 Babel 插件自动为每次 dynamic 调用生成对应的 id 参数。

Client 端渲染

为了确保 client 端能够正确地处理 server 端渲染出的 html,需要 client 端在调用 ReactDOM.hydrate(...) 之前,加载完所有需要用到的 async chunks。

为了提升加载速度,需要确保 async chunks 能够同其他资源一起并行加载。这需要 server 端能够分析当前渲染使用到了哪些 dynamic imports,查找到对应的 async chunks,并随着 html 一起输出。

以 home 页面为例,预期的输出可能是:

...
<script src="/public/vendor.[hash].js"></script>
<script src="/public/main.[hash].js"></script>
<script src="/public/page-home.[hash].js"></script>
<script src="/public/component-timeline.[hash].js"></script>
...

为此,需要额外实现一个 <Capture> 组件来收集使用到的模块,并在 dynamic(...) 返回的组件的 componentWillMount 函数内,通过 context 来调用 onCapture 函数。

const modules = [];
const html = ReactDOMServer.renderToString(
  <Capture onCapture={module => modules.push(module)}>
    <App />
  </Capture>,
);

同时需要配置 client 端与 server 端的 webpack,输出 stats 数据到 json 文件。通过 server 端的 stats 文件, 查找到模块 id 对应的模块文件路径; 再通过 client 端的 stats 文件,查找到最终构建出的 chunk 的文件路径。

const getChunks = (stats, modules) => {
  // TODO
};

const stats = {
  client: require("../build/client/stats.json"),
  server: require("../build/server/stats.json"),
};

const chunks = getChunks(stats, modules);

// ...
chunks.map(chunk => <script src={chunk.path} />);
// ...

在客户端,则通过 __webpack_modules____webpack_require__ 同步拿到对应的模块内容用于 React 渲染。