webpack
webpack 构建流程
webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
一、初始化参数
从配置文件和shell语句中读取与合并参数,得出最终的参数
二、开始编译
用上一步得到的参数初始化Compiler(编译器)对象,加载所有配置的插件,执行对象的run方法开始执行编译
三、确定入口
根据配置中的entry找出所有的入口文件
四、编译模块
从入口文件出发,调用所有配置的loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤,直到所有入口依赖的文件都经过了本步骤的处理
五、完成模块编译
在经过第四步使用loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
六、输出资源
根据入口和资源之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会
七、输出完成
在确定好输出内容后,根据配置确定输出的路径和文件名,将文件内容写入到文件系统
在以上过程中,webpack会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,
并且插件可以调用webpack提供的API改变webpack的运行结果。
八、总结
- 初始化: 启动构建,读取与合并配置参数,加载plugin,实例化Complier
- 编译: 从entry出发,针对每个module串行调用对应的loader去翻译文件的内容,再找到该modle依赖的module,递归的进行编译处理
- 输出: 将编译后的module组合成Chunk,将Chunk转换成文件,输出到文件系统中
webpack 配置优化
- webpack在启动时会从配置的Entry出发,解析出文件中的导入语句,再递归解析依赖包
- 对于导入语句webpack会做出以下操作:
- 根据导入语句寻找对应的要导入的文件
- 再根据要导入的文件后缀,使用配置中的loader去处理文件(如ES6需要使用babel-loader)
- 针对这两点可以优化查找途径
- 优化loader配置
-
loader处理文件的转换操作时很耗时的,所以需要让尽可能少的文件被loader处理
{ test: /.js$/, use: [ 'babel-loader?cacheDirectory', // 开启转换结果缓存 ], include: path.resolve(__dirname, 'src'), // 只对src目录中文件采用babel-loader exclude: path.resolve(__dirname, './node_modules' ) // 排除node_modules目录下的文件 }
-
exclude/include 通过exclude、include配置来确保转译尽可能少的文件; exclude指定要排除的文件,include指定要包含的文件 exclude优先级高于include,在include和exclude中使用绝对路径, 尽量避免使用exclude,更倾向于使用include // webpack.config.js const path = require('path') module.export = { // ... module: { rules: [ { test: /.js[x]?$/, use: ['babel-loader'], include: [path.resolve(__dirname, 'src')] }, ... ] } }
-
cache-loader 在一些性能开销较大的loader之前添加cache-loader,将结果缓存到磁盘中; 默认保存在node_modueles/.cache/cache-loader目录下。
- 安装依赖 npm i cache-loader -D
- cache-loader的配置很简单,放在其它loader之前即可 { test: /.js[x]?$/, use: ['cache-loader','babel-loader'], include: [path.resolve(__dirname, 'src')] }
- 注意:
- 如果只打算给babel-loader配置cache,可以直接给babel-loader添加选项cacheDirectory
- 默认缓存目录: node_modules/.cache/babel-loader,
- 开启babel-loader缓存和配置cache-loader,构建时间很接近
- resolve.modules 配置
不推荐使用
-
resolve.modules用于配置webpack去哪些目录下寻找第三方模块,默认是['node_modules'],但是,它会先去当前目录的./node_modules查找,没有的话再去../node_modules最后的根目录
-
所以当安装的第三方模块都放在项目根目录时,就没有必要一层层的查找,直接指明存放的绝对位置
resolve: { modules: [path.resolve(__dirname, 'node_modules')] }
-
可能出现的问题
- 你的依赖中还存在node_modules目录,就会出现,对应的文件存在,却提示找不到,因此个人不推荐
- 优化resolve.extensions 配置
项目较小,优化效果不明显
- 在导入没有文件后缀的路径时,webpack会自动带上后缀去尝试访问文件是否存在,而resolve.extensions用于配置尝试后缀列表,默认为: resolve: { extensions: ['.js', 'jsx', '.vue'] }
- 当遇到require('./data')时webpack会先尝试寻找data.js,没有再去找data.jsx;如果列表越长,或正确的后缀越往后,尝试的次数就会越多
- 在配置时为提升构建优化须遵守:
- 频率出现高的文件后缀优先放在前面
- 列表尽可能的小
- 书写导入语句时,尽可能写上后缀名
- HappyPack 并行构建优化
注: vue-loader不支持happypack,可以使用thread-loader来进行加速
当项目不是很复杂时,不需要配置happypack,因为进程的分配和管理也需要时间
-
安装happypack npm i happypack -D
-
核心原理: 将webpack中最耗时的loader文件转换操作任务,分解到多个进程中并行处理,从而减少构建时间
-
HappyPack
-
接入HappyPack
- 安装: npm i -D happypack
- 重新配置rules部分,将loader交给happypack来配置
-
参数:
- threads: 代表开启几个子进程去处理这类文件,默认3个
- verbose: 是否运行HappyPack输出日志,默认true
- threadPool: 代表共享进程池,即多个HappyPack示例使用一个共享进程池中的子进程去处理任务,以防资源占有过多
-
注:
- 当postcss-loader配置在HappyPack中,必须在项目中创建postcss.config.js // postcss.config.js module.exports = { plugins: [ require('autoprefixer')() ] }
-
示例 const HappyPack = require('happypack'); const happyThreadPool = HappyPack.ThreadPool({size: 5}); // 构建共享进程池,包含5个进程 // const os = require('os) // 开辟一个线程池 // 拿到系统CPU的最大核数,happypack 将编译工作灌满所有线程 // const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
plugins: [ // happypack 并行处理 new HappyPack({ // 用唯一ID来表示当前HappPack是用来处理一类特定文件的,与rules中的use对应 id: 'babel', loaders: ['babel-loader?cacheDirectory'], // 默认设置loader处理 threadPool: happyThreadPool // 使用共享进程池处理 }), new HappyPack({ id: 'css', loaders: [ 'css-loader', 'postcss-loader', 'sass-loader' ], threadPool: happyThreadPool }), ], module: { rules: [ { test: /\.(js|jsx)$/, use: ['happypack/loader?id=babel'], exclude: path.resolve(__dirname, './node_modules') }, { test: /\.(scss|css)$/, // 使用mini-css-extract-plugin 提取css此处,如果放在上面会出错 use: [MiniCssExtractPlugin.loader, 'happypack/loader?id=css'], include: [ path.resolve(__dirname, 'src'), path.join(__dirname, './node_modules/antd') ] }, { rest: /\.vue$/, use: [ 'thread-loader', 'vue-loader' ] } ] }
- ParallelUglifyPlugin代替自带UglifyJsPlugin 代码压缩插件
当前webpack使用TerserWebpackPlugin,默认就开启了多进程和缓存,构建时,你的项目中可以看到terser的缓存文件node_modules/.cache/terser-webpack-plugin。
- 自带JS压缩插件是单线程执行,而webpack-parallel-uglify-plugin可以并行执行
- 配置参数:
- uglifyJS: {} 用于压缩ES5代码时的配置
- test: /.js
/
- include: [] 使用正则区包含被压缩的文件
- exclude: [] 使用正则区包含不被压缩的文件
- cacheDir: '' 缓存压缩后的结果
- workerCount: '' 开启几个子进程去并行的执行压缩,默认cpu核数减1
- sourceMap: false; 压缩的代码是否生成对应的Source Map module.export = { // 优化 optimization: { minimizer: [ // webpack:production模式默认有js压缩,但是如果设置了css压缩,js压缩也要重新设置 // 因为使用了minimizer会自动取消webpack的默认配置 new optimizeCssPlugin({ assetNameRegExp: /.css$/g, cssProcessor: require('cssnano'), cssProcessorOptions: {discardComments: {removeAll: true}}, canPrint: true }), new ParallelUglifyPlugin({ cacheDir: '.cache/', uglifyJS: { output: { beautify: false, // 是否输出可读性较强的代码,设置为false comments: false // 是否保留代码中的注释,false }, compress: { warnings: false, // 删除无用代码时是否输出警告信息 drop_console: true, // 是否删除代码中的console // 是否内嵌虽然已经定义了,但是只用到一次的变量, // 比如将 var x = 1; y = x, 转换成 y = 1, 默认为否 collapse_vars: true } } }) ] } }
- HardSourceWebpackPlugin
HardSourceWebpackPlugin为模块提供中间缓存,
缓存默认存放路径是: node_modules/.cache/hard-source
在首次构建时没有太大变化,但第二次开始,构建时间大约可以节约80%
- 安装依赖 npm i hard-source-webpack-plugin -D
- 修改webpack配置 // webpack.config.js const HardSourcePlugin = require('hard-source-webpack-plugin') module.exports = { // ... plugins: [ new HardSourceWebpackPlugin() ] }
- noParse
如果一些第三方模块没有AMD/CommonJS规范版本,可以使用noParse来标识这个模块;
这样webpack会引入这些模块,但不会进行转化和解析,从而提升webpack构建性能,如lodash、jquery
- noParse属性值是一个正则表达式或是一个function // webpack.config.js module.exports = { // ... module: { noParse: /jquery|lodash/ } } 如果项目中使用了不需要解析的第三方依赖,那配置noParse很显然是一定会起到优化作用的
- DllPlugin
有时,如果所有的JS文件都打成一个JS文件,会导致最终生成的JS文件很大,
这时,我们就要考虑拆分bundles
- DllPlugin和DllReferencePlugin可以实现拆分bundles,并可以大大提升构建速度
- DllPlugin和DllReferencePlugin都是webpack的内置模块
我们使用DllPlugin将不会频繁更新的库进行编译,当这些依赖的版本没有变化时,就不需要重新编译
我们新建一个webpack的配置文件,专门用于编译动态链接库,如名为: webpack.config.dll.js
// webpack.config.dll.js
const webpack = require('webpack')
const path = require('path')
module.exports = {
entry: {
react: ['react', 'react-dom']
},
mode: 'production',
output: {
filename: '[name].dll.[hash:6].js',
path: path.resolve(__dirname, 'dist', 'dll'),
library: '[name]_dll' // 暴露给外部使用
// libraryTarget 指定如何暴露内容,缺省时就是 var
},
plugins: [
new webpack.DllPlugin({
// name和library必须一致
name: '[name]_dll',
// manifest.json的生成路径
path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
})
]
}
在package.json的scripts中增加:
{
"scripts": {
"dev": "NODE_ENV=development webpack-dev-server",
"build": "NODE_ENV=production webpack",
"build:dll": "webpack --config webpack.config.dll.js"
},
}
执行 npm run build:all,可以看到dist目录如下:
之所以将动态链接库单独放在 dll 目录下,主要是为了使用 CleanWebpackPlugin 更为方便的过滤掉动态链接库。
dist
--dll
--manifest.json
--react.dll.xxx.js
manifest.json用于让DllReferencePlugin映射到相关依赖上
修改webpack的主配置文件: webpack.config.js的配置:
// webpack.config.js
const webpack = require('webpack')
const path = require('path')
module.exports = {
// ...
devServer: {
contentBase: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
}),
new CleanWebpackPlugin({
// 不删除dll目录
cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**']
}),
// ...
]
}
使用npm run build构建,可以看到bundle.js的体积大大减小
修改public/index.html文件,在其中引入react.dll.xxx.js
<script src="/dll/react.dll.xxx.js"></script>