参考
尚硅谷新版Webpack5实战教程(从入门到精通)
揭秘webpack loader | ChampYin's Blog
深入浅出 Webpack
Creating a Custom webpack Plugin | DigitalOcean
疑问
已解决
只用css-loader,而不用style-loader得到的代码是怎样的?
loader 们做了什么?
css 文件中的依赖是谁处理的,webpack?
css-loader 将 css 文件读取为字符串后,给了 webpack 什么呢?
webpack-html-plugin 对 html 文件处理前,会先让 html-loader 处理,为什么?
core-js 的配置项 version 有什么用?
未解决
css-loader 将css文件变成commonjs模块加载js中,里面内容是样式字符串。变成commonjs模块是什么意思?
模块热替换中,前端的已打包的代码是如何精确更新的?
MiniCssExtractPlugin.loader 做了什么?
hot: true 之后对 HTML 文件做修改,为什么页面不能自动刷新?
devServer 中的 watchContentBase 作用是什么?
optimization 中的 runtimeChunk 解决的问题是由于动态引入模块导致的吗?
Loader 工作原理
webpack 只能直接处理 js 格式的代码。任何非 js 文件都必须被预先处理转换为 js 代码,才可以参与打包。loader 就是这样一个代码转换器。它由 webpack 的
loader runner执行调用,接收原始资源数据作为参数(当多个加载器联合使用时,上一个loader的结果会传入下一个loader),最终输出 js 代码(和可选的 source map)给 webpack 做进一步编译。
总结:loader 拿到的东西是文件内容的字符串,输出的东西必须是 JS 代码(一个字符串也算)。
关于 css-loader 做的事情:
webpack 将 css 文件内容读取为字符串交给 css-loader,css-loader 将字符串中的 @import 和 url() 转为 import 和 require(),style-loader 增加代码功能:创建 style 标签,将 style 标签和样式插入 head 标签中。这些转换后的内容再交给 webpack 处理,webpack 会处理其中的 import 和 require()。
开发环境配置
对JS的处理
webpack 能直接处理 js文件的互相引用,将多个js文件打包到一起。
对样式的处理
在js文件中引入css文件。然后css文件需要css-loader、style-loader去处理。
css-loader
将 @import 和 url() 转为 import 和 require()。
此类语法可以被 file-loader 处理为
url(/public-path/0dcbbaa701328a3c262cfd45869e351f.png)
或者被 url-loader 创建行内图片如
url(data:image/jpeg;base64,LzlqLzRBQ ... zdF3)
style-loader
在 <head> 中创建 <style> 标签,将css样式字符串放入。
对html文件的处理
使用 html-webpack-plugin,将 ./src 中的 index.html 复制到 ./build 文件中,并自动引入 built.js。
对图片的处理
file-loader
解析一个文件中的 import / require() 为 URL,并将文件复制到输出文件夹 ./build 下。
url-loader
功能类似 file-loader,增加功能:将文件转为 base64。
html-loader
将html文件中的内容作为字符串暴露出去。
将引用外部资源的标签中的 src、herf 等转为 import 或 require()。
例如:<img src="image.png">。
webpack-html-plugin 的 template 配置项使用了哪种文件,会先让对应的 loader 处理该文件。所以 index.html 不在依赖体系中,也会被对应 loader 处理。
对其它文件资源的处理
其它文件资源,例如字体文件。
使用 file-loader 即可,把 css、js、html、less 等排除,避免重复处理。
devServer
开启 devServer 后,打包后的内容没必要写入文件,直接发送给前端即可。
两大功能:监听文件变化,自动刷新。
文件监听
"文件监听功能是 Webpack 提供的。"
“在 Webpack 中监听一个文件发生变化的原理,是定时获取这个文件的最后编辑时间,每次都存下最新的最后编辑时间,如果发现当前获取的和最后一次保存的最后编辑时间不一致,就认为该文件发生了变化。配置项中的watchOptions.poll用于控制定时检查的周期,具体含义是每秒检查多少次。”
“在默认情况下,Webpack 只监听 Entry 文件所依赖的文件。而不是直接监听项目目录下的所有文件。”
自动刷新
“webpack-dev-server模块负责刷新浏览器。"
"在使用webpack-dev-server模块去启动webpack模块时,webpack模块的监听模式默认会被开启。webpack模块会在文件发生变化时通知webpack-dev-server模块。”
"自动刷新的原理:向要开发的网页中注入代理客户端代码,通过代理客户端去刷新整个页面。"
“通过DevServer启动构建后,代理客户端的代码被打包到了要开发的网页代码中。”
“在浏览器中打开网址 http://localhost:8080/ 后,代理客户端会向DevServer发起WebSocket连接。”
模块热替换
devServer 把更新的代码发送到前端,代理客户端通过某种方式更新了对应模块的代码。
没有发生重新打包,也没有将页面整个刷新。
生产环境配置
提取CSS成单独文件
使用 mini-css-extract-plugin,它是和 html-webpack-plugin 配合工作的。
(开发时使用 style-loader,打包速度更快。上线时将 CSS 提取成单独文件,使样式更快起效,用户体验更好。)
CSS的兼容性处理
webpack 的 mode 配置项:
告知 webpack 使用相应模式的内置优化。会启用一些内部插件。
使用 postcss-loader,它可以给 CSS 做很多处理。
postcss-preset-env 是一个 postcss 插件,用于将较新的 CSS 语法转化为大部分浏览器可以理解的形式。(已经包含了给样式加前缀的功能)
browserslist 用来声明支持哪些浏览器,可以写在 .browserslistrc 文件中,或者写在 package.json 中。(.browserslistrc 文件内容是和其它工具共享的)
写在package.json中的配置需要注意:这里默认运行的是 production 环境参数,如果希望以开发模式运行,就需要修改环境变量来改变其运行的环境,通过node的环境变量参数进行修改。修改内容放在webpack.config.js文件中: process.env.NODE_ENV = 'development'。
压缩CSS
webpack v5 之前使用 optimize-css-assets-webpack-plugin,webpack v5 用 css-minimizer-webpack-plugin。
它会在 webpack 构建期间搜寻 CSS 资源,并将其压缩。
js语法检查
使用 eslint-loader(已废弃)。
配置信息可以写在 package.json 中,或者 .eslintrc 文件中。
可以使用 airbnb 的规范,安装 eslint-config-airbnb-base(依赖另外两个包),配置信息写入 "extends": "airbnb-base"。
js兼容性处理
使用 babel-loader。babel 可以将新的 JS 语法转为旧的。
babel-loader 需要使用 @babel/preset-env 这个预设,现在可以做一些基本的兼容性处理,如 let。
如果需要更多的兼容性处理,如 Promise,则需要对 @babel/preset-env 写更多的配置,还要依赖于 core-js,它对新语法的支持是按需加载的,还可以指定兼容性做到哪个版本浏览器。
配置写法:
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// 预设:指示babel做怎么样的兼容性处理
presets: [
[
'@babel/preset-env',
{
// 按需加载
useBuiltIns: 'usage',
// 指定core-js版本
corejs: {
version: 3
},
// 指定兼容性做到哪个版本浏览器
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
]
}
}
推荐使用 .browserslistrc 指定。
@babel/preset-env 会按 browserslist的顺序 查找,除非设置了 targets 配置项。
压缩JS和HTML
mode: 'production' 时会启用 UglifyJsPlugin,对 JS 做压缩。
给 html-webpack-plugin 配置 minify 项,可以对 HTML 文件做压缩。
生产环境配置总结
rules 中有两条以上规则处理同一类文件时,可以用 enforce: 'pre' 控制先后顺序。(这里的多个 loader 目的不同,所以没用 use 连接放在一起)
关于 plugin
webpack 会提供很多 hooks,类似于生命周期函数,还会给回调函数提供对应时刻编译打包的各种详细信息。
所以插件可以在任意阶段做想做的任何事情,插件比 loader 要强大的多。webpack 自身的大部分代码也是由插件实现的。
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
开发插件时,webpack 会提供两个对象:Compiler 和 Compilation。
Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息。
Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
更多:5-4 编写 Plugin · 深入浅出 Webpack
优化
开发环境性能优化
- 优化打包构建速度
- HMR
- 优化代码调试
- source-map
生产环境性能优化
- 优化打包构建速度
- oneOf
- babel缓存
- 多进程打包
- externals
- dll
- 优化代码运行的性能
- 缓存(hash-chunkhash-contenthash)
- tree shaking
- code split
- 懒加载/预加载
HMR
devServer 中配置 hot: true。( 新版 devServer 已经默认开启此功能 )
样式文件:可以使用HMR功能,因为style-loader内部实现了。
html文件: 不用做HMR功能,因为没有多个模块,也没有打包。(hot: true 之后对 HTML 文件的修改页面不能自动刷新,需要修改 entry 入口,将 HTML 文件引入)
js文件:需要手动添加一点代码。在 index.js 中增加:
if (module.hot) {
module.hot.accept();
}
source-map
提供源代码到构建后代码映射,如果构建后代码出错了,通过映射可以追踪源代码错误。devtool: 'source-map'。
oneOf
对 rules 中的规则不用遍历全部,找到第一个匹配的即可。
babel 缓存
给 babel-loader 设置 cacheDirectory: true,之后每次只编译有更改的文件。
文件资源缓存更新
设置客户端缓存后,服务端资源更新时,为了客户端能及时获取新资源。可以设置 contenthash,如 filename: 'js/built.[contenthash:10].js'。
只要保证 index.html 文件没有走缓存,当它引用的其它文件名字变化时,就可以及时获取新资源。
tree shaking
此功能可以删除未引用的代码,即 export 出去但没有被 import 的代码。
mode: 'production'时会自动开启。只对 ES6 模块化的代码起作用。(webpack5 开始支持 commonjs,但有一些要求)
有些代码是有用的,虽然他们没有 export 任何东西,例如 polyfill 和 CSS 文件。可以在 package.json 中设置 "sideEffects": ["./src/a.js", "*.css"] 避免这些有用的代码被删除。
code split
作用:
- 为了更好的缓存。node_modules 中的代码不常变化,所以单独将其打包,业务代码更新时,node_modules 的打包缓存依然可用。
- 避免单个包太大,用户等待时间太长。保证先下载到必要的 js 文件。
- 分包后也可以做按需加载或预加载。
通过配置 splitChunks 将 node_modules 中代码单独打包一个 chunk 最终输出。写法如下:
optimization: {
splitChunks: {
chunks: 'all',
// chunks(chunk) {
// return chunk.name !== 'my-excluded-chunk';
// },
}
}
chunks 可以是一个函数,从而精确控制哪些包需要单独打包。
如果希望某个文件被单独打包成一个chunk,可以通过js代码实现,import 动态导入语法:
import(/* webpackChunkName: 'test' */'./test')
.then(({ mul, count }) => {
console.log(mul(2, 5));
})
.catch(() => {
console.log('文件加载失败~');
});
懒加载(按需加载) & 预加载
当文件需要使用时才加载。利用动态导入实现。
document.getElementById('btn').onclick = function() {
import(/* webpackChunkName: 'test' */'./test')
.then(({ mul }) => {
console.log(mul(4, 5));
});
};
预加载:会在使用之前,提前加载 js 文件。(等其他资源加载完毕,浏览器空闲了,再偷偷加载资源)
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test')
多进程打包
使用 thread-loader。
externals
作用:排除掉一些包,使他们不参与打包。
应用场景:有些包可能通过 cdn 引入,所以不需要打包。
externals: {
jquery: 'jQuery'
}
dll
对一些不常变化的第三方库(如 jquery、react、vue)单独打包,并在 HTML 文件中自动引入。需要增加一个打包配置文件,单独运行打包。配置相对麻烦一些。
将第三方库再次分包,避免单个包太大 (也是 code split)。同时每次打包时可以不必打包这些第三方库,提升打包速度。
配置详解
entry
1.string --> './src/index.js'
单入口
打包形成一个chunk。 输出一个bundle文件。
此时chunk的名称默认是 main
2.array --> ['./src/index.js', './src/add.js']
多入口
所有入口文件最终只会形成一个chunk, 输出出去只有一个bundle文件。
--> 只用在HMR功能中让html热更新生效
3.object
多入口
有几个入口文件就形成几个chunk,输出几个bundle文件
此时chunk的名称是 key
entry: {
index: ['./src/index.js', './src/count.js'],
add: './src/add.js'
}
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
// libraryTarget: 'global' // 变量名添加到哪个上 node
// libraryTarget: 'commonjs'
}
关于非入口的 chunk 请看:code split
当配置多个入口时,最终会生成多个js文件。需要将 output 中的 filename 配置为 filename: 'js/[name].[contenthash:10].js'。
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: {}
},
{
// 以下配置只会生效一个
oneOf: []
}
]
},
resolve
// 解析模块的规则
resolve: {
// 配置解析模块路径别名: 优点简写路径 缺点路径没有提示
alias: {
$css: resolve(__dirname, 'src/css')
},
// 配置省略文件路径的后缀名
extensions: ['.js', '.json', '.jsx', '.css'],
// 告诉 webpack 解析模块是去找哪个目录
modules: [resolve(__dirname, '../../node_modules'), 'node_modules']
}
devServer
contentbase:代表html页面所在的相对目录,如果我们不配置项,devServer默认html所在的目录就是项目的根目录。
devServer: {
// 运行代码的目录
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': ''
}
}
}
}
optimization
optimization: {
splitChunks: {
chunks: 'all'
// 默认值,可以不写~
/* minSize: 30 * 1024, // 分割的chunk最小为30kb
maxSiza: 0, // 最大没有限制
minChunks: 1, // 要提取的chunk最少被引用1次
maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量
maxInitialRequests: 3, // 入口js文件最大并行请求数量
automaticNameDelimiter: '~', // 名称连接符
name: true, // 可以使用命名规则
cacheGroups: {
// 分割chunk的组
// node_modules文件会被打包到 vendors 组的chunk中。--> vendors~xxx.js
// 满足上面的公共规则,如:大小超过30kb,至少被引用一次。
vendors: {
test: /[\\/]node_modules[\\/]/,
// 优先级
priority: -10
},
default: {
// 要提取的chunk最少被引用2次
minChunks: 2,
// 优先级
priority: -20,
// 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包模块
reuseExistingChunk: true
}
}*/
},
// 将当前模块的记录其他模块的hash单独打包为一个文件 runtime
// 解决:修改a文件导致b文件的contenthash变化
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
},
minimizer: [
// 配置生产环境的压缩方案:js和css
new TerserWebpackPlugin({
// 开启缓存
cache: true,
// 开启多进程打包
parallel: true,
// 启动source-map
sourceMap: true
})
]
}
runtimeChunk:当在 a.js 中按需加载 b.js 时,b.js 的修改导致 contenthash 变化,所以 a.js 的内容也发生变化,从而 a.js 的 contenthash 发生变化,导致 a.js 缓存失效,这是不必要的。通过 runtimeChunk 可以将 a.js 中的 b.js 的 contenthash 放到另一个文件中,问题解决。