这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战
五、Webpack 优化配置
5.1 开发环境性能优化
5.1.1 HMR(模块热替换)
HMR: hot module replacement 热模块替换 / 模块热替换
作用:一个模块发生变化,只会重新打包构建这一个模块(而不是打包所有模块) ,极大提升构建速度
代码:只需要在 devServer 中设置 hot 为 true,就会自动开启HMR功能(只能在开发模式下使用)
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
// 开启HMR功能
// 当修改了webpack配置,新配置要想生效,必须重启webpack服务
hot: true
}
每种文件实现热模块替换的情况:
-
样式文件:可以使用HMR功能,因为开发环境下使用的 style-loader 内部默认实现了热模块替换功能
-
js 文件:默认不能使用HMR功能(修改一个 js 模块所有 js 模块都会刷新)
--> 实现 HMR 需要修改 js 代码(添加支持 HMR 功能的代码)
// 绑定
if (module.hot) {
// 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
module.hot.accept('./print.js', function() {
// 方法会监听 print.js 文件的变化,一旦发生变化,只有这个模块会重新打包构建,其他模块不会。
// 会执行后面的回调函数
print();
});
}
-
注意:HMR 功能对 js 的处理,只能处理非入口 js 文件的其他文件。
-
html 文件: 默认不能使用 HMR 功能(html 不用做 HMR 功能,因为只有一个 html 文件,不需要再优化)
使用 HMR 会导致问题:html 文件不能热更新了(不会自动打包构建)
解决:修改 entry 入口,将 html 文件引入(这样 html 修改整体刷新)
entry: ['./src/js/index.js', './src/index.html']
5.1.2 source-map
source-map:一种提供源代码到构建后代码的映射的技术 (如果构建后代码出错了,通过映射可以追踪源代码错误)
参数:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
代码:
devtool: 'eval-source-map'
可选方案:[生成source-map的位置|给出的错误代码信息]
- source-map:外部,错误代码准确信息 和 源代码的错误位置
- inline-source-map:内联,只生成一个内联 source-map,错误代码准确信息 和 源代码的错误位置
- hidden-source-map:外部,错误代码错误原因,但是没有错误位置(为了隐藏源代码),不能追踪源代码错误,只能提示到构建后代码的错误位置
- eval-source-map:内联,每一个文件都生成对应的 source-map,都在 eval 中,错误代码准确信息 和 源代码的错误位
- nosources-source-map:外部,错误代码准确信息,但是没有任何源代码信息(为了隐藏源代码)
- cheap-source-map:外部,错误代码准确信息 和 源代码的错误位置,只能把错误精确到整行,忽略列
- cheap-module-source-map:外部,错误代码准确信息 和 源代码的错误位置,module 会加入 loader 的 source-map
内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快
开发/生产环境可做的选择:
开发环境:需要考虑速度快,调试更友好
-
速度快( eval > inline > cheap >... )
- eval-cheap-souce-map
- eval-source-map
-
调试更友好
- souce-map
- cheap-module-souce-map
- cheap-souce-map
最终得出最好的两种方案 --> eval-source-map(完整度高,内联速度快) / eval-cheap-module-souce-map(错误提示忽略列但是包含其他信息,内联速度快)
生产环境:需要考虑源代码要不要隐藏,调试要不要更友好
-
内联会让代码体积变大,所以在生产环境不用内联
-
隐藏源代码
- nosources-source-map 全部隐藏
- hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
最终得出最好的两种方案 --> source-map(最完整) / cheap-module-souce-map(错误提示一整行忽略列)
六、Webpack 配置详情
6.1 entry
entry: 入口起点
-
string --> './src/index.js',单入口
打包形成一个 chunk。 输出一个 bundle 文件。此时 chunk 的名称默认是 main
entry:{
index:'./src/js/index.js'
}
-
array --> ['./src/index.js', './src/add.js'],多入口
所有入口文件最终只会形成一个 chunk,输出出去只有一个 bundle 文件。
(一般只用在 HMR 功能中让 html 热更新生效)
entry:['./src/js/index.js','./src/js/add.js'],
-
object,多入口
有几个入口文件就形成几个 chunk,输出几个 bundle 文件,此时 chunk 的名称是 key 值
entry:{
index:'./src/js/index.js'
add:'./src/js/add.js'
},
6.2 output
output: {
// 文件名称(指定名称+目录)
filename: 'js/[name].js',
// 输出文件目录(将来所有资源输出的公共目录)
path: resolve(__dirname, 'build'),
// 所有资源引入公共路径前缀 --> 'imgs/a.jpg' --> '/imgs/a.jpg'
publicPath: '/',
chunkFilename: 'js/[name]_chunk.js', // 指定非入口chunk的名称
library: '[name]', // 打包整个库后向外暴露的变量名
libraryTarget: 'window' // 变量名添加到哪个上 browser:window
// libraryTarget: 'global' // node:global
// libraryTarget: 'commonjs' // conmmonjs模块 exports
},
6.3 module
module: {
rules: [
// loader的配置
{
test: /.css$/,
// 多个loader用use
use: ['style-loader', 'css-loader']
},
{
test: /.js$/,
// 排除node_modules下的js文件
exclude: /node_modules/,
// 只检查src下的js文件
include: resolve(__dirname, 'src'),
enforce: 'pre', // 优先执行
// enforce: 'post', // 延后执行
// 单个loader用loader
loader: 'eslint-loader',
options: {} // 指定配置选项
},
{
// webpack原本的loader是将每个文件都过一遍,比如有一个js文件 rules中有10个loader,第一个是处理js文件的loader,当第一个loader处理完成后webpack不会自动跳出,而是会继续拿着这个js文件去尝试匹配剩下的9个loader,相当于没有break。
而oneOf就相当于这个break
rules:[
oneOf: [
{
test:/\.css$/,
use:[...common_css_loader]
},
{
test:/\.less$/,
use:[...common_css_loader,'less-loader']
},
{
test:/\.html/,
loader:'html-loader'
}
]
]
}
},
6.4 resolve
// 解析模块的规则
resolve: {
// 配置解析模块路径别名: 优点:当目录层级很复杂时,简写路径;缺点:路径不会提示
alias: {
$css: resolve(__dirname, 'src/css')
},
// 配置省略文件路径的后缀名(引入时就可以不写文件后缀名了)
extensions: ['.js', '.json', '.jsx', '.css'],
// 告诉 webpack 解析模块应该去找哪个目录
modules: [resolve(__dirname, '../../node_modules'), 'node_modules']
}
这样配置后,引入文件就可以这样简写:import '$css/index';
6.5 dev server
devServer: {
// contentbase代表html页面所在的相对目录,如果我们不配置项,devServer默认html所在的目录就是项目的根目录
contentBase: resolve(__dirname, 'build'),
// 监视contentBase目录下的所有文件,一旦文件变化就会reload
watchContentBase: true,
watchOptions: {
// 忽略文件
ignored: /node_modules/
},
// 启动gzip压缩
compress: true,
// 端口号
port: 5000,
// 域名
host: 'localhost',
// 自动打开浏览器
open: true,
// 开启HMR功能
hot: true,
// 不要显示启动服务器日志信息
clientLogLevel: 'none',
// 除了一些基本信息外,其他内容都不要显示
quiet: true,
// 如果出错了,不要全屏提示
overlay: false,
// 服务器代理,--> 解决开发环境跨域问题
proxy: {
// 一旦devServer(5000)服务器接收到/api/xxx的请求,就会把请求转发到另外一个服务器3000
'/api': {
target: 'http://localhost:3000',
// 发送请求时,请求路径重写:将/api/xxx --> /xxx (去掉/api)
pathRewrite: {
'^/api': ''
}
}
}
}
其中,跨域问题:同源策略中不同的协议、端口号、域名就会产生跨域。
正常的浏览器和服务器之间有跨域,但是服务器之间没有跨域。代码通过代理服务器运行,所以浏览器和代理服务器之间没有跨域,浏览器把请求发送到代理服务器上,代理服务器替你转发到另外一个服务器上,服务器之间没有跨域,所以请求成功。代理服务器再把接收到的响应响应给浏览器。这样就解决开发环境下的跨域问题。
6.6 optimization
contenthash 缓存会导致一个问题:修改 a 文件导致 b 文件 contenthash 变化。
因为在 index.js 中引入 a.js,打包后 index.js 中记录了 a.js 的 hash 值,而 a.js 改变,其重新打包后的 hash 改变,导致 index.js 文件内容中记录的 a.js 的 hash 也改变,从而重新打包后 index.js 的 hash 值也会变,这样就会使缓存失效。(改变的是a.js文件但是 index.js 文件的 hash 值也改变了)
解决办法:runtimeChunk --> 将当前模块记录其他模块的 hash 单独打包为一个文件 runtime,这样 a.js 的 hash 改变只会影响 runtime 文件,不会影响到 index.js 文件
output: {
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build'),
chunkFilename: 'js/[name].[contenthash:10]_chunk.js' // 指定非入口文件的其他chunk的名字加_chunk
},
optimization: {
splitChunks: {
chunks: 'all',
/* 以下都是splitChunks默认配置,可以不写
miniSize: 30 * 1024, // 分割的chunk最小为30kb(大于30kb的才分割)
maxSize: 0, // 最大没有限制
minChunks: 1, // 要提取的chunk最少被引用1次
maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量为5
maxInitialRequests: 3, // 入口js文件最大并行请求数量
automaticNameDelimiter: '~', // 名称连接符
name: true, // 可以使用命名规则
cacheGroups: { // 分割chunk的组
vendors: {
// node_modules中的文件会被打包到vendors组的chunk中,--> vendors~xxx.js
// 满足上面的公共规则,大小超过30kb、至少被引用一次
test: /[\/]node_modules[\/]/,
// 优先级
priority: -10
},
default: {
// 要提取的chunk最少被引用2次
minChunks: 2,
prority: -20,
// 如果当前要打包的模块和之前已经被提取的模块是同一个,就会复用,而不是重新打包
reuseExistingChunk: true
}
} */
},
// 将index.js记录的a.js的hash值单独打包到runtime文件中
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
},
minimizer: [//数组里可以存放用于代码压缩的插件
// 配置生产环境的压缩方案:js/css
new TerserWebpackPlugin({
// 开启缓存
cache: true,
// 开启多进程打包
parallel: true,
// 启用sourceMap(否则会被压缩掉)
sourceMap: true
})
]
}