目录javascript
webpack
做为前端最火的构建工具,是前端自动化工具链最重要的部分,使用门槛较高。本系列是笔者本身的学习记录,比较基础,但愿经过问题 + 解决方式的模式,之前端构建中遇到的具体需求为出发点,学习webpack
工具中相应的处理办法。(本篇中的参数配置及使用方式均基于webpack4.0版本
)html本篇摘要:前端
本篇主要介绍基于
webpack4.0
的splitChunks
分包技术。vue
javascript
之因此须要打包合并,是由于模块化开发的存在。开发阶段咱们须要将js
文件分开写在不少零碎的文件中,方便调试和修改,但若是就这样上线,那首页的http
请求数量将直接爆炸。同一个项目,别人2-3个请求就拿到了须要的文件,而你的可能须要20-30个,结果就不用多说了。java
可是合并脚本可不是“把全部的碎片文件都拷贝到一个js
文件里”这样就能解决的,不只要解决命名空间冲突的问题,还须要兼容不一样的模块化方案,更别提根据模块之间复杂的依赖关系来手动肯定模块的加载顺序了,因此利用自动化工具来将开发阶段的js
脚本碎片进行合并和优化是很是有必要的。node
TS
或ES6
代码的编译)babel
是ES6
语法的转换工具,对babel
不了解的读者能够先阅读《大前端的自动化工厂(3)——Babel》一文进行了解,babel
与webpack
结合使用的方法也在其中作了介绍,此处仅提供基本配置:webpack
webpack.config.js
:ios
... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader' } ] } ] }, ...
.babelrc
:es6
{ "presets":[ ["env",{ "targets":{ "browsers":"last 2 versions" } } ]], "plugins": [ "babel-plugin-transform-runtime" ] }
使用webpack
对脚本进行合并是很是方便的,毕竟模块管理和文件合并这两个功能是webpack
最初设计的主要用途,直到涉及到分包和懒加载的话题时才会变得复杂。webpack
使用起来很方便,是由于实现了对各类不一样模块规范的兼容处理,对前端开发者来讲,理解这种兼容性实现的方式比学习如何配置webpack
更为重要。webpack
默认支持的是CommonJs
规范,但同时为了扩展其使用场景,webpack
在后续的版本迭代中也加入了对ES harmony
等其余规范定义模块的兼容处理,具体的处理方式将在下一章《webpack4.0各个击破(5)—— Module篇》详细分析。web
webpack
的输出的文件中能够看到以下的部分:
/******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ }
上面的__webpack_require__( )
方法就是webpack
的模块加载器,很容易看出其中对于已加载的模块是有统一的installedModules
对象来管理的,这样就避免了模块重复加载的问题。而公共模块通常也须要从bundle.js
文件中提取出来,这涉及到下一节的“代码分割”的内容。
1. 为何要进行代码分割?
代码分割最基本的任务是分离出第三方依赖库,由于第三方库的内容可能好久都不会变更,因此用来标记变化的摘要哈希contentHash
也好久不变,这也就意味着咱们能够利用本地缓存来避免没有必要的重复打包,并利用浏览器缓存避免冗余的客户端加载。另外当项目发布新版本时,若是第三方依赖的contentHash
没有变化,就可使用客户端原来的缓存文件(通用的作法通常是给静态资源请求设置一个很大的max-age
),提高访问速度。另一些场景中,代码分割也能够提供对脚本在整个加载周期内的加载时机的控制能力。
2. 代码分割的使用场景
举个很常见的例子,好比你在作一个数据可视化类型的网站,引用到了百度的Echarts
做为第三方库来渲染图表,若是你将本身的代码和Echarts
打包在一块儿生成一个main.bundle.js
文件,这样的结果就是在一个网速欠佳的环境下打开你的网站时,用户可能须要面对很长时间的白屏,你很快就会想到将Echarts
从主文件中剥离出来,让体积较小的主文件先在界面上渲染出一些动画或是提示信息,而后再去加载Echarts
,而分离出的Echarts
也能够从速度更快的CDN
节点获取,若是加载某个体积庞大的库,你也能够选择使用懒加载的方案,将脚本的下载时机延迟到用户真正使用对应的功能以前。这就是一种人工的代码分割。
从上面的例子整个的生命周期来看,咱们将本来一次就能够加载完的脚本拆分为了两次,这无疑会加剧服务端的性能开销,毕竟创建TCP链接是一种开销很大的操做,但这样作却能够换来对渲染节奏的控制和用户体验的提高,异步模块和懒加载模块从宏观上来说实际上都属于代码分割的范畴。code splitting
最极端的情况其实就是拆分红打包前的原貌,也就是源码直接上线。
3. 代码分割的本质
代码分割的本质,就是在“源码直接上线”和“打包为惟一的脚本main.bundle.js”这两种极端方案之间寻找一种更符合实际场景的中间状态,用可接受的服务器性能压力增长来换取更好的用户体验。
4. 配置代码分割
code-splitting
技术的配置和使用方法将在下一小节详细描述。
5. 更细致的代码分割
感兴趣的读者能够参考来自google开发者社区的文章《Reduce JavaScript Payloads with Code Splitting》自行研究。
webpack4
中已经内置了UglifyJs
插件,当打包模式参数mode
设置为production
时就会自动开启,固然这不是惟一的选择,babel
的插件中也能提供代码压缩的处理,具体的效果和原理笔者还没有深究,感兴趣的读者能够自行研究。
webpack4
废弃了CommonsChunkPlugin
插件,使用optimization.splitChunks
和optimization.runtimeChunk
来代替,缘由能够参考《webpack4:连奏中的进化》一文。关于runtimeChunk
参数,有的文章说是提取出入口chunk中的runtime部分,造成一个单独的文件,因为这部分不常变化,能够利用缓存。google开发者社区的博文是这样描述的:
The
runtimeChunk
option is also specified to move webpack's runtime into thevendors
chunk to avoid duplication of it in our app code.
splitChunks
中默认的代码自动分割要求是下面这样的:
node_modules中的模块或其余被重复引用的模块
就是说若是引用的模块来自node_modules
,那么只要它被引用,那么知足其余条件时就能够进行自动分割。不然该模块须要被重复引用才继续判断其余条件。(对应的就是下文配置选项中的minChunks
为1或2的场景)
分离前模块最小体积下限(默认30k,可修改)
30k是官方给出的默认数值,它是能够修改的,上一节中已经讲过,每一次分包对应的都是服务端的性能开销的增长,因此必需要考虑分包的性价比。
对于异步模块,生成的公共模块文件不能超出5个(可修改)
触发了懒加载模块的下载时,并发请求不能超过5个,对于稍微了解过服务端技术的开发者来讲,【高并发】和【压力测试】这样的关键词应该不会陌生。
对于入口模块,抽离出的公共模块文件不能超出3个(可修改)
也就是说一个入口文件的最大并行请求默认不得超过3个,缘由同上。
splitChunks
的在webpack
4.0以上版本中的用法是下面这样的:
module.exports = { //... optimization: { splitChunks: { chunks: 'async',//默认只做用于异步模块,为`all`时对全部模块生效,`initial`对同步模块有效 minSize: 30000,//合并前模块文件的体积 minChunks: 1,//最少被引用次数 maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: '~',//自动命名链接符 cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, minChunks:1,//敲黑板 priority: -10//优先级更高 }, default: { test: /[\\/]src[\\/]js[\\/]/ minChunks: 2,//通常为非第三方公共模块 priority: -20, reuseExistingChunk: true } }, runtimeChunk:{ name:'manifest' } } }
注:实例中使用的demo及配置文件已放在附件中。
单页面应用
单页面应用只有一个入口文件,splitChunks
的主要做用是将引用的第三方库拆分出来。从下面的分包结果就能够看出,node_modules
中的第三方引用被分离了出来,放在了vendors-main.[hash].js
中。
多页面应用
多页面应用的情形稍显复杂,以《webpack4:连奏中的进化》一文中的例子进行代码分割处理,源码的依赖关系为:
entryA.js: vue vuex component10k entryB.js: vue axios component10k entryC.js: vue vuex axios component10k
通过代码分割后获得的包以下图所示:
splitChunks
提供了更精确的分割策略,可是彷佛没法直接经过html-webpack-plugin
配置参数来动态解决分割后代码的注入问题,由于分包名称是不肯定的。这个场景在使用chunks:'async'
默认配置时是不存在的,由于异步模块的引用代码是不须要以<script>
标签的形式注入html
文件的。
当chunks
配置项设置为all
或initial
时,就会有问题,例如上面示例中,经过在html-webpack-plugin
中配置excludeChunks
能够去除page和about这两个chunk,可是却没法提早排除vendors-about-page这个chunk,由于打包前没法知道是否会生成这样一个chunk。这个场景笔者并无找到现成的解决方案,对此场景有需求的读者也许能够经过使用html-webpack-plugin
的事件扩展来处理此类场景,也可使用折中方案,就是第一次打包后记录下新生成的chunk名称,按需填写至html-webpack-plugin
的chunks
配置项里。
### 4.4 结果分析
经过Bundle Buddy
分析工具或webpack-bundle-analyser
插件就能够看到分包先后对于公共代码的抽取带来的影响(图片来自参考文献的博文):
【1】附加中文件说明:
webpack.spa.config.js
——单页面应用代码分割配置实例main.js
——单页面应用入口文件webpack.multi.config.js
——多页面应用代码分割配置实例entryA.js
,entryB.js
,entryC.js
——多页面应用的3个入口