React SPA 项目 Code Splitting 实战

Posted on:  at 

Code Splitting 是指按照你预设的方式去进行代码分离,如今前端项目越来越依赖于打包工具进行预处理,打包工具推出的代码分离特性应对以下情景:

  1. 单页应用(SPA)如何按照路由结构分离业务代码
  2. 把常用库进行代码分离(如moment、jquery等)
  3. 懒加载等实现的前提

webpack 官方 CodeSplitting 教程 中建议以以下三个角度去进行代码分离:

  1. 定义多入口:手动通过定义 entry 属性来分离模块
  2. 避免重复引用:使用  SplitChunksPlugin 来避免模块重复用引用
  3. 异步加载:将某些模块的代码分离为单独文件并通过函数按需加载

具体的代码分离看项目架构而异,下面记录一次简单的 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 路由的业务代码)

施工

相关依赖:
@loadable/component
webpack-DynamicImports

将模块抽离为 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 加载实例,无法直接用来作为路由组件,你要进行以下逻辑的处理:

  1. 在 chunk 组件外面包裹一个组件,处理:
  2. 在加载实例未 resolve 前显示占位符组件,组件加载完之后再显示实际的组件
  3. 把所有参数向下传递给实际组件

通过上面处理才能把 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 的改造