webpack5

文章目录

webpack5

重新整理一下 webpack 知识,内容过多,了解功能即可,遇到 api 查文档。

mode

webpack 4.x 引入了 mode 的概念。webpack mode 默认为 production, webpack serve 默认为 development。命令行中的 mode 比 webpack.config.js 的 mode 优先级更高。

  • 生产模式默认会启用各种性能优化的功能,开启代码压缩,运行时不打印 debug 信息,静态文件不包括 sourcemap,包括构建结果优化以及 webpack 运行性能优化。

    会将 process.env.NODE_ENV 设为 development,启用 NamedChunksPlugin 和 NamedModulesPlugin

    环境功能:需要生成 sourcemap,需要打印 debug,需要 live reload 或 hot reload 功能

  • 开发模式会开启 debug 工具,需要热加载,不进行代码压缩,包含 sourcemap 文件,运行时打印详细的错误信息,以及更加快速的增量编译构建

    会将 process.env.NODE_ENV 设为 production,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin, UglifyJsPlugin

    环境功能:可能需要分离 CSS 成单独的文件,以便多个页面共享同一个 CSS 文件,需要压缩 HTML/CSS/JS 代码,需要压缩图片

区分环境

  • --mode=production 用来设置模块内的 process.env.NODE_ENV,即可以在 ./src/index.js 通过 process.env.NODE_ENV 得到 production 这个值,但不能在 webpack.config.js 中得到这个值(得到 undefined)

  • --env=production 用来设置 webpack 配置文件的函数参数,webpack.config.js 的导出可以是一个函数,会传入 env 这个参数,此时可以通过 env 得到 production 这个值。但 process.env.NODE_ENV 仍为 undefined

  • cross-env 用来设置 node 环境的 process.env.NODE_ENV,可以在 webpack.config.js 中得到 production 的值了。cross-env NODE_ENV=production 实际上是对 window,linux,mac 里设置了一个环境变量为 product,需要 npm i -D 到项目中,是真正的环境变量,在 window 中相当于 set NODE_ENV=production,mac、linux 中相当于 export NODE_ENV=production 。只能在 webpack.config.js 都能拿到 process.env.NODE_ENV

  • webpack.DefinePlugin 是一个 plugin, 设置后可以在 webpack.config.js 和 ./src/index.js 都拿到 process.env.NODE_ENV,是一个字符串替换的过程。

    用法:在 plugins 数组中 new webpack.DefinePlugin({ “process.env.NODE_ENV”: JSON.stringify(process.env.NODE_ENV) /* 相当于变成 '"product"' 了 */ })

一般 cross-env 和 webpack.DefinePlugin 一起使用是比较完美的方案。

externals

配置外部模块,key 模块名,value 全局变量名。

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js
module.exports = {
externals: {
'jquery': 'jQuery',
'loadsh': '_'
}
}

// src/index.js
// 配置了 externals 之后,下面的代码相当于先 let jQuery = window.jQuery,这样不管 html 里有没有引入 jq 的 CDN,代码都能执行了,如果没用 CDN,就打包 node_modules 里的 jq,有的话就引用全局的 jq
let jQuery = require('jquery')
console.log(jQuery)

watch

1
2
3
4
5
6
7
8
module.exports = {
watch: true, // 开启监听文件变化
watchOptions: {
ignored: /node_modules/, // 忽略文件
aggregateTimeout: 300, // 防抖 300ms
poll: 1000 // 原理是轮询,每秒检测 1000 次文件是否有变化,有变化重新打包
}
}

loader

webpack 只识别 js 、 json 和 wasm,其他后缀文件需要用 loader 解析。所有的 loader 都需要通过 npm 安装后才能使用。

loader 有四种分类,pre、post、normal、inline,通过 enforce 属性指定这个值,默认为 normal,表示 loader 的执行顺序,pre 是先执行,post 是后执行,normal 在中间。 pre -> normal -> post

1
2
3
4
5
6
let rules = [
{ test: /\.js$/, use: 'loader1' },
{ test: /\.js$/, use: 'loader2', enforce: 'post' },
{ test: /\.js$/, use: 'loader3', enforce: 'pre' }
]
// loader3 -> loader1 -> loader2
  • css-loader 用来翻译处理 @import 和 url()
  • style-loader 可以把 css 转成 js,之后作为 <style> 标签插入到 dom 中
  • postcss-loader 处理 css 兼容性的 loader,可以加上各种厂商前缀
  • sass-loader、less-loader、stylus-loader 可以把对应格式的文件转为 css,需要 npm i 对应的 css 预处理器
  • file-loader 可以把 src 目录里依赖的源图片文件拷贝到目标目录里去,文件名一般为新的 hash 值

loader 与 plugin:loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量

全局暴露 loader

expose-loader 可以把一个变量放在全局对象。注意,如果想要在生成的 html 文件中的 script 标签里使用这个变量,需要加上 defer,因为包裹 webpack 打包出来的 js 文件的 script 标签有 defer 这个属性,会导致还未执行(未注入变量)打包后 js 时已经走了你自己的 script,从而找不到这个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.config.js
module.exports = {
module: {
rules: [
test: require.resolve('lodash'), // 也可以写成正则 test: /lodash/
loader: 'expose-loader',
options: {
exposes: {
globalName: '_', // 全局变量名
override: true // 如果有值,就覆盖
}
}
]
}
}

// 以下为文件中行内 loader 的写法,开头表示 loader,? 后表示参数, ! 后表示匹配的内容,写一次就添加到全局了
// src/index.js
let _ = require('expose-loader?expose=_&override=true!lodash')
import $ from 'expose-loader?exposes=$,jQuery!jquery
import { concat } from 'expose-loader?exposes=_.concat!lodash/concat'

babel

  • babel-loader 使用 babel 和 webpack 转译 js 文件,只是一个转换函数,不知道如何转换
  • @babel/core 这是 babel 编译的核心包,可以识别高级的 js 代码,但不知道该转换成什么
  • @babel/preset-env 是 babel 的兼容性预设,包含 es6 -> es5 的所有 plugins,不包含以下 plugin,决定将高级 js 语法转换成什么语法
  • @babel/preset-react 是包含 React 相关 plugin 的 Babel 预设
  • @babel/plugin-proposal-decorators 把类和对象装饰器转成 ES5,如 @connect class O { @readonly PI = 3.14 }
  • @babel/plugin-proposal-class-properties 转换静态类属性以及使用属性初始化语法声明的类型 class P { static a = 1; b = 2 }

babel 的 plugin 决定将高级 js 语法转换成什么语法,每个插件对应一个语法,preset-env 预设就是多个插件的集合。比如 plugin-transform-arrow-functions 就是箭头函数转成普通函数

eslint

eslint-loader 可以进行代码的检测,这个 loader 为 enforce: 'pre',webpack 中设置 loader 的 options: { fix: true } 可以开启遇到问题自动修复。

rules 中 的类型

  • off 表示不检查这项
  • error 表示发现就报错

在目录下新建 .eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
// root: true, // 是否为根配置文件, 因为 eslint 配置文件可以继承,所以需要判断
extends: 'airbnb', // 表示继承自 airbnb 提供的 eslint 规则,这个属性和 root 不共存
parser: 'babel-eslint', // 进行代码检查需要先把源代码转为抽象语法树
parserOptions: {
sourceType: 'module', // 源代码类型是 module
ecmaVersion: 2015
},
env: {
browser: true, // 如果没有这行,写一个 window.a 就会报错,因为 node 环境没有 window
node: true
},
rules: { // 这里写的 rules 如果和 extends 中的规则冲突,以这里为主
indent: ['error', 2], // 缩进风格,表示使用 2 个缩进,不是就报错
quotes: 'off', // 引号的类型
'no-console': 'error' // 禁止打印, error 表示发现就报错
}
}

由于 eslint 的 fix 只发生在 npm run build 的时候,比较不及时,如果希望每次保存都触发 eslint,可以在项目根目录下新建 .vscode 文件夹(记得放入 .gitignore),再新建 settings.json 配置文件,写入配置,再安装 eslint 插件即可。

这个 .vscode 文件夹及里面的 settings.json 也可以通过在 VSCode 中通过 Edit/Preferences/Settings 中修改 Workspace Tab 的配置文件后自动生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
// /.vscode/settings.json
{
"eslint.validate": [ // 检查的类型
"javascript",
"javascriptreact", // JSX
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": { // 保存的时候触发动作
"source.fixAll.eslint": true, // 根据 eslint 配置文件自动修复

}
}

npm i eslint eslint-loader babel-eslint -D

LF: LF 就是 linefolder(换行)

CRLF: CR 就是 carriage return(回车)

window 里换行符是 \r\n, \r 是回车,\n 是换行,这样写的来源是老式打印机,里面有个车存放墨盒,回车表示打印墨盒回到本行最前面,换行表示去下一行的同一个位置,现在其实已经不需要了,但习惯被保留下来。

unix 里换行符是 \n,没有 \r

mac 里换行符是 \r

VSCODE 右下角可以修改你的回车 CRLF 还是 LF。

Sourcemap

在 webpack 的 devtool 可以配置以下类型,控制 sourcemap 的生成,设置 false 不生成。

生成的源码里会通过在最后一行加上 //# sourceMappingURL=main.js.map 来关联 map

类型 含义
source-map 生成完整的 sourcemap,并且建立关联,有行列信息
eval-source-map 为每一个模块生成一个单独的 sourcemap 文件进行内联,并使用 eval 执行,由于每个模块的 sourcemap 分开了,所以有对 sourcemap 的缓存功能
cheap-module-eval-source-map 原始代码(只有行内),没有列信息,但有 loader 信息
cheap-eval-source-map 用 eval 包裹代码,只有行信息,没有 loader 信息
eval 用 eval 包裹代码,既有行也有列,还有 loader
cheap-source-map 外部生成 sourcemap 文件,不包含列和 loader 的 map
cheap-module-source-map 外部生成 sourcemap 文件,不包含列的 map(但包含 loader)

关键字

实际上这些类型都是六个关键字的组合:

  • eval 使用 eval 包裹代码
  • source-map 产生 .map 文件,包含原始代码信息(即包含 loader 的 sourcemap),包含行列信息
  • cheap 不包含列信息,也不包含 loader 的 sourcemap
  • module 包含 loader 的 sourcemap
  • inline sourcemap 以 base64 内嵌到打包的源码中,不单独生成 .map 文件
  • hidden 在目标源码中不建立关联 map

关键字可以任意组合,但是有顺序要求

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

开发环境

如果想要速度快, eval-cheap-source-map,想要调试更友好,cheap-module-source-map。

一般推荐用 eval-source-map

生产环境

先排除内联,因为我们要隐藏源代码和减少文件体积。

想调试友好选择 source-map>cheap-source-map/cheap-module-source-map>hidden-source-map/nosources-source-map

想速度快, cheap

这种就是 hidden-source-map(生成 map,但不在源码中关联 .map),devtool 的值为 hidden-source-map,引入 filemanager-webpack-plugin,拷贝 dist 下的 .map 文件至另一个文件夹后,删除 dist 下的 .map 文件。

需要调试时,添加指定路径的 .map 即可。

测试环境

把 devtool 设置为 false,引入 filemanager-webpack-plugin,我们需要更精细地控制 devtool

假设测试环境是在 dist 目录下开启了 http-server,那么它的 .map 会指向本机的 8081 (假设在 /maps 开启了一个 http-server -p 8081)目录下的 map。

需要把谷歌的 Enable Javascript source maps 打开。

如果这时候有 debugger,可以看到 webpack:// ,点过去就是源码了。

1
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
const webpack = require('webpack')
const FilemanagerWebpackPlugin = require('filemanager-webpack-plugin')
module.exports = {
devtool: false, // 关闭 devtool,自己通过 webpack 插件更精细的控制 devtool
plugins: [
// 生成 .map
new webpack.SourceMapDevToolPlugin({
// 在源码底部添加下面的代码,url就是相对 dist 的路径
append: `\n//# sourceMappingURL=http://127.0.0.1:8081/[url]`,
filename: `[file].map` // 生成的 js 文件叫 main.js,那么 map 就叫 main.js.map
}),
// 将打包的 .map 移动至 dist 同级的 maps 文件夹,删除 dist 下的 .map,这样就可以不把 .map 部署到测试环境了
new FilemanagerWebpackPlugin({
events: {
onEnd: {
copy: [
{
source: './dist/**/*.map',
destination: path.resolve(__dirname, 'maps') // 目标文件夹
}
],
delete: ['./dist/*.map'] // 不知道为什么写 ./dist/**/*.map 不生效
}
}
})
]
}

Plugin

1
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
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const FilemanagerWebpackPlugin = require('filemanager-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')


module.exports = {
plugins: [
/*
自动向所有模块中注入第三方模块,即 import _ from 'lodash',但是在 .html 的 script 中拿不到,因为不是全局注入,所以我们可以写成这样了
// import _ from 'lodash'
console.log(_)
*/
new webpack.ProvidePlugin({
_: 'lodash'
}),
// 使用一个 html 模板,在打包后会自动引入 js、css 文件
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 清除输出目录(默认 dist)下的 **/*
new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['**/*'] }),
// 主要功能是可以把未被文件依赖的静态文件也打包到 dist 中,这里是把根目录下的 public 文件夹内容移动至 dist/public 内容下
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'public'),
to: path.resolve(__dirname, 'dist/public')
}
]
}),
new MiniCssExtractPlugin({
// 把所有的 css 打包到一个 main.css 中,如果写成 filename: '[name].css' 则会打包成各自单独的 css 文件,如果写成 filename: 'css/main.css', 则会在 dist 下新建 css 目录,然后把这个 css 放进去
filename: 'main.css'
}),
// 以下两个 loader 介绍见上方 sourcemap 内容
new webpack.SourceMapDevToolPlugin({
append: `\n//# sourceMappingURL=http://127.0.0.1:8081/[url]`,
filename: `[file].map`
}),
new FilemanagerWebpackPlugin({
events: {
onEnd: {
copy: [
{
source: './dist/**/*.map',
destination: path.resolve(__dirname, 'maps')
}
],
delete: ['./dist/*.map']
}
}
})
]
}

devServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module.exports = {
devServer: {
port: 8080, // http 端口号,默认 8080
open: true, // 编译成功后自动打开浏览器
compress: true, // 是否压缩
static: path.resolve(__dirname, 'public'), // 额外的静态文件的根目录
proxy: {
'/api': {
target: 'http://xxx.com',
/*
pathRewrite:重写路径
如果没有 ^/api: '', /api 的请求会代理到 http://xxx.com/api,否则会被代理到 http://xxx.com
*/
pathRewrite: {
'^/api': ''
}
}
},
onBeforeSetupMiddleware(app) { // 相当于开启了一个 express 服务器,webpack5 以前属性名以前叫做 before, mock 就是在这里做的
app.get('/api/users', (req, res) => {
res.json([{ id: 1 }, { id: 2 }])
})
}
}
}

也可以在 express 中加入 webpackDevMiddleware 实现跨域,适用于已经有一个 express 项目在跑了,不想开 webpack,就可以集成,达到跨域效果

1
2
3
4
5
6
7
8
9
10
11
12
let express = require('express')
let webpack = require('webpack')
let webpackDevMiddleware = require('webpack-dev-middleware')
let webpackConfigs = require('./webpack.config.js')
let app = express()
let compiler = webpack(webpackConfigs)

app.use(webpackDevMiddleware(compiler, {}))
app.get('/api/users', (req, res) => {
res.json([{ id: 1 }, { id: 2 }])
})
app.listen(3000)

示例配置

webpack.config.js

webpack4 及 webpack5 的前期版本适用。

css-loader 最高到 5.2,如果再高,会和 file-loader 及 url-loader 冲突。

webpack4

1
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
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')

/*
module.exports = (env) => {
console.log(env) // 得到 srcipt 中设置的 --env=xxx 的 xxx 字符串
return {
entry: './src/index/js',
...
}
}
*/

module.exports = {
// mode: 'production', // production | development 默认生产,通过 cross-env 控制可以注释掉
devtool: false,
entry: './src/index.js', // 默认入口
output: { // 打包文件输出到哪
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
publicPath: '/' // 加载插入产出文件时候的路径前缀,比如 css、jpg 等文件的路径前缀
},
devServer: { // 通过 webpack serve 启用
port: 8080,
open: true, // 是否自动打开浏览器
compress: true, // 是否启动压缩
static: path.resolve(__dirname, 'static') // 额外的静态文件目录,不参与打包,这样配置完之后可以通过 localhost:8080/img.png 访问到项目下 /static/img.png 这个目录,之前 webpack4 这个参数名为 contentBase
},
module: {
rules: [
{
test: /\.js$/,
loader: 'eslint-loader',
enforce: 'pre',
options: {
fix: true // npm run build 时,如果发现有问题的格式,会尝试自动修复,如果成功就继续执行下面的步骤,不成功就报错
},
exclude: /node_modules/ // 排除这个目录
},
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
option: {
presets: [
[
'@babel/preset-env',
{
targets: '> 0.25%, not dead'
}
],
'@babel/preset-react'
],
/*
legacy: true 作用:值为 true,装饰器写法改为提案 1,值为 false,改为提案 2
提案 1:
@readonly
class P {}
提案 2:
class @readonly P {}

loose: true 作用:主要区别是类的赋值语句转 es5 的时候,值为 true,类的赋值语句转 es5 的时候为直接赋值,值为 false,通过 Object.defineProperty 赋值
class Circle() {
PI = 3.14
}

true:
function Circle() {
this.PI = 3.14
}
false:
function Circle() {
Object.defineProperty(this, 'PI', 3.14)
}

*/
plugins: [ // 这两个 plugin 顺序一定是这样的,否则会冲突,见官网 babeljs.io
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }]
]
}
}
},
// css 和 stylus 中的 style-loader 可以用 MiniCssExtractPlugin.loader 代替,这样可以拆分出单独的 css 文件
{
test: /\.css$/,
// use: ['style-loader', 'css-loader']
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true // 开启 cssModule,就是可以 import styles from './index.css' 了,而不是 import './index.css'
}
}
]
},
{
test: /\.styl(us)?$/, // 当然,如果你随便写个 .abc 格式的文件,但里面的语法是 stylus,也可以直接 test: /\.abc$/,也能成功使用 loader
use: ['style-loader', 'css-loader', 'stylus-loader']
},
{
test: /\.(jpg|png|gif|bmp|svg)$/,
use: [
{
// loader: 'file-loader'
loader: 'url-loader',
options: {
esModule: false, // 默认为 true, 为 true 时, const img = require('./img.png').default,需要 .default 才能得到 base64 的值,设置为 false 就可以直接拿到了
name: '[hash:8].[ext]',
limit: 8 * 1024, // 文件小于这个值,转为 base64 内嵌,可以少发一次请求
outputPath: 'images', // 指定输出的目录
publicPath: '/images' // 指定导出文件自动引入时的目录,如果没用的话应该就会读 output 下的 publicPath
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html' // 模板 html 文件
}),
new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['**/*'] }) // 在 build 前清空 output 的目录, ** 表示任意层级的路径,比如 /img/a/b 或者 /img, * 表示任意文件
]
}

webpack5

webpack5 中移除了 file-loader、url-loader、raw-loader(处理 Buffer 的,比如打包了个 pdf),并且官方不再使用。新加入的是 type 字段,见这里

  • file-loader -> asset/resource
  • url-loader -> asset/inline
  • raw-loader -> asset/source
  • url-loader + limit(转 base64 的) -> asset

如果要使用原来的 loader 的话,需要加上 type: ‘javascript/auto’,例子

1
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
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
}
},
],
+ type: 'javascript/auto'
},
]
},
}

// 新版示例
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
+ type: 'asset',
+ parser: {
+ dataUrlCondition: {
+ maxSize: 4 * 1024 // 4kb
+ }
+ }
},
]
},
}

postcss.config.js

1
2
3
4
5
6
7
8
9
10
// postcss.config.js
const postcssPresetEnv = require('postcss-preset-env')
module.exports = {
plugins: [
postcssPresetEnv({
// browsers 参数仅在没有有效的 browserslist 配置参数情况下才有效,这个配置参数可以是 .browserslistrc、package.json 里的 browserslist、环境变量里的 browserslist
browsers: 'last 5 versions' // 表示 css 兼容到最近的五个版本,如果最近五个版本都支持的,就不添加厂商前缀
})
]
}

package.json

1
2
3
4
5
6
7
8
9
10
11
12
{
"browserslist": { // 用于浏览器兼容,一般写在 package.json 中,这个值会被 @babel/preset-env 和 postcss-preset-env 用来确定需要转译的 JavaScript 特性和需要添加的 CSS 浏览器前缀
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
">0.2%"
]
}
}

loader-runner

pitch

先 pitch 再 normal

tapable

分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理