一.提高构建速度
-
缩小文件搜索范围
1.通过exclude、include 缩小搜索范围
module.exports = {
module:{
rules:[
{
test:/\.js$/,
loader:'babel-loader',
// 只在src文件夹中查找
include:[resolve('src')],
// 排除的路径
exclude:/node_modules/
}
]
}
}
2. 合理利用resolve 字段配置
2.1 配置resolve.modules:[path.resolve(__dirname,'node_modules')]避免层层查找
其中 resolve.modules会告诉webpack去哪些目录寻找第三方模块,如果不配置 path.resolve(__dirname,'node_modules'),则会依次查找./node_module、../node_modules,一层一层网上找,这显然效率不高。
2.2对庞大的第三方模块设置 resolve.alias,使webpack直接使用库的min文件,避免库内解析
可以通过别名的方式来映射一个路径,能让Webpack更快找到路径。
比如:
resolve.alias:{
'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
副作用是会影响Tree-Shaking,适合对整体性比较强的库使用。
2.3 resolve.extensions ,减少文件查找
resolve.extensions 用来表明文件后缀列表,默认查找顺序是:['.js','.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在后面。
-
缓存之前构建过的js
将Babel编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间。
loader:'babel-loader?cacheDirectory=true'
-
提前构建第三方库
处理第三方库的方法有很多种,其中,Externals不够聪明,一些情况下会引发重复打包的问题;而 CommonsChunkPlugin 每次构建时都会重新构建一次 vendor;处于效率考虑还是考虑使用DllPlugin。
DLL全称Dynamic-link library,(动态链接库)。到底怎么个动态法。原理是将网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去dll中获取,而且通常都是第三方库。那么为什么能提升构建速度,原因在于这些第三方模块如果不升级,那么只需要被构建一次。
使用方法
1.构建dll文件,表示有哪些库需要被提前打包出去。
// 单独配置在一个文件中
// webpack.dll.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry:{
// 统一打包的类库
vendor:['react']
},
output:{
path:path.join(__dirname,'dist'),
filename:'[name].dll.js',
library:'[name]-[hash]'
},
plugins:[
new webpack.DllPlugin({
// name 必须和 output.library 一致
name:'[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path:path.join(__dirname,'dist','[name]-manifest.json')
})
]
}
ps:
1.需要注意DllPlugin的参数中name值必须和output.library值保持一致
2.生成的manifest文件中会引用output.library值
2.接下来使用DllReferencePlugin 将依赖的第三方库引入项目
// webpack.config.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
//还有另外一种写法: 在html页面引入的方式
module.exports = {
// ...省略其他配置
plugins: [
// 告诉webpack哪些库不需要参与打包,同时使用时的名称也得变~
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: resolve('./dist/vendor-manifest.json'),
}),
// 将某个文件打包输出出去,并在html中自动引入该资源文件
new AddAssetHtmlWebpackPlugin({
filepath:resolve(__dirname,'dist/vendor.js')
})
]
}
-
并行构建而不是同步构建
受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。HappyPack和ThreadLoader作用是一样的,都是同时执行多个进程,从而加快构建速度。而Thread-Loader是webpack4提出的。
-
采用HappyPack开启多进程Loader
使用方式
npm i happypack -D
// webpack.config.json
const path = require('path');
const HappyPack = require('happypack');
module.exports = {
//...
module:{
rules:[{
test:/\.js$/,
// 这里的id对应下面的id
use:['happypack/loader?id=babel']
exclude:path.resolve(__dirname, 'node_modules')
},{
test:/\.css/,
use:['happypack/loader?id=css']
}],
plugins:[
new HappyPack({
id:'babel',
loaders:['babel-loader?cacheDirectory']
}),
]
}
}
-
ThreadLoader开启多进程
使用方式
module.exports = {
{
test:/\.js$/,
exclude:/node_modules/,
use:[
{
loader:'thread-loader',
options:{
wordkers:2 // 进程2个
}
},
...
]
}
需要注意的是,开启多进程的意义在于需要构建的文件的体积足够大,如果不够大,不然效果不够明显,明显有高射炮打小苍蝇的感觉。因为进程开启需要时间,进程之间的通信也需要时间,如果这点时间都不能忽略不计,那么意义不大。
-
采用Oneof
{
// 处理less资源
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
// 处理css资源
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
// 处理es6资源
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:[
[
// ...
]
]
}
},
{
// 处理图片资源
test: /\.(jpg|png|gif)$/,
loader: 'url-loader',
// ...
},
{
// 处理html中img资源
test: /\.html$/,
loader: 'html-loader'
},
{
// 处理其他资源
exclude: /\.(html|js|css|less|jpg|png|gif)/,
loader: 'file-loader',
}
通常来讲,同一种类型的文件只能由一个loader处理,那么正常来讲的逻辑应该是,比如我是一个css文件,那么我匹配到test为css后缀的loader我就应该立即执行了,但是事实是,虽然匹配到了,但是还是会遍历完整一遍再进行解析,这样来讲效果明显就更低了。
而Oneof语法就是解决这个问题的,使文件一旦匹配上loader之后就立即解析,省去了全盘遍历这个不必要的过程。
ps:如果对于需要多个loader共同解决的文件类型,比如js。那就需要把其中的loader放历Oneof之外,这样才能实现loader也能同时执行到:
举个例子:
module.exports = {
// 忽略前面配置
rules:[
// 先把eslint-laoder单独从Oneof拎出来
{
test:/\.js$/,
exclude:/node_modules/,
loader:'eslint-loader',
enforce:'pre',
options:{
fix:true // 自动检查风格错误
}
},
{
oneOf:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
// ...
}
]
}
]
}
-
HMR 模块热替换
原理
全称Hot Module Replacement 热模块替换,作用是当一个模块发生改变时,之后只会打包这样一个模块(而不是打包所有模块),极大提升构建速度。
配置
// webpack.config.js
module.exports = {
// 在DevServer的配置基础上
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
// 开启HMR功能
// 当修改了webpack配置,新配置要想生效,必须重新webpack服务
hot: true,
},
mode:'development'
}
-
html文件处理
默认不能使用HMR功能,而且对于html文件一般也不做热更新。对于单页面来讲,因为一旦html更新了,那么整个页面都刷新了,称不上真正意义的热更新。
-
css文件处理
默认情况下css可以使用HMR功能,因为style-loader内部实现了。
-
js文件处理
默认不能使用HMR功能,需要修改js代码,添加支持HMR功能代码
而且,HMR功能对js的处理,只能处理非入口js文件。
// index.js
import print from './print'
console.log('index js 被加载了。。。')
// 如果开启HMR功能
if(module.hot){
// 方法会监听 print.js 文件的变化,一旦发生变化,其他模块不会重新打包构建
// 重新执行的效果会打印:print被加载了~~~ print已经被加载了000
// 但是index.js 这个模块没有改变,所以 index.js 不会重新打印
module.hot.accept('./print.js',function(){
print()
})
}
二.压缩打包体积
-
删除冗余代码
使用TreeShaking删除无用代码,这里的无用指的是当发现引入模块的某些内容在其他地方并没有使用时,就被当作无用节点,从而被删掉。看起来时高级的技术,但是在有些版本也有被误杀的可能性。
-
使用前提
- 依赖ES6的import、export模块化语法
- webpack.config.js 的 mode:'production'
针对第一点,需要修改一下.babelrc
{
"presets":[
[
"env",
{"module":false} // 关闭babel的模块转换功能,保留es6模块化语法
]
]
}
现在举个例子说明一下TreeShaking的作用:
// index.js 打包入口
import {mul} from 'print.js'
console.log(mul(2,3));
// print.js
export function mul(a,b){
return a * b
}
// 无用的结点
export function count(a,b){
return a - b
}
// 假设需要打包的就这两个文件,那么count就应该在构建后的文件里面被删除掉。这就是TreeShaking的作用,可以有效减少打包的体积。
只是也会有翻车的时候,在某些版本的webpack中对于某些第三方库,或者css文件也有被误删的可能性,万一发现这种情况,可以在 package.json 中配置 "sideEffects": [*.css] ,代表css不想被摇掉。
-
压缩代码
// 配置说明:webpack4
module.exports = {
plugins:[
// 压缩css
new OptimizeCssAssetWebpackPlugin(),
// 压缩html
new HtmlWebpackPlugin({
// html无所谓压缩,只是去掉空格和注释
minify:{
collapseWhitespace:true,
removeComments:true,
},
template:'./src/index.html'
})
],
// 模式设置为生产环境就可以直接压缩js了
mode:'production'
}
-
代码分割实现按需加载
原理
单页应用的一个问题在于使用一个页面承载复杂的功能,要加载的文件体积很大,不进行优化的话会导致首屏加载时间过长,影响用户体验。做按需加载可以解决这个问题。具体方法如下:
- 将网站功能按照相关程度划分成几类
- 每一类合并成一个Chunk,按需加载对应的Chunk
- 例如,只把首屏相关的功能放入执行入口所在的Chunk,这样首次加载少量的代码,其他代码要用到的时候再去加载。最好提前预估用户接下来的操作,提前加载对应代码,让用户感知不到网络加载
用法
网页首次只加载main.js ,网页展示一个按钮,点击按钮时加载风格出去的show.js,加载成功过后执行show.js 里的函数。
// main.js
document.getElementById('btn').addEventListener('click',function(){
// 自定义打包chunk名
import(/*webpackChunkName:'show'*/,'./show')
.then((show)=>{
show('Webpack')
})
})
// show.js
module.exports = function(content){
window.alert('hello' + content)
}
// webpack.config.js
module.exports = {
// code split需要的配置
optimization:{
splitChunks:{
chunks:'all'
},
},
mode:'production'
}
按需加载的关键是 import().then().catch(e),可以看得出来是是PromiseAPI,所以对于不支持PromiseAPI要额外加载插件处理,比如现在core-js就已经取代了Polyfill。/* webpackChunkName:show */
是定义动态生成的Chunk的名称,默认名称是[id].js,定义名称方便调试代码。
-
Scope Hoisting
原理
作用域提升,用来分析模块间的依赖关系,尽可能将打散的模块合并到一个函数中,又不能造成代码冗余,所以只有被引用一次的模块才能被合并。 由于需要分析模块间的依赖关系,所以源码IXUS采用了ES6模块化的,否则Webpack会降级不采用 Scope Hoisting。
使用方式
// 比如我希望打包一下两个文件
// test.js
export const a = 1
// index.js
import {a} from './test.js'
对于这种情况,我们打包出来的代码会类似这样
[
/* 0 */
function(module,export,require){
// ..
}
/* 1 */
function(module,exports,require){
// ..
}
]
但如果使用Scope Hoisting,代码就尽可能的合并到一个函数中去,也就变成了
[
/* 0 */
function(module,exports,require){
// ...
}
]
在webpack4 中,需要配置
module.exports = {
optimization:{
concatenateModules:true
}
}
-
优化运行速度
1.资源文件缓存
除了刚才针对babel-loader在编译时的缓存,如果服务端压力太大,我们这时在服务设置,对构建路径下的文件都设置cookie实现强缓存。
就像如下:
app.use('./build', { maxAge: 1000 * 3600 }); // 设置强缓存
那么这时候,对于打包工作而言我们需要做到两点:
- 未发生改变的文件,重新构建之后文件名不变,这样能保证缓存到。
- 发生改变的文件,重新构建之后需要改动,这样才能保证拿到的是最新的代码,不至于源代码需要不起作用。
基于以上考虑,决定给文件后缀添加hash值的方式来解决这个问题。
Webpack针对添加hash值的方案有三个
- hash
效果:这样会导致,打包的hash值都一样,但是每次改动,假如我只改了css文件,但是js文件也会重新获取。(即缓存失效)
- chunkhash
效果:这里先来理解一下,chunk是个什么概念。假如从一个入口文件 index.js 开始,
// index.js
import main from '../src/css/main.css'
同个入口文件引入的文件,它们都同属于一个chunk,因此chunkhash都相同,那么就很明显,它也达不到我们的要求。加入我修改的css文件,index.js 的缓存照样缓存不到。
- contenthash
只根据修改内容来的hash值。真正做到了根据内容来决定资源文件到底需不需要构建。这种明显就是理想方案了。可以同时保证修改,能获取最新内容的同时,对于未修改的文件也能拿到缓存。
配置方式
module.exports = {
// 单入口
entry : './src/js/index.js',
output : {
filename : 'js/built.[contenthash:10].js',
path: resolve(__dirname,'build')
},
plugins:[
// 独立输出css代码
new MiniCssExtractPlugin({
filename:'css/built.[contenthash:10].css'
}),
]
}
2.预加载
原理
这个需要跟前面代码分割结合起来,前面那个例子是懒加载,在 main.js 中点击某个按钮是才触发加载某个特定的模块。这个预加载的意思是,这个show.js (子模块)在主页面加载完之后,再“偷偷”地进行异步加载,这样一来有两个好处:
第一,异步加载不会导致页面卡死的情况。
第二,预先加载好等待点击用户体验会更好。
但是也有弊端:就是目前来看,支持还不够好,兼容性上有些问题。
配置方式
// 针对前面代码分割的例子,修改一下 main.js 的内容即可
document.getElementById('btn').addEventListener('click',function(){
// 自定义打包chunk名
import(/*webpackChunkName:'show',webpackPrefetch:true**/,'./show')
.then((show)=>{
show('Webpack')
})
})
-
优化开发体验
1.Dev-Server 自动刷新
原理
通过安装webpack-dev-server,相当于自己本地起了一个开发服务器。源代码一旦做了修改,保存之后就可以直接查看修改内容。不需要反复使用webpack命令反复打包。
Dev-Server刷新浏览器有两种方式:
- 向网页中注入代理客户端代码,通过客户端发起刷新
- 向网页装入一个iframe,通过刷新iframe实现刷新效果。
特点
只会在内存中编译打包,不会有任何输出。
配置方式
默认情况下,以及devServer:{inline:true}都是采用第一种方式刷新页面。第一种方式DevServer因为不知道网页依赖哪些Chunk,所以会向每个chunk中都注入客户端代码,当要输出很多chunk时,会导致构建变慢。而一个页面只需要一个客户端,所以关闭inline模式可以减少构建时间,chunk越多提升越明显。
// webpack.config.js
// 安装 webpack-dev-server
// 启动指令 : npx webpack-dev-server
module.exports = {
devServer:{
// 关闭inline模式
inline:false
// 项目路径
contentBase:resolve(__dirname,'build'),
// 启动gzip压缩,默认为false
compress:true,
// 指定端口
port:8090,
// 自动打开浏览器
open:true,
}
plugins:[
// 实践证明,如果不加上下面一行,将会导致没办法自动刷新,没有Live Reloading enabled的功能
new webpack.HotModuleReplacementPlugin()
]
// 需要注意,必须时开发环境下
mode:'development'
}
2.sourceMap提高调试体验
概念
提供一种到构建后代码映射技术,如果构建后代码报错,可以通过映射追踪到源代码。
作用
- 在开发环境下可以提升构建速度和调试速度
- 在生产环境可以减小代码体积
选项
[inline-|hidden-|eval|-][nosources-][cheap-[module-]] source-map
总结
开发环境的选择
开发环境的要求是:1.速度快, 2. 调试更友好
从速度上看, eval > inline > cheap > ...
所以选择是: eval-source-map、 eval-cheap-source-map。
从调试方便来选择,source-map、cheap-module-source-map、cheap-source-map。
其中 cheap-module-source-map 和 cheap-source-map 的区别在于,cheap-module-source-map可以把loader的sourcemap也加进来,调试内容比较完整。
折中方案是: eval-source-map 。(react和vue脚手架就是用这个)
生产环境的选择
生产环境的要求是:
不能让代码体积太大,所以不用内联模式。
1.源代码到底要不要隐藏 ?
如果需要则选择:nosources-source-map (全部隐藏)、hidden-source-map(只隐藏源代码,会提示构建后代码)。
2.要不要对调试友好。
如果有需要,则选择 source-map