Webpack使用整理


前言

webpack 功能

打包:将不同类型资源按模块处理打包

静态:打包最终产出静态资源

模块:webpack 支持不同规范的模块开发

需要 webpack 的场景

当使用模块化开发时,尽管我们可以通过 type=module 的方式来使用 ES6 的模块化语法,但如果存在其他人使用了 CommonJS 模块化语法,就会出现问题。

webpack 工作模式

Webpack 并不会打包未使用的文件,只有在入口文件以及相关引入文件内明确使用后才会进行打包。

基本使用

全局安装

全局安装 webpackwebpack-cli

1
npm i webpack webpack-cli -g

局部安装

全局安装的方式并不能保证不同环境下安装的 webpack 版本是否一致,因此一般建议 局部安装 Webpack。

1
npm i webpack webpack-cli -D

打包运行

1
npx webpack

webpack 默认情况下会将项目根目录下的 src -> index.js 打包至 dist -> main.js

打包配置

webpack 的默认行为往往不能满足我们的需求,因此可以通过两种方式来定制化 webpack 的行为。

命令参数

可以通过给命令添加参数的方式来修改默认行为。例如如下命令指定了入口文件为 ./src/main.js,打包生成的目录为 ./build

1
npx webpack --entry ./src/main.js --output-path ./build

配置文件

在项目根目录下新建文件,取名为 webpack.config.js

配置文件编写格式如下。

1
2
3
4
5
6
7
8
9
10
const { resolve } = require( "path" );

module.exports = {
entry: "./src/main.js",
output: {
publicPath: './build',
filename: "index.js",
path: resolve( __dirname, "build" )
}
}

与命令行不同的是,output -> path 必须为 绝对路径,详细内容在后面讲解配置文件时会描述。

使用下面命令开始打包,此时不需要跟随参数。

1
npx webpack

修改配置文件名

配置文件名是允许修改的,需要对打包指令进行部分修改。这里以 test.webpack.js 为例。

1
npx webpack --config test.webpack.js

基本属性

这部分梳理下 webpack.config.js 的基本配置属性。

占位符

webpack 在多个名称配置的地方均可以配置占位符,这些占位符如下

  • [ext]:扩展名
  • [name]:文件名
  • [hash]:结合文件内容以 md4 算法生成的 128 位哈希值
  • [contentHash]:在这里是少有的与 hash 完全一致的情况
  • [hash:<length>]:指定 hash 的长度

context

基础目录,绝对路径,用于从配置中解析 entry 和 loader。

默认使用 Node.js 进程的当前工作目录,但建议在配置中手动声明,可以使该打包配置独立于当前目录。

1
2
3
module.exports = {
context: __dirname
}

context

基础目录,绝对路径,用于从配置中解析入口点(entry point)和 加载器(loader)。

默认使用 Node.js 进程的当前工作目录。因此即使 webpack 配置文件并未放置在项目根目录,也仍会按照工作目录查找 entry 配置中的相对路径。

entry

配置入口文件,可以为一个字符串、数组、对象或是函数。

当路径指定为相对路径时,此时相对于配置 context 所在的路径。

字符串

此时将会对指定的文件作为入口文件打包,默认输出为 main.js

1
2
3
module.exports = {
entry: "./src/index.js"
}

数组

此时会将数组内的多个文件一起打包,统一默认输出到 main.js

1
2
3
module.exports = {
entry: ["./src/index.js", "./src/global.js"]
}

对象

当为对象时,每个属性对应的文件将会被分别打包,默认输出的文件名即属性名。

1
2
3
4
5
6
module.exports = {
entry: {
index: "./src/index.js",
global: "./src/global.js"
}
}

此时打包目录中将会分别出现 index.jsglobal.js 两个文件。

而每个属性的值同样可以是字符串、数组或对象,当值为字符串或数组时,表现方式与上文同理。而为对象时,则可配置多个属性,如下:

1
2
3
4
5
6
7
8
9
10
module.exports = {
entry: {
index: {
import: "./src/index.js",
filename: "pages/[name].js",
dependOn: "console"
},
console: "./src/console.js"
}
}

各属性释义如下:

  • import:目标入口文件
  • filename:指定输出的文件名称,该配置会覆盖 output -> filename,允许使用占位符。
  • dependOn: 当前入口所依赖的入口,必须先于该入口加载(暂未搞明白)

还有其他属性:chunkLoadingasyncChunkslayer 暂未搞明白,就先不写了。

函数

当值为函数时,将会在每次 make 事件中被调用。

1
2
3
module.exports = {
entry: () => "./src/index.js"
}

函数可以与前三种方式结合使用。

1
2
3
4
5
6
7
8
9
10
module.exports = {
entry: () => ({
index: {
import: "./src/index.js",
filename: "page/index.js",
dependOn: "console"
},
console: "./src/console.js",
})
}

允许接受一个异步函数,可以从远程动态获取相关配置

1
2
3
module.exports = {
entry: () => new Promise( resolve => resolve( "./src/index.js" ) )
}

output

该属性最低要求为一个包含 filename 属性的对象。

path

打包的内容所输出到的路径,必须为绝对路径。

filename

决定每个输出的名称,将写入到 output.path 指定的目录下

1
2
3
4
5
module.exports = {
output: {
filename: "index.js"
}
}

当存在多个入口起点时,就不再应该使用静态名称,而是使用标识符的方式为多个起点指定输出名称。
此处的 [name]entry 中对象的键名。

1
2
3
4
5
module.exports = {
output: {
filename: "[name].asuka.js"
}
}

该选项同时也可以配置路径,例如如下写法会将文件输出到 output.path 下的 page 目录内。

1
2
3
4
5
module.exports = {
output: {
filename: "page/[name].asuka.js"
}
}

也可以使用函数对该属性进行配置。

1
2
3
4
5
6
7
module.exports = {
output: {
filename: data => {
return data.chunk.name === "main" ? "[name].js" : "[name]/[name].js";
}
}
}

publicPath

指定按需加载或加载外部资源(如图片、文件等)的基本引用路径,直接讲就是打包之后的 index.html 内部的基本引用路径,值是以 runtime(运行时) 或 loader(载入时) 所创建的每个 URL 为前缀。因此,在多数情况下,此选项的值都会以 / 结束

一般来讲,打包后的 index.html 对资源引用时,遵循的格式为 domain + publicPath + filename

例如对于如下配置:

1
2
3
4
5
6
7
module.exports = {
output: {
publicPath: '/build',
filename: "index.js",
path: resolve( __dirname, "build" )
}
}

index.html 内对打包输出的 js 文件的引用路径为 {domain}:{port}/build/index.js

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
//...
output: {
// One of the below
publicPath: "auto", // It automatically determines the public path from either `import.meta.url`, `document.currentScript`, `<script />` or `self.location`.
publicPath: "https://cdn.example.com/assets/", // CDN(总是 HTTPS 协议)
publicPath: "//cdn.example.com/assets/", // CDN(协议相同)
publicPath: "/assets/", // 相对于服务(server-relative)
publicPath: "assets/", // 相对于 HTML 页面
publicPath: "../assets/", // 相对于 HTML 页面
publicPath: "", // 相对于 HTML 页面(目录相同)
},
}

webpack-dev-server 也会默认从 publicPath 为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。

该属性设置为绝对路径例如 / 时,对于 webpack-dev-server 来说一切照常;对于线上环境例如 https://test.marrydream.top/test 目录,将会前往 https://test.marrydream.top/index.js 查找,导致无法找到资源。

而设置为相对路径例如 ./ 时,线上环境与本地 file:// 没有问题;webpack-dev-server 却无法找到资源。

resolve

用于配置各个模块如何解析。

alias

创建 importrequire 的别名,来确保模块引入变得更简单,值为一个对象,可配置多个别名。

示例:

1
2
3
4
5
6
7
module.exports = {
resolve: {
alias: {
"@": path.resolve( __dirname, 'src' )
}
}
}

此时对于 src/assets 目录下的 logo.jpg,直接按照如下方式引入即可。

1
import "@/assets/logo.jpg";

extensions

当路径未明确指定文件后缀名时,将会按照从左往右的顺序依次尝试 extensions 中所配置的后缀名,默认值为 [".js", ".json", ".wasm"]

示例:

1
2
3
4
5
module.exports = {
resolve: {
extensions: [ ".css" ]
}
}

此时对于同级目录下的 asuka.css,直接按照如下方式引入即可。

1
import "./asuka";

注:手动设置后将会覆盖掉默认值,建议设置时携带上默认值一起设置,一些第三方包会依赖默认值进行工作,覆盖掉后会造成一些意外错误。

mainFiles

当引入文件的路径结尾是个目录时,将会按照从左往右的顺序依次尝试 mainFiles 中所配置的文件名,自动匹配目录下的对应名称的文件,默认值为 [ "index" ]

示例:

1
2
3
4
5
6
module.exports = {
resolve: {
extensions: [ ".css" ],
mainFiles: [ "asuka" ]
}
}

此时对于同级目录 css 下的 asuka.css 文件,直接按照如下方式引入即可。

1
import "./css";

modules

告诉 webpack 解析模块时应该搜索的目录,当引入路径是个模块时,将会根据配置从左往右进行查找,默认值为 [ "node_modules" ]

1
2
3
4
5
module.exports = {
resolve: {
modules: [ "node_modules" ]
}
}

devServer

见下面 webpack-dev-server

mode

告知 webpack 使用相应模式的内置优化,存在三种值:

  • none:不使用任何默认优化选项
  • development:会将 DefinePluginprocess.env.NODE_ENV 的值设置为 development,模块和 chunk 使用默认有效名称,devtool 将会被设置为 eval
  • production:会将DefinePluginprocess.env.NODE_ENV 的值设置为 production,模块和 chunk 使用混淆名称,并进行代码压缩。

当未配置时,将会使用可能有效的 NODE_ENV 值作为 mode 值。若仍找不到任何值,则默认设置为 production

若希望根据 webpack.config.js 中的 mode 变量更改打包行为,则应将配置导出为函数。

1
2
3
4
5
6
7
8
9
module.exports = (env, argv) => {
if ( argv.mode === "development" ) {
config.devtool = "source-map";
}
if ( argv.mode === "production" ) {
//...
}
return config;
};

devtool

控制是否生成,以及如何生成 source map。

devtool 选项在内部添加 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 插件,可以通过直接使用这两个插来做到更多定制化才做。但切勿同时使用 devtool 与 这两个插件,那将会使得插件被应用两次。

source map

Source Map 就是一个信息文件,里面储存着位置信息,即存储着代码压缩混淆前后的对应关系。此时当代码出错时,浏览器将会根据原始代码定位错误,而非压缩后的代码。

当开启 source map 时,最终输出结果中,除了输出文件 index.js 外(此处举例为 index.js),还会包含一个 index.js.map 文件,该文件就是 source map 文件。

不过为了防止原始代码通过 source map 的方式暴露给他人,因此经常见到项目中仅在 development 环境下开启 source map,此时在 production 环境下打包的文件不会包含 source map 文件。

注:需在对应浏览器中开启 source map 功能,该功能才可以正常使用。绝大多数浏览器默认情况下是开启的,例如 chrome 的设置位置位于 f12 -> setting -> Preferences -> Enable JavaScript source maps/Enable CSS source maps

工作流程

根据源代码,生成 source map 文件,而后浏览器开启 source map 功能后

可能的值

更多配置方式详见官网 Devtool

eval

modedevelopment 时,默认为该值。此时对于打包生成的文件可以准确定位报错位置(但报错信息存在问题,错误指向 eval),而 webpack-dev-server 下则无法正常工作。

在开发环境下为推荐性能最佳配置方式。

source-map

vue 脚手架开发环境下所给出的配置方式,能够实现源代码映射需求。

此时打包结果中同时存在后缀为 .map 的 source map 文件,且在输出文件的结尾处以注释形式存在 sourceMappingURL 字段指向对应的 .map 文件。

使用高质量 SourceMap 进行生产构建的推荐选择。

eval-source-map

该方式的打包结果中不存在后缀为 .map 的 source map 文件,此时输出文件中的 sourceMappingURL 指向了一个 base64 路径,被以注释形式放在了 eval 方法内。

使用高质量 SourceMap 进行开发构建的推荐选择。

inline-source-map

同样不会在打包结果中出现 .map 文件,且 sourceMappingURL 指向一个 base64 路径,但被放置的位置与 source-map 相同,在文件的结尾处。

打包单个文件时推荐使用的方式。

其他属性值

devtool 的值是存在固定模式的,按照如下顺序和模式即可: [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

  • hidden:存在 .map 文件,但不会自动加载至浏览器内。
  • cheap:此时为性能优化版本,报错信息中仅显示到行,不会具体到列。
  • module:会展示未经 loader 处理的原始代码,该字段不会单独出现。

loader

主要用于在读取某一个特性类型时,对其进行转换。

为什么需要 loader

Webpack 默认只会处理 jsjson 模块,其他模块均不识别。loader 可以将这些模块转换为 Webpack 可识别的模块。

loader 的使用方式

Webpack 5 已经不再支持 cli 的方式来使用 loader,这里仅说明 行内 loader配置文件 两种方式。

行内 loader

以 css 为例,在引入的路径按以下格式在前面添加 loader

1
2
// import "loader1!loader2!文件路径"
import "css-loader!./css/render.css";

配置文件

webpack.config.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
module: {
rules: [
{
test: /\.css$/, // 一般是一个正则表达式,用来匹配需要处理的文件类型
use: [
{
loader: "css-loader", // loader 名称
options: "" // 可选,字符串或对象
}
] // loader 列表
}
]
}
}

对于不需要进行 options 配置且仅有一个 loader 时,可以按照如下方式简写

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.css$/,
loader: "css-loader"
}
]
}
}

当有多个 loader 但其中某个 loader 不需要进行 options 配置时,可以如下方式简写

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [ "css-loader", {
// 其他 loader
} ]
}
]
}
}

注意事项

loader 严格按照编写的顺序依次加载,先加载的 loader 会将处理结果传递给下一个 loader。因此需要加载多个 loader 时应严格遵循 loader 之间的先后顺序,否则会报错。

顺序:从右往左、从下往上

解析 css

基本的 css 代码解析需要用到两个 loader:css-loaderstyle-loader,作用分别如下:

  • css-loader:仅用来将 css 解析为 webpack 识别的语法,不会让 css 代码在页面中生效。
  • style-loader:在当前的页面上生成一个 style 标签,将处理好的内容添加为标签内容。

先安装两个 loader

1
npm i css-loader style-loader -D

修改配置文件如下,从上面可知应该先加载 css-loader 然后加载 style-loader

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [ "style-loader", "css-loader" ]
}
]
}
}

解析 less 与 sass

less

less-loader 的工作是将 less 代码解析为 css 代码,而在这个过程中其实用到了 less 来执行这项工作,因此也需要同时安装 less

1
npm i css-loader style-loader less-loader less -D

根据 less-loader 的工作原理我们不难理解,loader 的加载顺序应该为 less-loader - css-loader - style-loader

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [ "style-loader", "css-loader", "less-loader" ]
}
]
}
}

sass

sass-loaderless-loader 工作类似,而他同样也内置需要一个名为 node-sass 的依赖来进行将 sass 代码解析为 css 的工作。

1
npm i css-loader style-loader sass-loader node-sass -D

而配置方法也参考 less 的配置理解。

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.s(a|c)ss$/,
use: [ "style-loader", "css-loader", "sass-loader" ]
}
]
}
}

postcss 兼容处理

postcss 的详细讲解可以参考 Postcss及其使用,这里不再赘述。

css-loader 的 importLoaders 属性

当前存在两个 css 文件,如下

1
2
3
4
5
6
7
/* native.css */
.title {
user-select: none;
}

/* import-test.css */
@import "./native.css";

在配置了 postcss 的前提下打包后发现,最终生成的代码中并没有添加浏览器前缀。

1
<style>.title { user-select: none; }</style>

这是因为 postcss-loader 并不能对 @import@mediaurl() 语法进行处理,尽管在其之后的 css-loader 可以识别并处理这些代码。但 loader 并不能反向把代码重新丢给 postcss-loader。 因此导致并没有经过兼容处理的代码被放置到了网页上。

因此 css-loader 为我们提供了 importLoaders 属性用以解决该现象。配置方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [ "style-loader", {
loader: "css-loader",
options: {
importLoaders: 1
}
}, "postcss-loader" ]
}
]
}
}

这里的 importLoaders: 1 的意思为:当 css-loader 处理内容时,发现了 @import 等引入代码,将会把代码丢回前 1 个 loader(这里为 postcss-loader) 再次进行处理。

解析文件

对于图片等资源,webpack 需要接触 file-loader 来进行解析。

1
npm i file-loader -D

不难理解,可以对 webpack 进行如下配置

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g)|(png)|(gif)|(bmp)|(svg)$/,
use: [ "file-loader" ]
}
]
}
}

两种加载图片方式

此时存在两种情况:<img /> 标签与 css url() 引入图片。下面区分讨论

打包预览后若提示 Automatic publicPath is not supported in this browser,则需在 output 中配置:publicPath 为打包导出的文件夹路径名。

img 标签

这里有如下的 js 代码:

1
2
3
4
5
6
7
8
9
10
11
function packImg() {
const container = document.createElement( "div" );

const img = new Image();
img.src = require( "../imgs/test.jpg" );
container.appendChild( img );

return container;
}

document.body.appendChild( packImg() );

打包预览后发现实际的路径为:<img src="[object Module]">

原因是在 webpack 5 中,file-loader 使用 esModule 导出,这就导致了使用 require 引入图片时,得到的并非是图片资源,而是一个 { default: xxx } 格式的对象,资源数据被包裹在了 default 属性内。

此时有三种解决方案:

1、手动调用 default 属性

1
img.src = require( "../imgs/test.jpg" ).default;

2、使用 es 模块的方式引入

1
2
import imgSrc from "../imgs/test.jpg";
img.src = imgSrc;

3、修改 webpack loader 通用属性 esModule

在 webpack 的各个 loader 中存在一个通用属性 esModule,表示是否将导出的内容转化为 esModule,默认为 true。在 file-loader 中将其改为 false 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g)|(png)|(gif)|(bmp)|(svg)$/,
use: [
{
loader: "file-loader",
options: {
esModule: false
}
}
]
}
]
}
}

url() 方式

css-loader 可以解析 url(),解析的过程其实是将 url() 直接转为 require 语法。结合上面的内容我们可以得知,默认情况下 require 的导入方式为 esModule,得到的内容不正确导致图片无法渲染。

不过鉴于 css 代码中无法使用 上文中的第 1、2 种方式,因此可以通过给 css-loader 配置 esModule: false 来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [ "style-loader", {
loader: "css-loader",
options: {
esModule: false,
importLoaders: 1
}
}, "postcss-loader" ]
}
]
}
}

导出配置

默认情况下导出的文件名是非常混乱的,我们可以通过在 options 中对 name 属性配置占位符的方式来定制化输出文件的名称。

同时,也可以通过配置 outputPath 来定制化输出文件夹路径。

例如,可以通过下面的配置,使图片被打包在 img 文件夹下,以 文件名.6位hash值.扩展名 的格式命名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g)|(png)|(gif)|(bmp)|(svg)$/,
use: [
{
loader: "file-loader",
options: {
esModule: false,
name: "[name].[hash:6].[ext]",
outputPath: "img"
}
}
]
}
]
}
}

在上面的 outputPath 其实也可以省略,直接用反斜杠 / 拼接在 name 前面也可以生效,即 name: "img/[name].[hash:6].[ext]"

url-loader

url-loaderfile-loader 配置基本相同。

1
npm i url-loader -D

默认情况下两者的区别为:

  • file-loader:将要打包的图片资源拷贝到打包输出目录,并把图片的路径返回
  • url-loader: 将要打包的图片资源以 base64 的方式加载到代码中,不会拷贝到输出目录。

url-loader 工作方式的利弊非常明显,base64 的打包方式可以有效地减少请求图片资源的次数,但对于一些大型图片会导致数据量过大而加载缓慢。

limit 属性

其实在 url-loader 内部也可以调用 file-loader,通过一个 limit 属性来决定图片大小超过多大时改为 file-loader 对图片进行打包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g)|(png)|(gif)|(bmp)|(svg)$/,
use: [
{
loader: "url-loader",
options: {
esModule: false,
name: "[name].[hash:6].[ext]",
outputPath: "img",
limit: 25 * 1024
}
}
]
}
]
}
}

上述配置,对于小于 25kb 的图片使用 base64 进行处理,反之使用 file-loader 进行拷贝打包处理。

url-loader 并不内置 file-loader,因此当涉及到调用 file-loader 时,若未对其进行安装,则会报错。通常建议使用 url-loader 时同时安装 file-loader

babel 兼容性处理

babel 的详细讲解可以参考 Babel及其使用,这里不再赘述。

ts-loader

ts-loader 内置 typescript 的依赖来进行将 TypeScript 代码解析为 JavaScript 的工作,并在解析之前先进行语法的校验。

1
npm i ts-loader typescript -D

配置方法如下:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
// 可通过 babel-loader 对转化后的 js 代码进行进一步兼容转换
use: [ "babel-loader", "ts-loader" ]
}
]
}
}

也可以只使用 babel-loader 进行处理,babel 存在专门针对 TypeScript 的预设,各有优缺点,详见 Babel及其使用

Vue

vue-loader 在 16.x 之后为支持 vue3 的版本,若需支持 vue2,则应使用 16.x 以下的版本。

vue-loader 默认实现了 HMR 的热更新

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
use: [ "vue-loader" ]
}
]
}
}

在 vue-loader@15 之前,不需要我们去处理 vue-loader-plugin,在之后则需要我们自己手动的去加载这个插件。

vue-loader-plugin 已在 vue-loader 中默认安装,手动引入即可

1
2
3
4
5
6
7
const VueLoaderPlugin = require( "vue-loader/lib/plugin" );

module.exports = {
plugins: [
new VueLoaderPlugin()
]
}

asset module type

在 webpack 5 之前,处理图片等资源需要用到 file-loaderurl-loader,但在 webpack 5之后则可以直接使用内置的 asset 资源模块(asset module type)。

asset module type 中有几类常见的配置选项。

  • asset/resource:file-loader 的实现,可以把资源拷贝到指定的目录
  • asset/inline:url-loader 的实现,把相应的资源以 base64 的方式添加到行内。
  • asset/source:raw-loader 不常用
  • asset:通过配置参数来动态的决定该使用 asset/resource 还是 asset/inline

对 webpack 进行配置时,可以通过 type 属性来定义当前匹配要使用的类型。例如,下面的操作可以替代默认情况下 file-loader 的使用。

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g)|(png)|(gif)|(bmp)|(svg)$/,
type: "asset/resource"
}
]
}
}

asset/resource

对于 asset/resource 有两种配置输出路径方法。

全局配置

可以通过修改 output -> assetModuleFilename 来修改输出的路径,占位符格式与 file-loader 的导出配置基本一致,不同的是这里的 [ext] 会自动包含 .,不需要手动添加。

1
2
3
4
5
module.exports = {
output: {
assetModuleFilename: "img/[name].[hash:6][ext]"
}
}

该方式为所有的资源配置了相同的导出配置,会导致例如 字体 等文件也被打包在 img 目录下,不建议使用。

局部配置

使用 generator 来针对性的对规则进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g)|(png)|(gif)|(bmp)|(svg)$/,
type: "asset/resource",
generator: {
filename: "img/[name].[hash:6][ext]"
}
}
]
}
}

asset/inline

直接配置 type 即可,无需其他任何配置。

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g)|(png)|(gif)|(bmp)|(svg)$/,
type: "asset/inline"
}
]
}
}

asset

通过 asset 可以通过配置 parser -> dataUrlCondition -> maxSize 动态的决定使用 asset/resource 还是 asset/inline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g)|(png)|(gif)|(bmp)|(svg)$/,
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 24 * 1024
}
}
}
]
}
}

处理图标字体

对于图标字体,我们不需要让其进行 base64 处理,只需要直接被拷贝到 font 文件夹即可,因此可以如下方式配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
module: {
rules: [
{
test: /\.(ttf)|(woff2?)$/,
type: "asset/resource",
generator: {
filename: "font/[name].[hash:3][ext]"
}
}
]
}
}

webpack 插件

webpack 在打包过程中其实也拥有自己的生命周期,而插件可以贯穿打包的整个生命周期,并自由的选择在什么时候执行什么操作。例如打包时自动清除导出目录、打包过程对代码进行压缩、定义全局变量等。相较 loader,插件可以做更多的事情。

插件需要被配置在 plugins 属性下,以数组的形式,如下

1
2
3
4
5
6
module.exports = {
plugins: [
// plugin1
// plugin2
]
}

每一个插件本质上是一个 class,因此使用时直接 new XXX( ...arg ) 即可。

下面讲述部分插件的使用。

clean-webpack-plugin

可用于在每次打包发布时自动清理掉打包输出目录中的旧文件。

1
npm i clean-webpack-plugin -D

使用方式如下,详细配置参数参考 项目地址

1
2
3
4
5
6
7
const { CleanWebpackPlugin } = require( "clean-webpack-plugin" );

module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}

DefinePlugin

DefinePlugin 是一个 webpack 内置插件,用来全局定义变量。由于是内置插件,所以无需安装。

1
2
3
4
5
6
7
8
9
const { DefinePlugin } = require( "webpack" );

module.exports = {
plugins: [
new DefinePlugin( {
BASE_URL: "'./'"
} )
]
}

以键值对的形式写入插件的对象参数内即可生效。但需要注意的是,DefinePlugin 会把值原封不动的放置到全局,因此这里如果希望结果是字符串,应当为 "'./'" 而非 "./"

html-webpack-plugin

在此之前有我们手动的新建 html 并修改标题、引入js,使用此插件后将会在打包时自动的生成 html 并填充标题、加载js。

1
npm i html-webpack-plugin -D

默认情况下,生成的 html 标题为 Webpack App,并根据 output -> publicPath 导入打包的 js。可通过 title 等配置进行定制化修改,详见 项目地址

1
2
3
4
5
6
7
8
9
const HtmlWebpackPlugin = require( "html-webpack-plugin" );

module.exports = {
plugins: [
new HtmlWebpackPlugin( {
title: "test-title"
} )
]
}

自定义 html 模板

由于默认的模板常常不能满足我们的需求(例如 Vue 的 html 中需要存在 <div id="app"></div>),因此需要自定义 html 模板。

新建 public 文件夹(该文件夹规范上讲一般不参与打包而是直接拷贝到输出目录内),新建 index.html 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>

<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app">测试 html-webpack-plugin 模板</div>
</body>

</html>

为插件参数对象中提供 template 属性,来指定模板路径。并通过上面所讲的 DefinePlugin 来添加全局属性 BASE_URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const HtmlWebpackPlugin = require( "html-webpack-plugin" );
const { DefinePlugin } = require( "webpack" );

module.exports = {
plugins: [
new HtmlWebpackPlugin( {
title: "test-title",
template: "./public/index.html"
} ),
new DefinePlugin( {
BASE_URL: "'./'"
} )
]
}

copy-webpack-plugin

希望将部分文件直接拷贝到打包目录。

1
npm i copy-webpack-plugin -D

插件参数对象中存在一个 patterns 属性,值为对象数组,用来对多个文件设置拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const CopyWebpackPlugin = require( "copy-webpack-plugin" );

module.exports = {
plugins: [
new CopyWebpackPlugin( {
patterns: [ {
from: "public",
to: "icon",
globOptions: {
ignore: [ "**/index.html" ]
}
} ]
} )
]
}

对上面几个属性做一下说明:

  • form: 要被拷贝的目录/文件路径。
  • to:可选,拷贝至打包目录下的指定目录/路径,如这里就是 build/icon/。不配置时默认情况下会将 form 目录下的内容拷贝到打包根目录下即 build
  • globOptions:
    • ignore: 忽略待拷贝目录下的文件列表,如要拷贝 public 目录时,由于里面还存放着 html-webpack-plugin 插件所需的 index.html
      已经对其做出了拷贝处理,此时若不忽略该文件,将会控制台打印报错。其中这里的 **/ 为在 form 指定的目录下查找的意思,不可遗漏。

webpack-dev-server

在平常开发时,通常希望当代码做出改动时,webpack 会自动打包输出并自动更新预览页面,有两种实现方式。

为了解决这个需求,我们可以使用 watch 字段监听代码改动并自动打包,对打包输出的 index.html 使用 live server 方式打开预览。

命令行方式:

1
npx webpck --watch

配置文件方式:

1
2
3
4
module.export = {
// 默认为 false
watch: true
}

该方式有如下几点不足:

  • 当一个文件变化后,所有的文件都会重新进行打包编译。
  • 每次编译打包后都会进行文件读写(例如 clean-webpack-plugin 导致的清除打包目录操作)。
  • liver server 更新策略是刷新整个页面,我们期望他只是局部刷新所改动的部分。

此时便可以使用 webpack 提供的 webpack-dev-server 来实现该需求。

使用方式

webpack-dev-server 可以搭建一个可热更新的本地的服务器来提高开发效率。

1
npm i webpack-dev-server -D

使用如下命令,将会启动一个占用 8080 端口的服务器,且代码改动后将会自动打包。

1
2
# 会查找默认的 webpack.config.js,可同样使用 --config 进行自行指定
npx webpack server

值得注意的是,运行后并没有产生打包目录。该插件其实是把数据都存放在了内存中进行处理。

相关配置项

所有和 webpack-dev-server 有关的配置均可以在 devServer 中进行配置。

如果遇到问题,导航到 /webpack-dev-server 路线将显示提供文件的位置。例如,http://localhost:20715/webpack-dev-server

这一部分配置参考 DevServer,这里提供几个常用的配置。

port

指定开启服务的端口,默认 auto,即默认 8080,当端口冲突时会尝试替换为其他 prot

open

设置为 true 时,服务启动后自动打开浏览器,默认 false。可以传递 string object [string, object]

1
2
3
4
5
6
module.exports = {
//...
devServer: {
open: true,
},
};

可以传递一个字符串,用于启动时,在浏览器打开指定页面。值为数组且包含多项时。将会同时打开这些页面。

1
2
3
4
5
6
module.exports = {
//...
devServer: {
open: ['/login-page', '/another-page'],
},
};

proxy

在开发中常会遇到请求接口的跨域问题,尽管后端可以通过 cors 进行跨域配置,但让后端来专门为开发服务器放行似乎有些多此一举。由于 webpack-dev-server 本身就开启了一个服务,因此我们可以让这个服务去帮助我们请求另一个服务端的数据,从而解决跨域问题。这就是 proxy 属性所做的事情。

例如我们希望能通过如下的代码来做到请求 https://api-kozakura.marrydream.top/common/sao_admin/v1/user/page?p=0&s=16 的效果。

1
2
3
4
// 这里其实等同于请求 localhost:post/sao/user/page?p=0&s=16
fetch("/sao/user/page?p=0&s=16").then(async res => {
console.log("请求到了:", await res.json());
});

对 webpack.config.js 中进行如下配置即可实现该效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
// ...
devServer: {
// ...
proxy: {
"/sao": {
target: "https://api-kozakura.marrydream.top",
pathRewrite: { "^/sao": "/common/sao_admin/v1" },
changeOrigin: true
}
}
}
}

其中键值 /sao 是一个标识,表示仅对以 /sao 开头的请求地址进行代理。

下面对 proxy 的各个属性做出解释

target

代理指向目标,会替换当前主机地址为指定地址。例如在上面的案例中配置该属性后实际请求地址为 https://api-kozakura.marrydream.top/sao/user/page?p=0&s=16

pathRewrite

对代理后的地址进行重写。例如从 target 的解释中可以发现,导向的地址并不是我们所需要的地址。上面案例中通过重写将 /sao 替换为 /common/sao_admin/v1

changeOrigin

默认情况下,代理时保留主机标头的来源,即请求头中的 HOST 依然为本机地址(例如 localhost:xxxx)。此时对于一些对来源进行了限制处理的 api 会出现无法请求的情况。

设置为 true 后,后端 api 的视角下将会变为自己请求自己。(由于无法改变浏览器的行为,因此此时浏览器中的 HOST 依然显示为 localhost,但实际已经起作用了)

context

上面的案例中仅对单个标识进行了代理,如果想对多个标识进行同样的代理处理,可以使用 context 属性,且 proxy 改为数组写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
// ...
devServer: {
// ...
proxy: [
{
context: ["/sao"],
target: "https://api-kozakura.marrydream.top",
pathRewrite: { "^/sao": "/common/sao_admin/v1" },
changeOrigin: true
}
]
}
}

context 还可以采用函数写法,如下写法将会对所有的请求启用代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
// ...
devServer: {
// ...
proxy: [
{
context: () => true,
target: "https://api-kozakura.marrydream.top",
pathRewrite: { "^/sao": "/common/sao_admin/v1" },
changeOrigin: true
}
]
}
}

secure

默认情况下,将不接受使用无效证书在 HTTPS 上运行的后端服务器。可以通过设置 securefalse 来覆盖该行为。

compress

开启服务端的 gzip 压缩,默认 true

webpack-dev-middleware

webpack-dev-server 内部使用了 webpack-dev-middleware 包,该包是一个容器,可以把 webpack 处理后的文件传递给一个服务器。可以单独使用,以便进行更多自定义的设置来实现更多需求。

1
npm i webpack-dev-middleware

下面简单实现一下 webpack-dev-server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* server.js */
const express = require("express");
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");

// 获取 webpack 配置内容
const config = require("./webpack.config");
const confiler = webpack(config);

// 启动的端口
const port = 11451;

express()
// 将 webpack 打包的内容交给启动的服务
.use(webpackDevMiddleware(confiler))
// 在指定端口上启动服务
.listen(11451, () => {
console.log(`listen on http://localhost:11451`);
});

使用 node server 启动,可以得到与 webpack-dev-server 类似的效果。

热更新 与 HMR

热更新,即保存后自动刷新页面,可在对 devServer 进行如下配置:

1
2
3
4
5
module.exports = {
devServer: {
hot: true
}
}

开启后控制台将会打印这样一段文字,并在发生更改时自动刷新页面。

1
[HMR] Waiting for update signal from WDS...

若设置了 hot: true 后热更新无效果,可能是 mode: development.browserslistrc 冲突,设置 target 屏蔽即可。

1
2
3
4
5
6
7
8
module.exports = {
module: "development"
devServer: {
hot: true
},
// 屏蔽 .browserslistrc 配置
target: "web"
}

但此时为全局热更新,即刷新整个页面。我们希望它可以实现局部热更新,此时仅更新改动的部分,不会影响其他部分组件;控制台会保留之前的数据,并打印修改后的输出。

可在入口文件下进行如下配置:

1
2
3
4
5
6
if( module.hot ) {
module.hot.accept([ "./js/hmr.js" ], () => {
// 每次更新时的回调函数
console.log( "hmr.js 模块更新" );
})
}

配置后,每次 babel.js 内的代码发生变动时,将只更新发生变动的部分。

区分环境打包

可以通过追加 --env 的 flag 来指定打包时的变量。

package.json 中新增两条 script:

1
2
3
4
5
6
{
"scripts": {
"dev": "webpack --env NODE_ENV=development",
"build": "webpack --env NODE_ENV=production"
}
}

此处 NODE_ENV 与对应的值可自定义。

将 webpack 配置文件改写为函数形式:

1
2
3
4
5
6
module.exports = ( env ) => {
console.log( env );
return {
// ...
}
}

执行 npm run dev 进行打包,可以得到打印结果:

1
{ WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, NODE_ENV: 'development' }

显而易见,我们可以通过设置自定义变量参数,来区分环境的对配置文件进行配置。

合并多个配置文件

为了更直观的针对不同环境进行配置,可以将配置文件拆分为三个,分别为 webpack.common.jswebpack.prod.jswebpack.dev.js,放置到 config 文件夹下。

package.json 中的 scripts 进行修改:

1
2
3
4
5
6
{
"scripts": {
"dev": "webpack --config ./config/webpack.common.js --env NODE_ENV=development",
"build": "webpack --config ./config/webpack.common.js --env NODE_ENV=production"
}
}

webpack.prod.jswebpack.dev.js 中分别抛出各自的配置文件:

1
2
3
4
module.exports = {
// mode: "development",
// mode: "production"
}

webpack.common.js 中进行统一处理,此处使用了 webpack 提供的 merge 方法来合并配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { merge } = require( "webpack-merge" );

module.exports = env => {
// 通过上文所说的 --env 变量确定当前环境,获取当前应该加载的配置文件
const config = env.NODE_ENV === "production"
? require( "./webpack.prod.js" )
: require( "./webpack.dev.js" );


// 开发与生产环境下的通用配置
const baseConfig = {};

// 抛出合并后的配置
return merge( baseConfig, config );
}