解决 roadhog 单页应用提取公共模块的问题

前言
roadhog 在打包单页应用文件的时候如果采取异步加载模块文件的话,每个模块里面应用到的一些公用库(antd、moment等)会在每个生成的异步模块文件里面重复应用到,导致所有文件的总体积增加了很多(因为公用库被重复引用了)本文描述的是如何解决这个问题

问题描述

在 cpbs PC 端开发过程中,发现在经过 roadhog build --debug --analyze 命令打包之后的文件体积很大,具体内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Compiled successfully in 25.6s.

File sizes after gzip:

1.56 MB dist\index.js
1.27 MB dist\role.async.js
1.26 MB dist\privilegeAudit.async.js
1.23 MB dist\merchant.async.js
1.23 MB dist\agent.async.js
1.23 MB dist\merchantAudit.async.js
1.23 MB dist\agentAudit.async.js
1.23 MB dist\merchantReview.async.js
1.23 MB dist\agentReview.async.js
1.22 MB dist\user.async.js
1.18 MB dist\privilege.async.js
1.15 MB dist\transactionTotal.async.js
1.14 MB dist\statisticRemit.async.js
1.14 MB dist\statisticContrast.async.js
1.14 MB dist\transactionFlow.async.js
239.48 KB dist\login.async.js
17.2 KB dist\index.css
3.93 KB dist\error.async.js
1.7 KB dist\antd.js

Analyze result is generated at dist/stats.html.

roadhog 将每个 model 都打包为一个单独的异步调用的 js 文件,这些文件即使在 gzip 压缩之后每个都需要平均 1M 的体积,在查看打包的具体内容 (stats.html)之后,发现体积过大的这些包都对一些共同使用到的组件进行了打包,而非引用模块部分的代码体积只有 300k 左右:

模块打包文件示意图

经过分析,发现体积过大的原因有以下几点:

  • moment 模块被重复引用
  • moment 模块引用了多余的本地化文件(约300k)
  • antd 重复引用到的模块,如 TableButtonForm 等被各个模块重复打包,增加了体积

解决思路

  • 将体积大的公用的模块进行打包(momentantd部分使用频率高的模块)
  • 移除 moment 模块没有使用到的语言包

本文只讲述如何将公共模块抽离出来作为一个单独文件


解决过程

使用 CommonsChunkPlugin 将公用模块抽离到 ventor.js

修改 roadhog 的配置文件:

1
2
3
4
// .roadhogrc.js
...
"entry": "./src/index.js",
...

在根目录添加 webpack.config.js,输入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const webpack = require('webpack')

module.exports = function wp(webpackConfig, env) {
// 对roadhog默认配置进行操作,比如:
if (env === 'production') {
// 上线环境使用分包打包方式
webpackConfig.entry = {
index: './src/index.js',
vendor: [
'moment',
'mockjs',
'lodash',
'react',
'react-dom',
'react-helmet',
],
}
// webpackConfig.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
webpackConfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({
name: ['vendor'],
minChunks: Infinity,
}))
}

return webpackConfig
}

至此,完成了将除 antd 之外的公用模块的抽离

使用 CommonsChunkPluginantd 的高频模块抽离到 antd.js

修改 webpack.config.js为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const webpack = require('webpack')

module.exports = function wp(webpackConfig, env) {
// 对roadhog默认配置进行操作,比如:
if (env === 'production') {
// 上线环境使用分包打包方式
webpackConfig.entry = {
index: './src/index.js',
vendor: [
'moment',
'mockjs',
'lodash',
'react',
'react-dom',
'react-helmet',
],
antd: [
'antd/lib/button',
'antd/lib/icon',
'antd/lib/table',
'antd/lib/date-picker',
'antd/lib/form',
'antd/lib/modal',
'antd/lib/grid',
'antd/lib/input',
],
}
// webpackConfig.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
webpackConfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'antd'],
minChunks: Infinity,
}))
}

return webpackConfig
}

修改 index.htmlindex.ejs

/src 目录下的文件进行如下调整:

1
2
3
4
|-index.ejs
|-index.js
|-index.less
|-router.js

各个文件的配置为:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- index.ejs -->
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<div id="root"></div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import 'babel-polyfill'
import dva from 'dva'
import createHistory from 'history/createBrowserHistory'
import createLoading from 'dva-loading'
import { message } from 'antd'
import moment from 'moment'
import 'moment/locale/zh-cn'
import './index.less'
import './styles/lib/animate.css' // 引入全局样式动画库
import './themes/index.less'

moment.locale('zh-cn')
const ERROR_MSG_DURATION = 3 // 3 秒

// 1. Initialize
const app = dva({
history: createHistory(),
onError(error) {
console.error(error)
message.error(error.message, ERROR_MSG_DURATION)
},
})

// 2. Plugins
app.use(createLoading({ effects: true }))

// 3. Model

// 4. Router
app.router(require('./router'))

// 5. Start
app.start('#root')

index.lessrouter.js 的内容这里不赘述

经过以上配置之后,index.ejs 能够根据 roadhog 里面的 entry 配置自动引入所有入口文件。

注意 CommonsChunkPlugin 配置里面的 name 的数组顺序会影响到 entry 的引用顺序,如果出现 webpack 未定义的错误,尝试改变这个属性值的顺序,因为 CommonsChunkPlugin 是按照抽离顺序将 webpack 的全局对象放到了 entry 的最后一个入口文件里面。


解决成果

经过上面的处理之后,打包的个文件体积为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Compiled successfully in 21.1s.

File sizes after gzip:

1.41 MB dist\antd.js
596.22 KB dist\index.js
556.53 KB dist\role.async.js
549.17 KB dist\privilegeAudit.async.js
526.47 KB dist\merchant.async.js
524.27 KB dist\agent.async.js
520.04 KB dist\merchantAudit.async.js
519.58 KB dist\agentAudit.async.js
516.8 KB dist\merchantReview.async.js
516.34 KB dist\agentReview.async.js
508.46 KB dist\user.async.js
474.88 KB dist\privilege.async.js
443.19 KB dist\transactionTotal.async.js
434.33 KB dist\statisticRemit.async.js
433.45 KB dist\statisticContrast.async.js
429.59 KB dist\transactionFlow.async.js
279.46 KB dist\vendor.js
112.09 KB dist\login.async.js
17.2 KB dist\index.css
3.92 KB dist\error.async.js

Analyze result is generated at dist/stats.html.

可以看到被抽离了公用模块之后的各模块的体积基本减少到了 500k(具体抽离哪些模块还可以再精准研究),代码包的整体面积无疑是减少了很多的。


后续报道

发现在使用多入口方式的时候无法触发热更新,所以在开发环境之下还是使用旧有的打包方式,只在线上环境使用抽离式打包,解决方式就是在 webpack.config.js 里面添加一个环境变量判断,详细见上面代码。

1
2
3
4
5
6
7
8
9
10
const webpack = require('webpack')

module.exports = function wp(webpackConfig, env) {
// 对roadhog默认配置进行操作,比如:
if (env === 'production') {
// ...do what you want
}

return webpackConfig
}

资料来源

roadhog - 支持 vendor 的配置 #370