软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

前端SSR复用CSR代码拆分

前端客户端渲染(CSR)时,一般都进行代码拆分,按需懒加载页面相关的JS/CSS与数据,交互体验比较好且容易开发。但是只进行CSR时,首屏显示比较缓慢。服务端渲染(SSR)时,一次请求能拿到页面完整的HTML内容,我们用来解决首屏显示缓慢的问题。同时使用CSR与SSR,就面临如何复用所有的代码,包含CSR代码拆分。

在React/Preact中,SSR复用CSR代码拆分有两种实现:@loadable/componentpreact-cli 。@loadable/component中使用NodeJS的require()动态加载JS文件,而preact-cli则直接静态打包到一个JS文件中。

1. CSR与SSR动态加载的差异

CSR代码拆分时,Webpack使用<script>标签动态加载JS,JS都是异步执行,Promise响应script.onload事件。

function requireEnsure(chunkId) {
    var promises = [];
    ...
    var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    promises.push(installedChunkData[2] = promise);

    // start chunk loading
    var script = document.createElement('script');
    var onScriptComplete;
    ...

    script.onerror = script.onload = onScriptComplete;
    document.head.appendChild(script);

    return Promise.all(promises);
}

SSR中,Webpack直接使用NodeJS的require()同步加载对应的JS,再执行空的Promise.all([])获取到异步对象。

function requireEnsure(chunkId) {
    var promises = [];

    var chunk = require("./" + ({"0":"containers-BooksContainer"}[chunkId]||chunkId) + ".js");

    return Promise.all(promises);
}

2. @loadable/component代码拆分

需要在Webpack脚本中使用@loadable/babel-plugin与@loadable/webpack-plugin的配合,然后在代码中如下使用:

const BooksContainer = loadable(() => import('./containers/BooksContainer'), {
  fallback: <div>Loading...</div>,
});

CSR与SSR中均使用@loadable/component的代码,只是在SSR时判断动态组件已经同步加载、立即渲染。

启动SSR渲染之前,需要加载项目的脚本:

const ssrExtractor = new ChunkExtractor({
    statsFile: ssrStats,
    entrypoints: ['server'],
    outputPath: ssrOutputPath,
  });
  ssrExtractor.requireEntrypoint();

  const routeContent = { url: '' };
  const jsx = ssrExtractor.collectChunks(<App url={url} preloadedState={backendData} routeContent={routeContent} />);
  const appHtml = renderToString(jsx) || '';

3. Preact-cli代码拆分

使用Preact-cli时,只需按照其规则放文件即可,不需要使用动态的import()导入js。CSR时,其自动转换为@preact/async-loader

符合以下规则的文件,CSR将使用异步加载:

src/routes/{*,*/index}.{js,jsx,ts,tsx}
src/components/routes/{*,*/index}.{js,jsx,ts,tsx}
src/components/async/{*,*/index}.{js,jsx,ts,tsx}

用法:

import Home from './routes/home';
import BooksPage from './routes/books';

const App = ({ routeContent }) => (
  <div id="app" class={styles.app}>
    <Router>
      <Route path="/" component={Home} />
      <Route path="/books" component={BooksPage as FunctionalComponent} />
      <Redirect default to="/" routeContent={routeContent} />
    </Router>
  </div>
);