webpack 5.x 简易手册

本文源码 在此

起步

检查 node.js 和 npm 的版本。

1
2
3
4
$ node -v
v16.13.2
$ npm -v
8.1.2

新建一个目录,进入。新建 package.json 文件。

1
2
3
$ mkdir webpack-demo
$ cd webpack-demo
$ npm init -y

为了提高下载速度,安装 cnpm。

1
$ sudo npm i -g cnpm --registry=https://registry.npmmirror.com

用 cnpm 安装 webpack 及其命令行工具。检查安装的版本。

1
2
3
4
5
$ cnpm i -D webpack webpack-cli
$ npx webpack -v
webpack: 5.67.0
webpack-cli: 4.9.2
webpack-dev-server not installed

package.jsonscripts 中添加 "build": "webpack"

1
2
3
"scripts": {
"build": "webpack"
}

这样就可以用 npm run build 命令来执行一次打包。

新建两个子目录 srcdist,分别用来放置源文件和打包输出。

1
$ mkdir src dist

新建 ./src/app.js 文件,作为打包入口。

1
$ touch ./src/app.js
1
alert('Hello webpack!')

新建 index.html 文件,假设打包输出为 ./dist/bundle.js

1
$ touch index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<title>Hello World!</title>
</head>
<body>
<p>Hello World!</p>

<script src="./dist/bundle.js"></script>
</body>
</html>

最终的目录结构:

1
2
3
4
5
6
7
8
9
$ tree -L 2 -I "node_modules"
.
|-- dist
|-- index.html
|-- package.json
`-- src
`-- app.js

2 directories, 3 files

基本配置

习惯上将 webpack 的配置文件命名为 webpack.config.js,须手动创建,放在项目根目录下,webpack 会自动引用此配置文件。

1
$ touch webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const path = require('path')

module.exports = {
// 环境,production 生产环境 / development 开发环境
mode: 'production',

// 打包的入口
entry: './src/app.js',

// 打包的输出
output: {
filename: 'bundle.js',
// 输出目录的绝对路径
path: path.resolve(__dirname, 'dist'),
// 输出目录的相对路径(相对网站根目录)
publicPath: '/'
},

// 模块,装载器等,见下文
module: {},

// 插件,涉及 `.css` 的剥离和压缩、`.html` 的动态生成等,见下文
plugins: []
}

尝试进行一次打包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ npm run build

> webpack-demo@1.0.0 build
> npx webpack

asset bundle.js 24 bytes [compared for emit] [minimized] (name: main)
./src/app.js 23 bytes [built] [code generated]
webpack 5.67.0 compiled successfully in 254 ms

$ tree -L 2 -I "node_modules"
.
|-- dist
| `-- bundle.js
|-- index.html
|-- package.json
|-- src
| `-- app.js
`-- webpack.config.js

2 directories, 5 files

尝试用浏览器打开 index.html,看是否有弹窗。

认识装载器

装载器在 webpack.config.jsmodule.rules 中列出。

1
2
3
4
module: {
rules: [
]
}

rules 中的元素是对象类型,通常包含 test use 两个属性。test 匹配文件名,use 列出装载器,由此形成一条流水线(调用顺序与书写顺序相反)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { importLoaders: 1 }
},
{
loader: 'less-loader',
options: { noIeCompat: true }
}
]
}
]
}

上例表示以 .less 结尾的文件依次用 less-loader css-loader style-loader 处理。

只需一个装载器时,用 loader 代替 use

1
2
3
4
{
test: /\.(png|svg|jpg|gif)$/,
loader: 'file-loader'
}

添加装载器

安装上述提到的 3 个的装载器:

1
$ cnpm i -D style-loader css-loader file-loader
  • css-loader 负责解析 .js 文件中的 import '.css' 以及 .css 文件中的 @import '.css'
  • style-loader 负责生成 <style> 标签并追加到 <head> 标签中。

webpack.config.jsmodule.rules 中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
loader: 'file-loader'
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
loader: 'file-loader'
}
]
}

新建 ./src/app.css 文件。

1
$ touch ./src/app.css
1
2
3
p {
background-color: blue;
}

./src/app.js 中引入:

1
import './app.css'
1
2
3
4
5
6
7
8
9
10
11
12
13
$ rm -f ./dist/* && npm run build
$ tree -L 2 -I "node_modules"
.
|-- dist
| `-- bundle.js
|-- index.html
|-- package.json
|-- src
| |-- app.css
| `-- app.js
`-- webpack.config.js

2 directories, 6 files

检查 <p> 标签的背景色。

使用 Bootstrap 5.x

安装 Bootstrap 及其依赖包。

1
$ cnpm i -D bootstrap @popperjs/core

导入 Bootstrap 的 JavaScript 和 CSS,在 ./src/app.js 的顶部插入:

1
2
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'

index.html 中使用 Bootstrap 的样式类 text-success

1
<p class="text-danger">Hello World!</p>

重新打包,检查字体的颜色。

省略后缀名

.js 文件中用 import 导入模块时,可以省略模块的后缀名,但必须在 webpack.config.jsresolve.extensions 中列出。

1
2
3
resolve: {
extensions: ['.js', '.json', '.css', '.sass', '.vue']
}

多入口

新建 JS 文件。

1
$ touch ./src/admin.js
1
alert('Hello admin!')

webpack.config.js 中,entry 改成对象类型,output.filename[name] 拼接。

1
2
3
4
5
6
7
entry: {
app: './src/app.js',
admin: './src/admin.js'
},
output: {
filename: '[name].bundle.js',
},

相应修改 index.html

1
2
<script src="./dist/app.bundle.js"></script>
<script src="./dist/admin.bundle.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ rm -f ./dist/* && npm run build
$ tree -L 2 -I "node_modules"
.
|-- dist
| |-- admin.bundle.js
| |-- app.bundle.js
| |-- app.bundle.js.LICENSE.txt
|-- index.html
|-- package.json
|-- src
| |-- admin.js
| |-- app.css
| `-- app.js
`-- webpack.config.js

2 directories, 9 files

重新打包,检查是否有两次弹窗。

第二次弹窗

分离第三方库

可以单独打包第三方库,而不在 .js 文件中引入,只须在 entry.vendors 中列出第三方库,这会得到 vendors.bundle.js 文件(文件名取决于 output.filename)。

1
2
3
entry: {
vendors: ['bootstrap', '@popperjs/core']
}

index.html 中添加:

1
<script src="./dist/vendors.bundle.js"></script>

注释 ./src/app.js 中的 import 'bootstrap'

1
// import 'bootstrap'

剥离 CSS

剥离 .js 引入的 .css,合并成单独的 .css 文件,每个入口对应一个。

安装插件 mini-css-extract-plugin

1
$ cnpm i -D mini-css-extract-plugin

plugins 中实例化插件,在 module.rules 中应用插件。同时,为方便演示:

  • 删除 ./src/admin.js,相应修改 entry
  • 修改 output.filename
  • MiniCssExtractPlugin.loader 替换 style-loader
  • plugins 中初始化 MiniCssExtractPlugin
1
$ rm -f ./src/admin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

entry: {
// admin: './src/admin.js',
},
output: {
// filename: '[name].bundle.js',
filename: '[name].js',
},
module: {
rules: [
{
test: /\.css/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
]
},
plugins: [
// 将输出 `[name].css`
new MiniCssExtractPlugin({ filename: '[name].css' })
]

修改 index.html

1
2
3
4
<link rel="stylesheet" href="./dist/app.css">

<script src="./dist/vendors.js"></script>
<script src="./dist/app.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ rm -f ./dist/* && npm run build
$ tree -L 2 -I "node_modules"
.
|-- dist
| |-- app.css
| |-- app.js
| |-- vendors.js
| `-- vendors.js.LICENSE.txt
|-- index.html
|-- package.json
|-- src
| |-- app.css
| `-- app.js
`-- webpack.config.js

2 directories, 9 files

压缩 CSS

安装插件 css-minimizer-webpack-plugin

1
$ cnpm i -D css-minimizer-webpack-plugin

optimization.minimizer 中实例化即可。

1
2
3
4
5
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')

optimization: {
minimizer: ['...', new CssMinimizerPlugin()]
}

... 表示对 minimizer 进行扩展而不是覆盖,以保留内置的 .js 压缩插件。

自动清理打包输出

安装插件 clean-webpack-plugin

1
$ cnpm i -D clean-webpack-plugin

plugins 中实例化即可。

1
2
3
4
5
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

plugins: [
new CleanWebpackPlugin()
]

带 hash 的文件名、自动生成 index.html

安装插件 html-webpack-plugin

1
$ cnpm i -D html-webpack-plugin

文件名用 [fullhash] 拼接。在 plugins 中调用 HtmlWebpackPlugin 指定模板 .ejs、输出文件名、是否将 .css .js 直接嵌入模板而不通过 <link> <script> 引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const HtmlWebpackPlugin = require('html-webpack-plugin')

output: {
filename: '[name].[fullhash].js',
publicPath: '/'
},
plugins: [
new MiniCssExtractPlugin({ filename: '[name].[fullhash].css' }),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.ejs',
filename: 'index.html',
inject: false
})
]

删除没用的 index.html,新建模板 ./src/index.ejs

1
2
$ rm -f ./index.html
$ touch ./src/index.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<title>Hello World!</title>

<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[0] %>">
</head>
<body>
<p class="text-danger">Hello World!</p>

<script src="<%= htmlWebpackPlugin.files.js[0] %>"></script>
<script src="<%= htmlWebpackPlugin.files.js[1] %>"></script>
</body>
</html>

重新打包,检查是否正确生成 ./dist/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ npm run build
$ tree -L 2 -I "node_modules"
.
|-- dist
| |-- app.7faf852820591e38bbd0.css
| |-- app.7faf852820591e38bbd0.js
| |-- index.html
| |-- vendors.7faf852820591e38bbd0.js
| `-- vendors.7faf852820591e38bbd0.js.LICENSE.txt
|-- package.json
|-- src
| |-- app.css
| |-- app.js
| `-- index.ejs
`-- webpack.config.js

2 directories, 10 files

开发时配置

安装 webpack-merge

1
$ cnpm i -D webpack-merge

新增配置文件 webpack.dev.config.js

1
$ touch webpack.dev.config.js
1
2
3
4
5
6
7
8
9
const { merge } = require('webpack-merge')
const common = require('./webpack.config.js')

module.exports = merge(common, {
// 开发环境
mode: 'development',
// 到源码的映射
devtool: 'inline-source-map'
})

用处见下文。

使用 webpack-dev-server

webpack-dev-server 可以快速开启一个 Web 服务,并在文件发生改变时重新编译并通知浏览器。

安装 webpack-dev-server

1
$ cnpm i -D webpack-dev-server

webpack.dev.config.js 中新增配置项 devServer

1
2
3
4
5
6
devtool: 'inline-source-map',
devServer: {
host: "127.0.0.1",
port: 8080,
historyApiFallback: true // 兼容 HTML5 history API
}

修改 package.jsonscripts

1
2
3
"scripts": {
"dev": "webpack serve -c webpack.dev.config.js --open"
}

webpack 的 serve 命令用于启动 webpack-dev-server,--open 选项用于启动浏览器。

执行 npm run dev 查看效果。

1
2
3
4
5
6
7
8
9
10
11
12
$ tree -L 2 -I "node_modules"
.
|-- dist
|-- package.json
|-- src
| |-- app.css
| |-- app.js
| `-- index.ejs
|-- webpack.config.js
`-- webpack.dev.config.js

2 directories, 6 files

使用 Vue.js 3.x

安装。

1
$ cnpm i -D vue@next @vue/compiler-sfc vue-loader@next

修改 webpack.config.jsmodule.rulesplugins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const webpack = require('webpack')
const { VueLoaderPlugin } = require("vue-loader")

module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
]
},
plugins: [
new VueLoaderPlugin()
],

修改 ./src/index.ejs

1
2
3
4
5
<body>
<div id="app" v-cloak></div>

<script src="<%= htmlWebpackPlugin.files.js[0] %>"></script>
</body>

修改 ./src/app.css

1
2
3
[v-cloak] {
display: none;
}

新增文件 ./src/app.vue

1
$ touch ./src/app.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<p @click="onClick" class="text-danger">Hello World!</p>
</template>

<script>
export default {
methods: {
onClick() {
alert('clicked')
}
}
}
</script>

./src/app.js 中添加:

1
2
3
4
import { createApp } from 'vue'
import app from './app.vue'

createApp(app).mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
$ tree -L 2 -I "node_modules"
.
|-- dist
|-- package.json
|-- src
| |-- app.css
| |-- app.js
| |-- app.vue
| `-- index.ejs
|-- webpack.config.js
`-- webpack.dev.config.js

2 directories, 7 files

执行 npm run dev 查看效果。

使用 Electron 16.x

webpack.config.js 中增加 target 并修改 output

1
2
3
4
5
6
7
8
9
10
11
target: 'electron-renderer',

output: {
// filename: '[name].[fullhash].js',
filename: '[name].js',
// publicPath: '/'
publicPath: './' // 相对于 HTML 文档本身所在目录

plugins: [
// new MiniCssExtractPlugin({ filename: '[name].[fullhash].css' }),
new MiniCssExtractPlugin({ filename: '[name].css' }),

必须设定 target,否则不能在渲染进程中使用 fs path 等模块。

安装 Electron。

1
2
3
4
5
6
7
$ cnpm i -D electron electron-packager
$ npx electron -v
v16.0.7
$ npx electron-packager --version
Electron Packager 15.2.0
Node v15.10.0
Host Operating system: win32 10.0.19042 (x64)

新建 main.js 作为主进程。

1
$ touch main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { app, BrowserWindow } = require('electron')

function createWindow() {
const options = {
width: 400,
height: 300,
webPreferences: { nodeIntegration: true }
}
const win = new BrowserWindow(options)
win.loadFile('./dist/index.html') // 相对于 Electron 的工作目录
}

app.whenReady().then(() => {
createWindow()

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

win.loadFile('./dist/index.html'); HTML 文档的路径相对于 Electron 的工作目录。HTML 文档中引用的外部文件(如 CSS、JS 文件等)的路径则相对于 HTML 文档本身。

修改 package.json,添加 main 字段。

1
2
3
4
5
6
7
"main": "main.js",
"scripts": {
"build": "webpack",
"dev": "electron .",
"test": "webpack && electron .",
"make": "electron-packager . --ignore='\\.gitignore|webpack*\\.js|node_modules|src' --overwrite --download.mirrorOptions.mirror=https://npm.taobao.org/mirrors/electron/"
}
  • --ignore 忽略 .gitignore webpack*.js node_modules/ src/ 等文件。
  • --overwrite 如存在旧的软件包,直接覆盖。
  • --download 从淘宝 NPM 镜像下载。

./src/index.ejs 添加:

1
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

用 webpack 打包,用 Electron 运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ npm run build
$ tree -L 2 -I "node_modules"
.
|-- dist
| |-- app.css
| |-- app.js
| |-- index.html
| |-- vendors.js
| `-- vendors.js.LICENSE.txt
|-- main.js
|-- package.json
|-- src
| |-- app.css
| |-- app.js
| |-- app.vue
| `-- index.ejs
|-- webpack.config.js
`-- webpack.dev.config.js

2 directories, 13 files

$ npm run dev

可以用 npm test 代替以上两个命令。

打包成 .exe 文件。

1
2
3
4
5
6
7
$ npm run make

> webpack-demo@1.0.0 make
> electron-packager . --ignore='\\.gitignore|webpack*\\.js|node_modules|src' --overwrite --download.mirrorOptions.mirror=https://npm.taobao.org/mirrors/electron/

Packaging app for platform win32 x64 using electron v11.3.0
Wrote new app to...

使用 Element Plus

安装下列两个插件就可以直接在 .vue 中使用 Element Plus 组件,而不需要任何 import 语句。

1
$ npm i -D element-plus unplugin-vue-components unplugin-auto-import

修改 webpack.config.jsmodule.rulesplugins 两个字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module: {
rules: [
{
test: /\.m?js$/,
resolve: {
byDependency: { esm: { fullySpecified: false } }
}
}
]
},

plugins: [
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
]

.vue 中使用 Element Plus 提供的按钮组件。

1
2
3
<template>
<el-button>I am ElButton</el-button>
</template>