前言
在前端工程的的打包史中,common文件向来都不是一个好处理的方面。在这一块,webpack提供了CommonsChunkPlugin来处理这个事情,但是在由于文档的模棱两可,再加上各种配置选项的多样性和某些bug,还是有不少坑的。
分析包
所谓工欲善其事必先利其器,我们既然想做common方面的优化,那么首先肯定要知道打包后的文件体积庞大的主要原因。说到这里就不得不提到一个相当好用的工具:webpack-bundle-analyzer
它既是一个webpack插件,又是一个命令行工具。能够将webpack包的内容转换成可缩放的树状图,方便进行交互分析。恩。。。就是这玩意:
安装
npm install --save-dev webpack-bundle-analyzer
作为插件使用
在 webpack.config.js
中:
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// ...
plugins: [new BundleAnalyzerPlugin()]
// ...
默认配置如下:
new BundleAnalyzerPlugin({
// Can be `server`, `static` or `disabled`.
// In `server` mode analyzer will start HTTP server to show bundle report.
// In `static` mode single HTML file with bundle report will be generated.
// In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`.
analyzerMode: 'server',
// Host that will be used in `server` mode to start HTTP server.
analyzerHost: '127.0.0.1',
// Port that will be used in `server` mode to start HTTP server.
analyzerPort: 8888,
// Path to bundle report file that will be generated in `static` mode.
// Relative to bundles output directory.
reportFilename: 'report.html',
// Automatically open report in default browser
openAnalyzer: true,
// If `true`, Webpack Stats JSON file will be generated in bundles output directory
generateStatsFile: false,
// Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`.
// Relative to bundles output directory.
statsFilename: 'stats.json',
// Options for `stats.toJson()` method.
// For example you can exclude sources of your modules from stats file with `source: false` option.
// See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
statsOptions: null,
// Log level. Can be 'info', 'warn', 'error' or 'silent'.
logLevel: 'info'
})
命令行使用
第一步:
webpack --profile --json > stats.json
第二步:
webpack --profile --json | Out-file 'stats.json' -Encoding OEM
执行成功后,你将看到以下动态网页:
在这里顺便放上线上文件加载waterfall,作为对比。
问题
通过图表可以看到,在以下配置下:
config.plugins.push(new CommonsChunkPlugin('commons', 'js/commons.[hash].bundle.js'));
打包出来的文件还是有很多问题的:
common包规划不合理, swiper.js ,area.json等公用文件大量重复加载。
antd 没有抽离出来,无法并行加载,也无法进一步做运行时按需加载。
echarts在每个使用的包都单独打包一份,只要包含echarts的包,基本一百多kb。(线上压缩并开启gzip)
import {ImgBigSwiper} from 'components/src/index';
这种写法会导致将components里面的所有组件全部打包进页面的js。应该这样写:import ImgBigSwiper from 'components/src/ImgBigSwiper';
挨个引入,见webpack将ES6编译成CommonJs后只引入用到的模块common.js独占490kb,要等这个包加载完后index才开始解析路由。
在这个过程中,会发现一个有趣的事情。就是index.html页面的script加载分为以下两个部分:
......
<script type="text/javascript" src="//res.dinghuo123.com/src/common/ueditor/ueditor.config.js"></script>
<script type="text/javascript" src="//res.dinghuo123.com/src/common/ueditor/ueditor.config.js"></script>
<script type="text/javascript" src="//res.dinghuo123.com/src/common/ueditor/ueditor.config.js"></script>
......
<script>
document.write('<script src="https://resource.dinghuo123.com/dist/ydhv2/webpack.assets.js?v=' + Math.random() + '"><\/script>');
document.write('<script src="' + window.WEBPACK_ASSETS['commons'].js + '"><\/script>');
document.write('<script src="//res.dinghuo123.com/src/common/ueditor/ueditor.config.js"><\/script>');
document.write('<script src="' + window.WEBPACK_ASSETS['index'].js + '"><\/script>');
</script>
然后你会发现,是上面一块的script并行加载完,才并行加载下一个script标签的内容。大家可以思考一下为什么。
改进配置
进过调整之后的CommonsChunkPlugin配置:
config.plugins.push(new CommonsChunkPlugin({
name: 'commons',
minChunks: Infinity // 随着 入口chunk 越来越多,这个配置保证没其它的模块会打包进 公共chunk
}));
config.plugins.push(new CommonsChunkPlugin({
async:'antd',
minChunks(module) {
var context = module.context;
return context && context.indexOf('antd/dist') >= 0;
}
}));
config.plugins.push(new CommonsChunkPlugin({
async:'echarts',
minChunks(module) {
var context = module.context;
return context && (context.indexOf('echarts') >= 0 || context.indexOf('zrender') >= 0);
}
}));
这里用到了minChunks和async两个配置。
minChunks
其中第一name的commons
是一个entry
入口,里面是一个依赖包的数组。minChunks
设置为Infinity
这个配置保证没其它的模块会打包进 公共chunk。因为说实话,CommonsChunkPlugin的commons分析实在是不怎么只能,还是手动控制会更好一些。
当然,你可以传入一个 function
,以添加定制的逻辑(默认是 chunk 的数量),这个函数会被 CommonsChunkPlugin 插件回调,并且调用函数时会传入 module 和 count 参数。
module 参数代表每个 chunks 里的模块,这些 chunks 是你通过 name/names 参数传入的。
module.context: The directory that stores the file. For example: '/my_project/node_modules/example-dependency'
module.resource: The name of the file being processed. For example: '/my_project/node_modules/example-dependency/index.js'
count 参数表示 module 被使用的 chunk 数量。
当你想要对 CommonsChunk 如何决定模块被打包到哪里的算法有更为细致的控制, 这个配置就会非常有用。
async
下面的内容是官网弄过来的,其实我也看不太懂。。。
如果设置为
true
,一个异步的 公共chunk 会作为options.name
的子模块,和options.chunks
的兄弟模块被创建。它会与options.chunks
并行被加载。可以通过提供想要的字符串,而不是true
来对输出的文件进行更换名称。
结果
还是先看打包分析的结果吧:
通过上面分析可以看到:
common合理划分,抓大放小。
antd和echarts提取出来,并行加载。
components 用到什么打包什么。(手动控制的)
大大减小了其他业务包的体积,93%的业务包大小控制在25K以内,剩下7%的业务包大小控制在50k以内。(开启gzip)
首屏加载资源的总大小几乎没有变化。
接下来的方向
echarts 和Ueditor运行时按需加载。
tree-shaking
的探索
参考
Vendor and code splitting in webpack 2
webpack 按需打包加载
weboack Split app and vendor code
awesome-webpack-cn