前言
在前一篇中,我们了解了webpack
的一些基本配置,在此节中,将继续介绍一些webpack
高级配置,以供于提升开发体验。
Source map
SourceMap
(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件方案。
其会生成一个xxx.map
文件,包含源代码与构建后代码每一行、每一列的映射关系。且打包后的bundle
文件中可以通过最后一行//# sourceMappingURL=xxx.map
关联到xxx.map
文件。
当构建后代码出错了,会通过xxx.map
文件,从构建后代码出错位置找到源代码出错位置,从而让浏览器提示源代码文件出错位置在,有助于我们快速找到错误位置。
在webpack中通过配置devtool控制是否生成,以及如何生成source map
官方文档中提供了许多devtool选项
大致一看配置项有很多,但其实只是几种关键字的组合搭配,每个关键字都代表一个特性:
eval
:不会生成xxx.map
文件,使用eval
包裹模块代码,其中sourceURL
指向源代码文件路径//# sourceURL=webpack://webpack/./src/xxx
inline
:不单独生成.map
文件,将其作为DataURI嵌入hidden
:不在源码末尾添加sourceURL
,让浏览器无法感知,但是可以将其放在服务器中,当页面发生异常时,将异常发回监控服务中,根据异常的堆栈信息与map
文件进行映射以确定具体位置cheap
:生成的sourcemap
没有列映射,也不包含loader
的sourcemap
module
:包含loader
的sourcemap
nosources
:生成的sourcemap
中不包含源代码内容,可以部署到生产环境中
验证 devtool 名称时, 我们期望使用某种模式, 注意不要混淆 devtool 字符串的顺序, 模式是:
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
如何选择devtool
开发环境
在开发环境中,我们的需求是重新构建时提供较快的速度,并且方便调试,官方推荐了以下四种:
eval
- 每个模块都使用eval()
执行,并且都有//# sourceURL
。此选项会非常快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码(没有从 loader 中获取 source map),所以不能正确的显示行数。
eval-source-map
- 每个模块使用eval()
执行,并且 source map 转换为 DataUrl 后添加到eval()
中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。
eval-cheap-source-map
- 类似eval-source-map
,每个模块使用eval()
执行。这是 "cheap(低开销)" 的 source map,因为它没有生成列映射(column mapping),只是映射行数。它会忽略源自 loader 的 source map,并且仅显示转译后的代码,就像eval
devtool。
eval-cheap-module-source-map
- 类似eval-cheap-source-map
,并且,在这种情况下,源自 loader 的 source map 会得到更好的处理结果。然而,loader source map 会被简化为每行一个映射(mapping)。
生产环境
在生产环境中,除了以上需求我们更加注意的还是源代码的安全性,如何配置才可以不存在源代码泄漏的风险,继续看一下官方推荐:
(none)
(省略devtool
选项) - 不生成 source map。source-map
- 整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。将你的服务器配置为,不允许普通用户访问 source map 文件!hidden-source-map
- 与source-map
相同,但不会为 bundle 添加引用注释。如果你只想 source map 映射那些源自错误报告的错误堆栈跟踪信息,但不想为浏览器开发工具暴露你的 source map,这个选项会很有用。你不应将 source map 文件部署到 web 服务器。而是只将其用于错误报告工具。nosources-source-map
- 创建的 source map 不包含sourcesContent(源代码内容)
。它可以用来映射客户端上的堆栈跟踪,而无须暴露所有的源代码。你可以将 source map 文件部署到 web 服务器。这仍然会暴露反编译后的文件名和结构,但它不会暴露原始代码。
当然,凡事不是绝对的,需要具体情况具体分析。
HMR(HotModuleReplacement)
HMR热模块替换:在程序运行中添加、替换或删除模块,而无需重新加载整个页面
Live Reloading 热重载:修改文件后,webpack自动编译,浏览器自动刷新,相当于
window.location.reload()
在我们开发过程中,我们自然希望修改了某个模块,就只有这个模块需要重新编译,其他模块没必要在重新请求加载,这种情况就需要配置HMR
修改配置
// webpack.dev.js
module.exports = {
// xxx
devServer: {
host: "localhost",
port: "3000",
open: true,
hot: true, // 开启HMR功能 只能用于开发环境
},
};
此时修改css样式资源,发现已经具备HMR功能了,但是js还不行
// main.js
// 判断是否支持HMR功能
if (module.hot) {
module.hot.accept("./js/sum", function () {
const result = sum(1, 2, 3, 4);
console.log(result);
});
}
此时修改
js/sum.js
文件时:通过控制台如下输出可以看出js文件的HMR也成功生效[HMR] Updated modules: [HMR] - ./src/js/sum.js [HMR] App is up to date.
在使用vue
或react
实际开发中,不需要上面那么麻烦,可以分别借助vue-loader
,react-refresh
插件实现。
OneOf
当前配置打包时每个文件都会经过所有的loader
处理,即使配置了test
正则,仍然会遍历所有的loader
oneOf:
规则
数组,当规则匹配时,只使用第一个匹配规则
修改配置
// webpack.dev.js / webpack.prod.js
module.exports = {
// xxx
module: {
rules: [{
oneOf: [
// 配置loader
],
}],
},
};
对于同一类型文件 如果需要多个loader处理,例如js文件,可以单独抽离出oneof,确保oneof中一个文件类型对应一个loader
Babel/Eslint Cache
每次打包js文件都会经过Eslint
检查与Babel
编译,我们可以对其检查结果做缓存,以提升二次构建速度。
修改配置
// webpack.prod.js
module.exports = {
// xxx
module: {
rules: [{
test: /.js$/,
// node_modules目录下文件无需编辑
exclude: /node_modules/,
use: [{
loader: "babel-loader",
options: {
/**
* cacheDirectory:默认值为 false。
* 当有设置时,指定的目录将用来缓存 loader 的执行结果。
* 之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、
* 高性能消耗的 Babel 重新编译过程(recompilation process)。
* 如果设置了一个空值 (loader: 'babel-loader?cacheDirectory')
* 或者 true (loader: 'babel-loader?cacheDirectory=true'),l
* oader 将使用默认的缓存目录 node_modules/.cache/babel-loader,
* 如果在任何根目录下都没有找到 node_modules 目录,将会降级回退到操作系统默认的临时文件目录。
*/
cacheDirectory: true,
/**
* cacheCompression:默认值为 true。
* 当设置此值时,会使用 Gzip 压缩每个 Babel transform 输出。
* 如果你想要退出缓存压缩,将它设置为 false
* 如果你的项目中有数千个文件需要压缩转译,那么设置此选项可能会从中收益。
*/
cacheCompression: false,
}},
],
}],
},
plugins: [
new ESLintPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
cache: true,
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/eslint")
})
]
};
注意
babel-loader
与eslint-webpack-plugin
的开启缓存字段名称稍有不同
Thread-loader
多进程打包 需要安装
thread-loader
使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。
每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。
请仅在耗时的操作中使用此loader!!!!
修改配置
// webpack.prod.js
// nodejs核心模块
const os = require("os");
// 获取当前电脑cpu核数
const threads = os.cpus().length;
module.exports = {
// xxx
module: {
rules: [
{
test: /.js$/,
// node_modules目录下文件无需编辑
exclude: /node_modules/,
use: [
{
loader: "thread-loader",
options: {
workers: threads,
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
},
},
],
},
],
},
plugins: [
new ESLintPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
cache: true,
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/eslint"),
// 开启多进程
threads,
}),
],
};
我们目前打包内容很少 配置多进程打包后实际会使此项目打包时间增加
@babel/plugin-transform-runtime
Code Split
多入口打包
在一些大型项目中,我们开发一般会按模块开发,这种时候如果项目是单入口打包,即使我想改动的模块很小,也要将整个项目全量启动。这种情况,我们就可以按照模块来配置多入口打包,开发哪个模块就启动哪个模块。
src下新建index.js文件作为另一个入口文件
修改配置
// webpack.prod.js
module.exports = {
entry: {
main: "./src/main.js",
index: "./src/index.js",
},
output: {
// 文件输出路径 要求绝对路径
path: path.resolve(__dirname, "../dist"),
/**
* [name]是webpack命名规则,使用chunk的name作为输出的文件名。
* 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
* chunk的name是啥呢? 比如:entry中xxx: "./src/yyy.js", name就是xxx。和文件名无关。
*/
filename: "js/[name].js",
// 在打包之前 将path整个目录内容清空 再进行打包
clean: true,
},
}
splitChunks
提取重复代码,如果多处入口文件中都引用了同一份代码,如果我们不做处理,这份代码会被打包到每一个入口文件中,导致代码重复,体积过大。
此时需要借助splitChunks提取多入口的重复代码,只打包生成一个js文件
修改文件
两份入口文件都同时引入了./js/sum
文件
// src/index.js
import sum from "./js/sum";
const res = sum(1, 2);
console.log(res);
// src/main.js
import sum from "./js/sum";
const res = sum(3, 4);
console.log(res);
修改配置
// webpack.prod.js
module.exports = {
// xxx
optimization: {
splitChunks: {
// 对所有模块都进行分割
chunks: "all",
// 以下是默认值
// minSize: 20000, // 分割代码最小的大小
// minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
// minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
// maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
// maxInitialRequests: 30, // 入口js文件最大并行请求数量
// enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
// cacheGroups: { // 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\/]node_modules[\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
// default: { // 其他没有写的配置会使用上面的默认值
// minChunks: 2, // 这里的minChunks权重更大
// priority: -20,
// reuseExistingChunk: true,
// },
// },
// 修改配置
cacheGroups: {
// 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\/]node_modules[\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
default: {
// 其他没有写的配置会使用上面的默认值
name: "sum",
minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
}
观察输出文件
- 文件结构
├── dist
| ├── js
| ├──main.js
| ├──index.js
| └──sum.js
可以看到sum.js
文件已经被单独打包出成一个文件,而main.js以及index.js
通过引入sum.js
文件来进行使用。