近期本人在使用 TypeScript 实现一些 Web 小游戏,期望通过工具完成项目构建和打包,因此研究了下 Webpack 和 Rollup,并最终选择了 Webpack。本文将简单讲解如何使用 Webpack 从零开始搭建一个项目,并完成最终产物的打包。
Webpack 介绍
Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具,本质上它的功能是从一个或多个入口点构建一个依赖图,然后将项目中所需的每一个模块组合成一个或多个 bundles。这些模块和最终产物均为静态资源,比如 HTML 文件、JavaScript 脚本、CSS 样式、图片、文本等。
为什么需要 Webpack?一个简单的 Web 页面除了图片、视频这类资源外,可能只有 index.html、main.js、main.css。由于内容不多,我们仅在三个文件内开发也比较方便。部署也仅需要将这些文件拷贝到 Web 服务器上即可。
index.html
main.css
main.js
然而现在大部分的 Web 页面都是相对复杂的,单单是 JavaScript 代码量级都不小,如果都放到一个源代码文件中,不利于后续迭代和维护。于是人们都将代码模块化,分到不同的文件或模块中。
index.html
main.css
main.js
js/user.js
js/book.js
但是这样又引入新的问题,不管是 ES Module 还是 script 标签,都会导致浏览器需要发起多次请求获取模块文件,带来新的网络开销。而 Webpack 的引入,可以处理所有源代码并最终仅产出一个 JavaScript 文件,这样既解决了单文件维护成本高的问题,也解决了多文件带来网络开销的问题。这就是 Webpack 的打包。
Webpack 还提供了其他打包功能,比如最小化、代码混淆。除了 JavaScript,Webpack 还支持处理 CSS、HTML、图片、文本等,可以用于统一 Web 项目的所有静态资源打包。
项目创建
首先,我们创建一个新的 npm 项目,用于本文演示 Webpack 的项目构建。
1
2
3
| mkdir webpack-demo
cd webpack-demo
npm init
|
然后通过 npm 安装 Webpack 以及相关的命令行工具依赖。
1
| npm install -D webpack webpack-cli
|
接下来我们在项目根目录下创建 webpack.config.js 文件,这是默认的 Webpack 配置文件,也可以在 Webpack 命令执行的时候通过 -c 参数指定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // webpack.config.js
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('webpack').Configuration}
*/
export default {
// 项目源代码入口
entry: "./index.js",
// 最终产物输出地址、文件名等信息
output: {
path: resolve(__dirname, "./dist/"),
filename: "index.js",
clean: true, // 生成产物时清除旧产物文件
},
};
|
此时我们执行 npx webpack 会提示报错,这里存在两个原因:一是我们的 Webpack 配置文件是使用 ESM 编写的,Webpack 默认没法识别 import 等关键词;二是我们还没有创建 index.js 文件。
首先我们修改 package.json 文件,添加将项目指定为 module 类型。
1
2
3
| {
"type": "module"
}
|
其次编写一个简单的 JavaScript 文件,仅用于测试 Webpack 项目是否能正常构建。
1
2
3
4
5
6
| // index.js
function add(a, b) {
return a + b;
}
console.log(add(1, 2));
|
然后执行 Webpack,生成产物。
1
| npx webpack --mode=production
|
此时,Webpack 已经将 index.js 打包并经过优化、剪枝、压缩等步骤,最终产出文件 dist/index.js。
1
2
| // dist/index.js
(()=>{"use strict";console.log(3)})();
|
JavaScript 打包
引入 TypeScript
TypeScript 是基于 JavaScript 的编程语言,是 JavaScript 的超集,在后者的基础上增加了类型系统等新特性。在 Web 项目中引入 TypeScript 替换 JavaScript,可以使得我们在开发阶段就避免出现一些容易犯错的简单问题。
在 Webpack 项目中引入 TypeScript 需要安装 typescript 和 ts-loader,用于将 TypeScript 源代码编译成 JavaScript 代码。
1
| npm install -D ts-loader typescript
|
创建 tsconfig.json 文件,按需配置 TypeScript 的选项,或直接使用 tsc –init 初始化后修改对应的 module 和 target。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["./"]
}
|
然后将 index.js 修改为 index.ts,并添加上类型相关信息。
1
2
3
4
5
6
| // index.ts
function add(a: number, b: number): number {
return a + b;
}
console.log(add(1, 2));
|
为了让 Webpack 项目能正常处理 ts 文件,我们需要修改配置文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // webpack.config.js
export default {
entry: "./index.ts", // 修改为 ts 文件
resolve: {
extensions: ['.js', '.json', '.ts'], // 添加对 ts 扩展的处理
},
module: {
rules: [
// 添加对 ts|tsx 文件的规则, 并使用 ts-loader 处理
{
test: /\.(ts|tsx)$/i,
loader: 'ts-loader',
exclude: ['/node_modules/'],
}
]
},
};
|
此时我们已经能顺利将 TypeScript 源码文件最终打包成 JavaScript 产物。
模块引用
当项目代码变得复杂之后,我们需要将代码模块化,便于代码维护和迭代。这里我们将上文的 TypeScript 代码中的 add 函数通过 ESM 抽离出来独立成模块。
1
2
3
4
| // math.ts
export function add(a: number, b: number): number {
return a + b;
}
|
1
2
3
4
| // index.ts
import { add } from './math';
console.log(add(1, 2));
|
通过 npx webpack –mode=production 即可将多个 TypeScript 文件打包最终生成一个 JavaScript 代码产物。
HTML 打包
上文仅讲解了 JavaScript/TypeScript 代码的打包,而一个 Web 项目必然缺不了 HTML 文件,接下来我们讲解 HTML 资源的打包。
Webpack 打包 HTML 依赖于 html-webpack-plugin 插件,首先安装插件。
1
| npm install -D html-webpack-plugin
|
然后修改 webpack.config.js 以启用该插件,并指定对应的 HTML 文件模板和 favicon 图标文件地址。(这里 favicon 图标可以自行准备,或移除配置)
1
2
3
4
5
6
7
8
9
10
11
| // webpack.config.js
import HtmlWebpackPlugin from 'html-webpack-plugin';
export default {
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
favicon: resolve('./assets/favicon.svg'),
}),
],
};
|
接下来,编写 HTML 文件。
1
2
3
4
5
6
7
8
9
10
11
12
| <!-- index.html -->
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Webpack Demo</title>
</head>
<body>
<h1>Webpack Demo</h1>
</body>
</html>
|
需要注意,我们在 HTML 文件中并没有引用相关的 JavaScript 文件,也没有引用 Webpack 配置中指定的 favicon 文件。在生成产物的时候,Webpack 会自动在 HTML 中注入对应的依赖。
1
2
| <link rel="icon" href="favicon.svg" />
<script defer="defer" src="index.js"></script>
|
此时通过 npx webpack –mode=production 构造完产物后,再通过 serve 部署产物,可以发现 HTML 文件已被正常打包,并正确引用了对应的 favicon 文件和 JavaScript 文件,浏览器控制台也按预期输出数字 3。
CSS 样式打包
引入原生 CSS
CSS 样式的打包也是 Web 项目的重要一环,虽然我们可以通过 HTML 行内样式或者 JavaScript 来绘制各种组件的样式,但单独封装成一个 CSS 文件更有利于维护和样式复用。
与 HTML 类似,Webpack 也是通过插件的方式实现 CSS 文件打包。这里我们使用 css-loader 和 mini-css-extract-plugin 两个插件,前者使得我们可以在 JavaScript 或 TypeScript 文件中引用 CSS 文件,后者允许我们将样式都封装到一个独立的 CSS 文件中。
首先安装插件。
1
| npm install -D css-loader mini-css-extract-plugin
|
然后修改 Webpack 配置以支持 CSS 文件打包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // webpack.config.js
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
export default {
plugins: [
new MiniCssExtractPlugin(),
],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
}
]
},
};
|
然后在 index.ts 中引用 CSS 文件。
1
2
3
4
| /* index.css */
h1 {
color: red;
}
|
1
2
| // index.ts
import './index.css';
|
此时再执行 Webpack 打包,会发现 HTML 文件中已经添加上对 CSS 文件的引用。
1
| <link href="main.css" rel="stylesheet" />
|
但是可以发现,目前 Webpack 打包的 CSS 产物并没有类似 HTML 和 JavaScript 一样被压缩,我们可以通过插件 css-minimizer-webpack-plugin 进行压缩。
1
| npm install -D css-minimizer-webpack-plugin
|
1
2
3
4
5
6
7
8
| // webpack.config.js
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
export default {
optimization: {
minimizer: [`...`, new CssMinimizerPlugin()],
},
};
|
引入 LESS
原生 CSS 语法相对简单,无法用简单的语句实现较为复杂的场景,LESS 和 SASS 是两种兼容 CSS 的扩展语言,在 CSS 的基础上增加了不少实用的扩展,因此被广泛用于 Web 开发。这里仅介绍 LESS 的引入,SASS 类同。
首先安装 less 和对应的插件。
1
| npm install -D less less-loader
|
然后修改 Webpack 配置。
1
2
3
4
5
6
7
8
9
10
11
| // webpack.config.js
export default {
module: {
rules: [
{
test: /\.less$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
},
]
},
};
|
然后实现和引用 LESS 文件,即可将 LESS 文件最终打包成 CSS 文件。
/* index.less */
body {
h1 {
color: red;
}
}
1
2
| // index.ts
import './index.less';
|
资源打包
Webpack 提供了三种资源模块类型用于打包资源。
- asset/resource:打包单独的文件并导出 URL;
- asset/inline:导出一个资源的 data URI;
- asset/source:导出资源的源代码。
这里以 svg 资源为例,我们试着用三种类型打包 svg 文件。首先修改 Webpack 配置以支持 svg 资源打包。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // webpack.config.js
export default {
module: {
rules: [
{
test: /\.svg$/i,
type: 'asset/resource', // 指定资源类型
// 这里可以指定打包资源的文件格式, 仅 asset/resource 能指定该值
generator: { filename: 'assets/[name][ext]' },
}
]
},
};
|
然后在 JavaScript 或 TypeScript 文件中引用 svg 文件即可对指定资源进行打包。
1
2
3
4
5
6
| // 引用文件, 打包时会产出资源文件
import './assets/test.svg';
// 引用文件, 打包时会产出资源文件, svg 值为资源文件的路径
import svg from './assets/test/svg';
console.log(svg); // => http://localhost:3000/assets/test.svg
|
如果你使用的是 TypeScript,引用 svg 文件时可能编辑器会报错,这是因为编辑器无法将 svg 文件识别为一个正常的 module,只需要我们添加一个 d.ts 文件即可解决该问题。
1
2
3
4
5
| // global.d.ts
declare module '*.svg' {
const src: string;
export default src;
}
|
以上为 asset/resource 的情况,如果指定资源为 asset/inline 或者 asset/source 类型时,将不再产出最终资源文件,而是以内联的形式存在。
1
2
3
4
5
6
7
| // asset/inline
import svg from './assets/test/svg';
console.log(svg); // => data:image/svg+xml;base64,xxxxxx
// asset/source
import svg from './assets/test/svg';
console.log(svg); // => <svg>xxxxxx</svg>
|
调试环境
在以上的所有例子中,我们都是通过 npx webpack –mode=production 命令使用 Webpack 生成最终产物,而通常开发时我们更需要一个调试环境,能够监听本地源代码修改并实时更新页面,以提高开发效率。
Webpack 通过插件 webpack-dev-server 提供了类似的能力,首先安装对应的插件。
1
| npm install -D webpack-dev-server
|
然后修改 Webpack 配置。
1
2
3
4
5
6
7
8
9
10
| // webpack.config.js
export default {
devServer: {
compress: true,
hot: true,
open: true,
port: 3000,
watchFiles: ['*'], // 监听根目录所有文件修改
}
};
|
然后通过 Webpack CLI 启用开发服务器,这时候我们再修改本地代码,Webpack 会重新触发打包并刷新浏览器。
1
| npx webpack serve --mode=development
|
结语
本文通过从零开始搭建一个 Web 项目开发和构建环境,来介绍 Webpack 的使用。受限于本人的水平,可能内容比较浅层,并没有涉及 Webpack 的原理或更复杂的使用场景。
其实大学的时候有简单接触过 Webpack,但更多还是基于 Vue、React 一些现成的框架进行开发和构建,没有过多接触和修改 Webpack 相关的配置,写这篇文章也算是给予我一个机会从初学者的角度去好好了解 Webpack 的定位、使用方法等细节吧。