Webpack5
# webpack 基础
# webpack 概念
webpack 是一个打包工具,是一个围绕着项目做工程化的框架。gulp 是一个批量生产脚本工具,针对文件进行某种处理。这两种工具看似相似,但是区别很明显。
举个例子:
现在有一个项目,是 Java 项目,使用后端模版引擎(例如 Velocity )来渲染页面。
这个项目不是前端常见的 Node 项目,当需要使用 压缩 / 兼容 / 上传OSS并替换文件内容 等常见的生产操作时,我们的思路是:读取 Java 打包生成的 target 文件(类似 node 打包中一般会使用 dist 一样),操作 js / css 文件,把文件放到正确的位置,即只针对文件进行某种操作。
这种情况就很适合使用 gulp 。
而 webpack 适合于所有的前端项目:有 node 环境,需要压缩 babel 等常见操作来优化项目代码。
实际上是有对应包的,我在github上查到,可以尝试使用: gulp下有 gulp-velocity (opens new window)
webpack中 velocity-render-loader (opens new window)
# webpack 基础配置
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
const json5 = require('json5')
module.exports = {
// entry: './src/index.js', // 单入口文件
entry: { // 多入口文件
// 修改为多个入口文件时,会报异常:相同的bundle文件名 => 需要修改 output.filename 的规则
// Error: Conflict: Multiple chunks emit assets to the same filename bundle.js (chunks index and another)
// index: './src/index.js',
// another: './src/another-module.js'
// 会产生一个问题:引用的第三方库会重复引用进所有的js中
// 方法1
// index: {
// import: './src/index.js',
// dependOn: 'shared', // 定义出共享文件
// },
// another: {
// import: './src/another-module.js',
// dependOn: 'shared',
// },
// shared: 'loadsh' // 抽出共享的 loadsh 保存为 shared.chunck
// 方法2 使用插件
// 添加 optimization.splitChunks.chunks 的配置
index: './src/index.js',
another: './src/another-module.js'
// 方法3 在js中动态引入
// 示例代码 ./src/index.js 搜索 webpackChunkName
},
output: {
// filename: 'bundle.js', // 编译输出文件 这里是固定但文件入口的
// filename: '[name].bundle.js', // 编译输出文件 多文件入口需要按规则配置
filename: 'scripts/[name].[contenthash].js', // 编译输出文件 添加hash串 相当于版本号了
path: path.resolve(__dirname, './dist'), // 编译输出目录(必须是绝对路径)
clean: true, // 清理dist目录
assetModuleFilename: '[contenthash][ext]', // 默认所有资源文件的文件规则
publicPath: 'http://localhost:8080/', // 默认公共根目录
},
mode: 'development', // 模式 本地开发
// mode: 'production', // 模式 生产环境
devtool: 'inline-source-map', // 生成map文件,方便在mode=dev时调试文件
plugins: [
new HtmlWebpackPlugin({ // 打包生成html文件
template: './index.html', // 生成html的模版文件
filename: 'app.html', // 生成html的文件名
inject: 'body', // script插入的位置
}),
new MiniCssExtractPlugin({ // 插件需要先实例化再使用, 否则 报错 // You forgot to add 'mini-css-extract-plugin' plugin...
filename: 'styles/[contenthash].css' // 生成html的文件名
}),
],
devServer: { // webpack-dev-server的配置项
// webpack-dev-server 不会真实的生成dist文件夹,而是把代码放到内存中
// 所以只需要把 output.filename、devServer.static 设置一致即可
// 并且如果有文件变动 需要重启服务,否则内存中是没有新增的文件的,就会报错 Module parse failed: Unexpected character
static: './dist' // 服务启动访问的路径
},
module: { // 资源模块
// test 不只会匹配 html 中引用的文件 还会匹配 css 中引用的文件,注意 可以用 文件路径或者名称或者文件大小来区分不同需求下的操作模式
rules: [
{
test: /\.png$/, // 文件名的匹配规则
type: 'asset/resource', // source资源转换类型
// asset/resource 会复制文件到对应目录,并且重命名文件
generator: { // 关联规则对应的生成文件路径, 会覆盖 output.assetModuleFilename 的值
filename: 'images/[contenthash][ext]' // 文件名
}
},
{
test: /\.svg$/,
type: 'asset/inline',
// asset/inline 会读取文件并使用文件的 Basse64 形式调用图片
// 由于没有对应文件夹需求,所以不需要 generator 参数
},
{
test: /\.txt$/,
type: 'asset/source'
// asset/source 会读取文件的内容放到bundle文件中直接引用
},
{
test: /\.jpeg$/,
type: 'asset',
// asset 会自动选择 inline / resource 模式,选择规则:如果资源大于8KB,就使用resource
// 如果代码选择了resource模式 就会自动使用 output.assetModuleFilename 来替代 generator,或者手动设置 generator使用
generator: { // 关联规则对应的生成文件路径, 会覆盖 output.assetModuleFilename 的值
filename: 'images/[contenthash][ext]' // 文件名
},
parser: { // 解析器,用来调整默认的8K规则
dataUrlCondition: { //
maxSize: 4 * 1024 // 4KB // 限制规则
}
}
},
{
test: /\.(css|less)$/,
// use: ['style-loader', 'css-loader', 'less-loader'],
// use中是有顺序的,从后往前执行
// css-loader 负责解析css
// style-loader 负责把代码放到页面中
// style-loader 负责把less转为css
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'], // 为了使用 MiniCssExtractPlugin 插件,生成css文件,就不需要再插入css到html中。所以去掉style-loader
// MiniCssExtractPlugin 合并生成css文件
},
{
test: /\.(eot|ttf|woff|woff2|otf)$/,
type: 'asset/resource', // source资源转换类型
},
{
test: /\.(tsv|csv)$/,
use: 'csv-loader' // 读csv的loader 生成一个 Array
},
{
test: /\.xml$/,
use: 'xml-loader' // 读xml的loader 生成一个 Object
},
{
// 自定义文件格式
test: /\.json5$/,
type: 'json', // 按照 XX 文件类型读取文件
parser: {
parse: json5.parse // 使用解析器
}
},
{
test: /\.js$/,
exclude: /node_modules/, // 需要排除的文件规则
use: {
loader: 'babel-loader', // js兼容
options: {
presets: ['@babel/preset-env'], // 参数 预设
plugins: [
[
'@babel/plugin-transform-runtime', // 浏览器提示 regeneratorRuntime is not defined 错误,需要安装 @babel/plugin-transform-runtime 自动导入包
]
],
}
}
}
]
},
optimization: { // 优化配置
minimizer: [
new CssMinimizerWebpackPlugin(), // 压缩css
new TerserWebpackPlugin(), // 压缩js
],
splitChunks: {
// chunks: 'all', // 自动拆分第三方插件
cacheGroups:{ // 缓存组
vendor:{ // 第三方库
test: /[\\/]node_modules[\\/]/, // 文件路径匹配规则
name: 'vendors', // 生成的文件名
chunks: 'all', // 对所有的chunck做处理
}
}
}
},
performance: {
hints: false // 关闭性能提示
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
另外,为了支持不同环境下使用不同的需要脚本传递参数,然后使用 node 处理。这部分属于 node 的功能,与webpack关系不大,在任何框架下都可以使用。
// 作为配置项入口
// 用来 在不同环境下 合并正确的 配置项文件
const { merge } = require('webpack-merge') // merge json 因为 webpack option 层级比较复杂,所以使用插件比较省事
// 引入多个配置文件
const commenConfig = require('./webpack.config.common')
const prodConfig = require('./webpack.config.prod')
const devConfig = require('./webpack.config.dev')
module.exports = (env) => {
// 根据 shell 脚本传递参数,然后返回最终的 option
/* shell 脚本 --env XXX 在这里来传递参数
"dev": "npx webpack server --config ./config/webpack.config.js --env development",
"build": "npx webpack --config ./config/webpack.config.js --env production"
*/
switch(true){
case env.development: return merge(commenConfig, devConfig);
case env.production: return merge(commenConfig, prodConfig);
default: new Error('No matching config.')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Github: Test Code (opens new window)
# webpack 进阶配置
下面的配置中有些新概念需要大致了解:
- source-map:是用来在chrome dev tool的source里查看压缩后的js文件实际的代码位置,方便调试压缩后的代码
- tree-shark:在压缩时,把多余的、未调用的代码清理掉
- 单页面/多页面:首先这个与XX框架无关,只是Vue等推荐SPA,但是做seo时更推荐多页面(在项目过大时 也推荐使用)
- require / import:主要是js引入其他js的语法有多种,功能一样但特征不一样
- PWA:离线可使用(pwa使用service worker,清除之后就不能离线访问)
这里的参数不全,用哪个插件就去看文档
const path = require('path');
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const EslintWebpackPlugin = require('eslint-webpack-plugin'); // eslint 插件
const WordboxWebpackPlugin = require("workbox-webpack-plugin") // pwa 插件
/*
webpack 依赖关系工具推荐
webpack-bundle-analyzer
*/
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
mode: 'development',
// 多入口
// entry: ['./src/app2.js', './src/app.js', 'lodash'], // 把所有js打包到一个文件里
entry: { // 根据key生成多个文件
// 简写:直接写路径
app: './src/app.js',
app2: './src/app2.js',
app3: './src/app3.js',
app4: './src/app4.js',
app5: './src/app5.js',
// 也可以添加参数
main: {
import: ['./src/app.js', './src/app2.js'],
dependOn: 'lodash', // 意思是 可能会使用这个外部js,不需要再打包进来
},
main2: {
import: ['./src/app3.js'],
dependOn: 'lodash', // dependOn 后面填写的值 是 entry 的 key,即可以定义 lodash 打包到 aaa 中,这里就填 aaa
filename: 'page/[name].js' // 测试:把html和js放到一起
},
// aaa: 'lodash',
lodash: {
import: 'lodash',
filename: 'common/[name].js'
},
},
resolve: {
alias: { // 给绝对路径 定义 全局的变量以简化代码
'@': path.resolve(__dirname, './src')
},
// extensions: ['.json', '.js', '.vue'], // 文件引用不使用后缀时,顺序读取对应后缀的文件(默认读取 .js 文件)
extensions: ['.ts', '.json', '.js', '.vue'], // 优先解析ts
},
devServer: { // 服务配置
static: path.resolve(__dirname, './dist'), // 静态文件地址
compress: true, // 请求gzip压缩代码
// port: 8080, // 端口号
host: '0.0.0.0', // 局域网公开(局域网内可以用ip访问服务)
headers: { // 自定义请求头
'X-Access-Token': 'asdasdasd',
},
proxy: { // 开启代理
'/api': 'http://localhost:9000', // 把接口 指向 XXX服务器
},
// https: true, // 使用https访问
// https: {}, // 可以配置第三方证书防止浏览器报错
// http2: true, // 自带https证书
historyApiFallback: true, // 给不正确的路由返回index文件(需要注意的是,如果资源是绝对路径,可能异常,改下publicPath)
hot: true, // 热替换 不刷新dom直接更新
// css css-loader 自带 module.hot.accept
// js 需要 module.hot.accept 手动处理下(vue啥的一般自带)
// 插件 HotModuleReplacementPlugin 可以帮助自动设置js的(webpack 5开始 自带了)
liveReload: true, // 热加载
client: {
overlay: true, // 默认开启页面上异常覆盖层
},
// 启动服务时中间件
devMiddleware: {
writeToDisk: true // 启动服务时实时修改dist (不仅放到内存里 还会存到dist里)
}
},
output: {
filename: 'scripts/[name].[contenthash].js', // 编译输出文件 添加hash串 相当于版本号了
path: path.resolve(__dirname, './dist'),
clean: true
},
// source map 配置
// source map 是用来锁定打包前代码的行数的功能代码。文件中包含代码的映射信息,包括 行数、列数。
// devtool: 'eval', // 默认配置,使用eval执行 生成行内 source map
// devtool: false, // 关闭 source map // 生产环境 推荐使用
// devtool: 'source-map', // 生成并引用 source map文件
// devtool: 'hidden-source-map', // 仅生成 不使用 source map文件
// devtool: 'inline-source-map', // 行内 source map 并把代码转换为 dataUrl 形式
// devtool: 'eval-source-map', // 使用eval执行 生成行内 source map 并把代码转换为 dataUrl 形式
// devtool: 'cheap-source-map', // 生成并引用 source map文件 (文件中缩略列信息)
devtool: 'cheap-module-source-map', // 属于增加module支持的 source-map 开发环境推荐使用
externalsType: 'script',
externals: { // 配置外部定义的包
// 手动写法
// 'jquery': 'jQuery', // key 是 js中引入时候的名称,value 是外部引入的js 暴露出来的对象名称 然后在 在html模版中手动添加
// 自动写法-需要 externalsType 配置
'jquery': [ // key 是 js中引入时候的名称
'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js', // 外部引入的js路径(自动添加到模版中)
'$', // 表示上面的js在浏览器中暴露的对象, 与引入时赋值的 $$ 无关
],
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: [
'last 1 version', // 支持到浏览器的最后一个版本
'> 1%', // 支持使用率大于1%的浏览器
],
useBuiltIns: 'usage',
corejs: 3 // 根据 安装的 corejs版本号写
}
]
]
}
},
// use: ['babel-loader', 'eslint-loader'], // eslint-loader 已被弃用 现在改用 eslint-webpack-plugin
},
{
test: /\.css$/,
exclude: /node_modules/,
// use: ['style-loader', 'css-loader'],
use: [
'style-loader', // 不加参数的写法
{ // 加参数的写法
loader: 'css-loader',
options: {
modules: true // css 模块 功能开启
}
},
'postcss-loader', // 自动隔离css样式 防止冲突,使用的时候需要import
]
},
{
test: require.resolve('./src/app3.js'),
// 两种写法都可
use: 'imports-loader?wrapper=window', // 用来给模块中 this 赋值
// use: [ // 还有更复杂的配置 用到的时候看文档
// {
// loader: 'imports-loader',
// options: {
// wrapper: 'window'
// }
// }
// ],
},
{
test: require.resolve('./src/utils.js'), // 帮助没有export到代码导出
use: 'exports-loader?type=commonjs&exports=PublicServer,multiple|Constant.size|Constant,multiple|Utils',
},
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
},
],
},
plugins: [
// 单页面
// new HtmlWebpackPlugin({
// template: './index.html',
// }),
// 多页面
// 每个html模版文件都需要实例化一个HtmlWebpackPlugin
// 写多个HtmlWebpackPlugin需要自定义输出文件名 filename 否则报错:ERROR in Conflict: Multiple assets emit different content to the same filename index.html
new HtmlWebpackPlugin({
// 自定义的属性 可以通过 ejs 语法,使用 htmlWebpackPlugin.options 参数调用
title: '多页面应用',
template: './index.html',
inject: 'body',
chunks: ['lodash', 'main'], // 填写 entry 的 key ,表示该模版中需要插入哪些打包的js
filename: '1.html',
// publicPath: 'http://www.a.com/as', // chunks js的路径
}),
new HtmlWebpackPlugin({
title: '2',
template: './list.html',
chunks: ['lodash', 'main2'],
filename: 'page/2.html',
// publicPath: 'http://www.b.com/',
})
new EslintWebpackPlugin(), // eslint 如果eslint不通过,webpack中抛出异常 否则小问题时webpack直接通过编译了
// 拓展: husky 插件 可以在git hock中添加shell脚本,可以时间在git过成中先脚本后commit 等具体行为,可以缩减eslint次数,仅提交前检查
// new BundleAnalyzerPlugin(), // 可以实时预览的依赖关系工具
// shimming预置全局变量
// 也是另一种引入包的方法
new webpack.ProvidePlugin({
_: 'lodash' // 只要代码中有 _ ,就在引入lodash到全局变量中 , 这样会打包进所有使用_变量的js中
})
// PWA 插件
new WordboxWebpackPlugin.GenerateSW({
clientsClaim: true, // 快速启用 service worker
skipWaiting: true, // 跳出等待
})
],
// tree-shaking配置:用于消除无用代码(测试环境需要手动开启 mode production时自动)
// 它依赖于ES2015中的 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。(MDN)
// 对 require 是无法支持的
// 另外 webpack5 默认所有代码是无副作用(可以tree-shaking)需要手动配置 sideEffect(package.json) 不做操作的文件(例如 theme.css 类似文件)
optimization: {
usedExports: true, // 自动删除所有没用到的代码
},
// sideEffect: true | false | []
// 所有代码有副作用(默认)| 都无副作用随便删 | 这些包有副作用
// 例子: "sideEffect": ["*.css"], 表示 所有的css都是有副作用的 不能删
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
代码在这里:github/webpack-test/02~05 (opens new window)。因为方便查看参数功能 所以我拆成多个项目尝试
# webpack 模块联邦
概念:现在有多个项目,其中某些项目可以把自己的某些模块抛出来给其他项目使用,项目之间互相使用暴露出来的组件,构成了某种关系
webpack中实现模块联邦 只需要使用一个插件 ModuleFederationPlugin 即可。
可以了解下 微前端 的概念
/*
这是一个可以调用别的项目也可以被别的项目调用的项目
其中 ModuleFederationPlugin 的
remotes 参数负责调用外部项目暴露的组件
exposes 负责暴露项目中可以被调用的组件
*/
const htmlWebpackPlugin = require("html-webpack-plugin")
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
mode: 'production',
entry: './src/index.js',
plugins: [
new htmlWebpackPlugin(),
// 模块联邦
new ModuleFederationPlugin({
name: 'home', // 应用别名
filename: 'remoteEntry.js', // 生成文件名
remotes: { // 引用的其他组件的名字(远端的路径)
// 这里引用nav 需要填 nav项目中 模块联邦里的 name 还有发布的地址 remoteEntry.js就是那边的 filename
nav: 'nav@http://localhost:8001/remoteEntry.js'
},
exposes: { // 暴露给别的应用使用的组件
'./HomeList': './src/HomeList.js' // 别人使用时 基于key访问, value 表示项目内该组件的实际路径
},
shared: { // 模块里包含的共享组件
}
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
PS:虽然我没尝试过两个项目的模块互相调用,但是大概率会发生内存溢出bug 所以注意就好
Github: Test Code (opens new window)
# webpack 性能优化
发布时的优化推荐:
- 升级版本(包括 webpack 与 node)
- 将loader应用于最少数量的必要模块(loader 里 include 写的越小越好,再叠加 exclude,需要执行的文件越少速度越快)
- 减少非必要的 loader plugin 的使用
- 减少使用 node 读取文件的 api(速度较慢)
- webpack的配置项 resolve.modules resolve.extensions resolve.mainFile resolve.descriptionFiles 会增加文件系统的使用次数
- 如果不使用 symlinks 设置 false
- 如果使用自定义plugin且没有指定上下文 可以设置 resolve.cacheWithContext: false
- 生成更少的代码时编译速度更快
- 选择更小的三方库
- 移出无用代码
- 只编译你正在开发的代码
- 多页面中使用 SplitChunksPlugin 并开启 async 模式
- 持久化缓存(使用 cache 选项)
- 自定义plugin / loader时,注意定能问题
- DllPlugin 可以为更改不频繁的代码生成单独的编译结果,但是会提升复杂度
- worker池 多线程打包(使用 thread-loader 把消耗资源的打包任务分配给一个worker)(不合适的时候会有副作用)
开发时的优化推荐:
- 当使用其他工具的watch来触发webpack时,可能会使webpack的监听模式失效而使用轮询模式,使用 watchOptions.poll 可以增加轮询时间
- 编译文件放在内存不是硬盘里(支持的插件:webpack-dev-loader webpack-hot-middleware webpack-dev-middleware)
- 减少使用 stats.toJson,webpack4默认使用这个存数据 尽量避免获取stats的数据
- devtool的值会有性能差异
- eval 性能最好 但是不转义代码
- cheap-source-map 性能不错 但map质量稍差
- eval-sourcee-map 可以增量编译
- 最佳选择 eval-cheap-module-source-map
- 避免使用生产环境插件(TerserPlugin等压缩/混淆插件)
- 最小化entry chunk(runtimeChunk: true)
- 避免额外的优化步骤(以下设置为false:removeAvailableModules removeEmptyChunks splitChunk)
- 不使用 node 8.9.10 - 9.11.1 (性能回退)
- ts-loader 关闭类型检查(transpileOnly: true)如果有需求建议使用 ForksCheckerWebpackPlugin 可以将检查移至单独进程中
- 不使用source map