React SPA 项目 Code Splitting 实战
Posted on: at
Code Splitting 是指按照你预设的方式去进行代码分离,如今前端项目越来越依赖于打包工具进行预处理,打包工具推出的代码分离特性应对以下情景:
- 单页应用(SPA)如何按照路由结构分离业务代码
- 把常用库进行代码分离(如moment、jquery等)
- 懒加载等实现的前提
webpack 官方 CodeSplitting 教程 中建议以以下三个角度去进行代码分离:
- 定义多入口:手动通过定义 entry 属性来分离模块
- 避免重复引用:使用 SplitChunksPlugin 来避免模块重复用引用
- 异步加载:将某些模块的代码分离为单独文件并通过函数按需加载
具体的代码分离看项目架构而异,下面记录一次简单的 React SPA 项目的 Code Splitting 改造
思路
按照路由拆分项目,把产物结构改造成如下结构:
# 改造前
DLL.js # 环境dll
index.html # 入口html
main.[contentHash].js # 业务代码入口
# 改造后
chunks/
route_index.[contentHash].js # / 路由的业务代码
route_about.[contentHash].js # /about 路由的业务代码
DLL.js # 环境dll
index.html # 入口html
main.[contentHash].js # 业务代码入口(已剔除 /, /about 路由的业务代码)
施工
将模块抽离为 Chunk
webpack 支持使用关键字 import() 来异步打包模块(被引入的这个模块视为分裂点 chunk,会被另外打包为 chunkFile),没有被命名的 chunk 会被名为自动生成的 chunkId,可以通过 magic comments 来手动定义 chunkFileName,
相关知识点文档:
webpack-import()
magic comments
// 定义了一个名为 route_index 的 chunk
const Index = import(
/* webpackChunkName: "route_index" */
'./routes/index'
)
根据你的 webpack 配置里面的 output.chunkFilename 上面抽离的 chunk 会被按照定义输出,如:
// webpack.dev.conf.js
module.exports = {
output: 'dist',
publicPath: '/',
filename: '[name].[contenthash].js',
chunkFilename: 'chunks/[name].[chunkhash].js',
}
// chunk "route_index" 会生成为以下文件:
// chunks/router_index.d1f38639a5badd76c7c8.js
使用异步加载React模块
上述代码是把某个路由的模块文件 chunk 化,但是 import() 生成的是一个 Promise 加载实例,无法直接用来作为路由组件,你要进行以下逻辑的处理:
- 在 chunk 组件外面包裹一个组件,处理:
- 在加载实例未 resolve 前显示占位符组件,组件加载完之后再显示实际的组件
- 把所有参数向下传递给实际组件
通过上面处理才能把 chunk 和 React 组件结合起来,不过我们有现成的库 @loadable/component_ _,它能提供一个包裹组件完成上述功能,具体代码如下:
// ./src/components/Loading/index.js
const Loading = () => {
return (
<div>loading...</div>
)
}
export default Loading
// ./src/common/utils.js
import loadable from '@loadable/component'
import Loading from "../components/Loading"
export const dynamicWrap = func => loadable(func, {
fallback: <Loading />,
})
// ./src/router.js
import {
BrowserRouter as Router,
Route,
Link,
} from 'react-router-dom'
import {
dynamicWrap,
} from './common/utils.js'
const Index = dynamicWrap(() => import(
/* webpackChunkName: "router_index" */
'./routes/index'
))
const About = dynamicWrap(() => import(
/* webpackChunkName: "router_about" */
'./routes/about'
))
export default () => {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about/">About</Link>
</li>
</ul>
</nav>
<Route
path="/" exact
component={Index}
/>
<Route
path="/about/"
component={About}
/>
</div>
</Router>
);
}
至此就完成了按照路由结构实现 CodeSplitting 的改造