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