前言
自从新项目的技术栈启用vue以后,项目的构建工具也自然而然的从原来的内部的工具切换成了webpack,在感受到HMR,各式各样loader的强大后,也随着项目的逐渐变大,依赖的模块越来越多,webpack的构建效率成为了制约团队开发效率的短板。因此,我们来介绍一下多页面下,我们是如何优化webpack的效率的(毕竟本文标题是不完全指北,如果还有其他更好的方法,欢迎留言给我)。
项目背景
我们的项目是基于vue
的多页面项目
,webpack配置文件基于vue-cli
进行改写,因此在webpack中存在多个entry
,项目的大体结构如下
|---src
|---pages
|---xxx1 - 某业务页面1
|---App.vue - 该业务主入口vue组件
|---xxx1.html - (与目录同名,业务模板文件)
|---xxx1.js - (与目录同名,业务主入口js文件)
|---xxx2 - 某业务页面2
|---App.vue - 该业务主入口vue组件
|---xxx2.html - (与目录同名,业务模板文件)
|---xxx2.js - (与目录同名,业务主入口js文件)
下面,我们基于这样的多页面结构具体讲述一下我们是如何对webpack进行构建优化(基于webpack3)
公共代码提取
使用过vue-cli的童鞋都知道,生成模板项目的时候默认使用了** CommonsChunkPlugin来作为code splite工具,本质上通过配置minChunk提出公共部分代码,便于在多页面中缓存(如:页面A和B都有vendor.js,那么访问了页面A,下一次访问页面B,B中的vendor.js直接加载内存中的就好了),从而达到性能提升的目的。当是该Plugin**也有不足,即他是动态编译和进行code splite。怎么理解呢,即每次打包构建,他都会执行一次重复的去执行code splite, 而且因为minChunk策略各不相同,每一次上线以后,提取的公共代码vendor.js
内容可能因为版本的不同而不同,但是,像(vue, vuex vue-router)等三方库
基本上是稳定的,不需要根据业务的变化而变化。因此,基于此我们可以提取出这些第三方库提前预构建好,而不是让他随着版本再次构建
方法一
最简单的方式莫过于直接将这些js合并压缩混淆挂载在全局节点上,但是如果这样做,我们在业务代码中就只能通过window下的属性来使用它们提供的各个功能,打破了模块化的封装,因此该方案并不好。
方法二
考虑到CommonChunkPlugin的局限性,webpack官方提供了另外一个插件DllPlugin,这个插件需要和DLLReferencePlugin配合使用。
熟悉 Windows 的朋友就应该知道,DLL 所代表的含义。在 Windows 中,有大量的 .dll 文件,称为动态链接库。动态链接库提供了将应用模块化的方式,应用的功能可以在此基础上更容易被复用。
因此我们的目的即使用DLL插件,将不修改的模块公共部分提取出来单独打包
,我们先建立webpack.dll.config.js
,这个文件内容很简单。
const path = require('path');
const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const config = require('../config');
module.exports = {
entry: {
vendor: ['vue/dist/vue.esm.js', 'vuex', 'axios', 'vue-router', 'babel-polyfill', 'lodash'] // 所需要的打包前端公共模块
},
output: {
path: path.join(__dirname, '../static/js'), // 打包后文件输出的位置
filename: '[name].dll.js',
/**
* output.library
* 将会定义为 window.${output.library}
* 在这次的例子中,将会定义为`window.vendor_library`
*/
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({ //主要是使用这个插件去打包js
/**
* path
* 定义 manifest 文件生成的位置
* [name]的部分由entry的名字替换
*/
path: path.join(__dirname, '.', '[name]-manifest.json'),
/**
* name
* dll bundle 输出到那个全局变量上
* 和 output.library 一样即可。
*/
name: '[name]_library',
context: path.join(__dirname, '..')
}),
new UglifyJsPlugin({ // 使用这个插件可以混淆打包完成的js
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
})
]
};
执行 webpack --config build/webpack.dll.config.js
后,webpack会自动生成2个文件,其中vendor.dll.js即合并打包后第三方模块。另外一个vendor-mainifest.json存储各个模块和所需公用模块的对应关系。
将第三方模块打完包以后,我们就需要使用DLLReferencePlugin来将它和我们的业务代码进行融合,我们修改webpack.base.config(vue-cli生成配置)
,添加plugin如下:
plugins: [
new webpack.DllReferencePlugin({
context: __dirname, // 与DllPlugin中的那个context保持一致
manifest: require('./vendor-manifest.json')
}),
......
]
同时,我们还需要手动的将vendor.dll.js
插入类似index.html
这样的模板文件才可以生效
这样就完成了使用dll插件提取公共第三方库的操作,一般情况下,我们不会增加或者减少第三方库,但是一旦出现这种情况,我们都需要手动重新去打一个包来进行替换。那么有没有更自动的方式来完成这件事呢?
方法三
AutoDllPlugin出现在了我的视野,这个插件自动同时相当于完成了DllReferencePlugin
和DllPlugin
的工作,只需要在webpack.base.config
中添加
plugins: [
new AutoDllPlugin({
inject: true, // will inject the DLL bundles to html
context: path.join(__dirname, '..'),
filename: '[name]_[hash].dll.js',
path: 'res/js',
plugins: mode === 'online' ? [
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
})
] : [],
entry: {
vendor: ['vue/dist/vue.esm.js', 'vuex', 'axios', 'vue-router', 'babel-polyfill', 'lodash']
}
})
]
,不需要额外的webpack.dll.config.js
配置以及不需要手动将打完好的包拷贝到对应的模板文件中。
小结
大多数情况,我们推荐方法3,不过方法3相比方法2,增加了每次启动重新构建一次新的vendor.js,开发阶段首次启动会构建一次新的vendor,增加一些额外的时间(实测下来影响并不大),不过也避免了更新第三方库增减而忘记打包对业务产生的影响
多进程构建
webpack和其他大部分js工具相同都是单线程对项目进行处理,
然而 Webpack 这个工具强就强在流程设计的扩展性如此之强,可以人为的加上多进程处理。
其在编译文件流程如下:
1. 开始编译 (Compiler#run)
2. 开始编译入口文件 (Compilation#addEntry)
2.1 开始编译文件 (Compilation#buildModule => NormalModule#build)
2.2 执行 Loader 得到文件结果 (NormalModule#runLoaders)
2.3 根据结果解析依赖 (NormalModule#parser.parse)
2.4 处理依赖文件列表 (Compilation#processModuleDependencies)
2.5 开始编译每个依赖文件 (异步,从这里开始递归操作: 编译文件->解析依赖->编译依赖文件->解析深层依赖...)
这里的关键在于递归操作 2.5 开始编译每个依赖文件 这一步是异步设计,每个依赖文件的编译彼此之间互不影响。不过虽然是异步的,但还是跑在一个线程里。但是这样的设计却带来了多进程的可行性。
编译文件中主要的耗时操作在于 Loader 对源文件的转换操作,而 Loader 的可异步的设计使得转换操作的执行并不被限制在同一线程内。下面对 Loader 进行改造,使其支持多进程并发:
2.2 执行 Loader 得到文件结果
LoaderWrapper 作为新的 Loader 入口接收文件输入信息
LoaderWrapper 创建一个子进程 (child_process#fork) (这一步可维护一个进程池)
子进程中,通过调用原始 Loader,转换输入文件,然后把最终结果传递给父进程
父进程将收到的结果作为 Loader 结果传递给 Webpack
HappyPack 的实现就是这个流程,我们来使用babel-loader作为例子,来讲解一下HappyPack如何配置
通常情况下,我们使用的babel-loader
如下所示
webpack.base.config.js
...
module: {
rules: [
...
{
test: /\.js$/,
include: [resolve('src'), resolve('lib'),resolve('test'), resolve('node_modules/webpack-dev-server/client')], // 通过合理配置include也可以对提升构建性能
use: [
{
loader: 'babel-loader'
},
],
exclude: /node_modules/ // 通过合理配置exclude也可以对提升构建性能
}
}
转换成HappyPack,配置改写为
const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
// 省略其他配置
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: [resolve('src'), resolve('lib'), resolve('test'), resolve('node_modules/webpack-dev-server/client')],
use: [
{
loader: 'happypack/loader?id=happybabel' // 将loader换成happypack并将id指向插件id参数
},
],
exclude: /node_modules/
}
]
},
plugins: [
new HappyPack({ // HappyPack插件
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory=true'],
threadPool: happyThreadPool,
})
]
}
HappyPack不只可以对babel-loader进行处理,其他vue-loader,css-loader等都可以用他进行加速优化,只需要如上增加实例以及改写loader即可。使用HappyPack整体优化后,在我们的项目中,构建速度基本可以提高70%。
多页面html-webpack-plugin优化
作为webpack中的第一大插件html-webpack-plugin
,大家应该或多或少的使用过,这个插件会根据你的模板代码,通过不同的模板引擎构建出对应的html,ejs甚至ftl文件,在标准的SPA中,该插件性能不会性能瓶颈,但是如果你使用的是多页面,该插件的构建速度绝对是地狱级别的,
如,我只是简单修改了一个vue文件的一个文案,在阶段居然花费了16s,这大大减慢了开发效率,感受不到HMR的优势
我们找到html-webpack-plugin
中emit
事件钩子,注入事件代码
我们发现,他会对每一个入口文件都执行一遍emit中所有代码逻辑
,
因此,我们需要考虑,如何只在自己修改到的入口,执行emit下面的流程就好了。
在浏览了很多issure后,发现已经有现有的轮子帮助我们完成了判断和缓存的功能.
html-webpack-plugin-for-multihtml,
修改配置代码代码
const HtmlWebpackPlugin = require('html-webpack-plugin-for-multihtml');
// 省略其他代码
plugins:[
new HtmlWebpackPlugin({
template: filePath,
filename: `${filename}.html`,
chunks: ['manifest', 'vendor', filename],
inject: true,
multihtmlCache: true // 增加该配置
})
]
该插件通过在webpack done
钩子函数中设置相关变量,来保证原html-webpack-plugin
插件中emit仅触发一次全部流程。来达到提速的效果。升级以后,修改文案,HMR的速度从原来的秒级改为毫秒级。
参考文档
HappyPack - Webpack 的加速器