用搬家公司的例子来入门webpack
📚 官方文档:Webpack 官网 | Webpack 中文文档
🚀 实践项目:webpack-simple-demo - 本文档的完整示例项目
Webpack 主要是干什么的?
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler
)。简单来说,它就是把你的项目中的各种文件(TS
、Less
、JS
、CSS
、图片等)打包成一个或多个浏览器可以直接使用的文件。特别像搬家公司,把你的东西打包进各种盒子里。
英文中"bundle
"意为"捆扎、打包",形象地描述了将多个松散文件整合为单一或少量文件的过程。
想象你打包东西,也是一样,把一堆东西打包成一个或多个包装。
核心功能(用简单的话理解)- 打包、依赖、转换、优化
1. 模块打包 📦
- 作用:把分散的多个文件合并成少数几个文件
- 比喻:就像把一堆散乱的积木组装成一个完整的模型
- 实际效果:原本 100 个 JS 文件 → 打包后变成 2-3 个文件
2. 依赖管理 🔗
- 作用:自动处理文件之间的依赖关系
- 比喻:就像一个智能管家,知道先加载哪个文件,后加载哪个文件
-
实际效果:你不用手动管理
<script>
标签的顺序
3. 资源转换 🔄
- 作用:把各种格式的文件转换成浏览器能理解的格式
-
例子:
- Sass/SCSS → CSS
- TypeScript → JavaScript
- ES6+ → ES5(兼容老浏览器)
- 图片优化和压缩
4. 代码优化 ⚡
- 作用:压缩代码、删除无用代码、合并重复代码
- 效果:文件变小,加载更快
用生活例子理解:
想象你要搬家:
Webpack
= 搬家公司- 你的文件 = 家里的物品
- 打包过程 = 整理装箱
- 最终结果 = 整齐的搬家箱子
搬家公司会:
- 把散乱的物品整理好(
模块打包
) - 知道哪些物品要一起放(
依赖管理
) - 把大件拆解成小件方便运输(
格式转换
) - 用最少的箱子装最多的东西(
代码优化
)
Webpack 核心概念详解
1. Entry(入口) 🚪
-
定义:告诉
Webpack
从哪个文件开始打包 - 搬家比喻:就像搬家公司问"从哪个房间开始收拾?"
-
实际例子:
src/index.js
就是入口,Webpack
从这里开始分析依赖 - 重要性:入口决定了整个依赖图的起点
2. Output(输出) 📦
-
定义:告诉
Webpack
把打包后的文件放在哪里 - 搬家比喻:就像告诉搬家公司"把箱子都放到新家的客厅"
-
实际例子:
dist/bundle.js
就是输出文件 - 配置:可以设置文件名、路径、公共路径等
3. Loaders(加载器) 🔧
-
定义:告诉
Webpack
如何处理不同类型的文件 -
搬家比喻:就像搬家公司有专门的工具处理不同物品
- 易碎品用泡沫包装(
CSS Loader
) - 大件家具要拆解(
Babel Loader
) - 衣服要折叠压缩(
Terser Loader
)
- 易碎品用泡沫包装(
-
常见 Loaders:
-
babel-loader
:处理JavaScript
-
css-loader
:处理CSS
-
file-loader
:处理图片、字体等 -
sass-loader
:处理Sass/SCSS
-
4. Plugins(插件) 🛠️
-
定义:扩展
Webpack
功能的工具 -
搬家比喻:就像搬家公司提供的增值服务
- 清洁服务(
HtmlWebpackPlugin
) - 保险服务(压缩优化插件)
- 监控服务(开发服务器)
- 清洁服务(
-
常见 Plugins:
-
HtmlWebpackPlugin
:自动生成HTML
-
MiniCssExtractPlugin
:提取CSS
-
CleanWebpackPlugin
:清理输出目录
-
5. Mode(模式) ⚙️
-
定义:告诉
Webpack
使用哪种模式(开发/生产) - 搬家比喻:就像告诉搬家公司"这是临时搬家还是永久搬家"
- 开发模式:快速打包,保留调试信息
- 生产模式:优化打包,压缩代码
6. Module(模块) 📄
- 定义:项目中的每个文件都是一个模块
- 搬家比喻:就像家里的每个房间、每件物品
- 特点:模块之间可以有依赖关系
-
处理:
Webpack
会分析模块间的依赖关系
7. Chunk(代码块) 🧩
- 定义:打包后的代码块
- 搬家比喻:就像整理后的搬家箱子
-
类型:
- Entry Chunk:入口代码块
- Vendor Chunk:第三方库代码块
- Common Chunk:公共代码块
8. Dependency Graph(依赖图) 🕸️
- 定义:模块之间的依赖关系图
- 搬家比喻:就像搬家前的物品清单和摆放关系图
-
作用:
Webpack
根据依赖图决定打包顺序 -
特点:有向无环图(
DAG
)
搬家比喻完整版 🏠
想象你要搬家,Webpack
就是专业的搬家公司:
Webpack 概念 |
搬家比喻 | 具体说明 |
---|---|---|
Entry |
从哪个房间开始收拾 | 告诉搬家公司从主卧开始 |
Output |
箱子放到新家哪里 | 所有箱子都放到客厅 |
Loaders |
专业工具处理不同物品 | 易碎品用泡沫,大件要拆解 |
Plugins |
增值服务 | 清洁、保险、监控服务 |
Mode |
搬家类型 | 临时搬家 vs 永久搬家 |
Module |
家里的每个房间/物品 | 厨房、卧室、客厅等 |
Chunk |
整理后的搬家箱子 | 按房间分类的箱子 |
Dependency Graph |
物品摆放关系图 | 知道哪些物品要一起搬 |
构建项目 - 理解 webpack
💡 完整示例:本文档的所有代码示例都可以在 webpack-simple-demo 项目中找到
mkdir webpack-simple-demo
cd webpack-simple-demo
pnpm init -y
package.json
增加脚本
{
"name": "webpack-simple-demo",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"build:prod": "webpack --mode=production",
"dev": "webpack --mode=development"
},
"keywords": ["webpack"],
"license": "MIT",
"devDependencies": {
"webpack": "^5.44.0",
"webpack-cli": "^4.7.2"
}
}
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
src/
目录结构
src/double.js
function double(a) {
return a * 2;
}
module.exports = {
double,
};
src/sumDouble.js
const { double } = require('./double');
function sumDouble(a, b) {
return double(a + b);
}
module.exports = {
sumDouble,
};
src/index.js
const { sumDouble } = require('./sumDouble');
console.log(sumDouble(1, 2));
var indexExport = 'indexExport';
module.exports = {
indexExport,
};
运行构建
pnpm install
pnpm run build
那么会生成一个 dist/bundle.js
文件,这个文件是打包后的文件,包含了所有的依赖和模块。
分析 dist/bundle.js
文件
bundle.js
文件是一个自执行函数,包含了三个主要部分:
-
模块定义区域,存储了所有模块(有导出内容的)的代码,以键值对的形式存储。键是模块路径,值是函数,函数接收三个参数,分别是模块对象,导出对象,
require
函数,函数内部是模块的代码。 -
模块缓存系统,存储了所有模块的缓存结果,以键值对的形式存储。键是模块路径,值是对象,对象的结构是
{ exports: {} }
,exports
是模块的导出对象。 -
入口模块执行,执行入口模块的代码,会调用
require
函数,require
函数会调用模块缓存系统,如果模块缓存系统中存在,则直接返回缓存结果,否则会调用模块定义区域,执行模块的代码,设置模块的导出对象,然后缓存结果,最后返回模块的导出对象。
这里注意,src
中的模块里 require
语句会替换为 __webpack_require__
函数。__webpack_require__
函数是 Webpack
自己实现的 require
函数,用来替代 Node.js
的 require
。它负责加载和管理模块。功能是:检查缓存、创建模块、执行模块、返回导出对象。
(() => {
// 1.模块定义区域
var __webpack_modules__ = {
'./src/double.js': (module) => {
function double(a) {
return a * 2;
}
module.exports = {
double,
};
},
'./src/index.js': (
module,
__unused_webpack_exports,
__webpack_require__
) => {
// require 语句被替换为 __webpack_require__ 函数
const { sumDouble } = __webpack_require__('./src/sumDouble.js');
console.log(sumDouble(1, 2));
var indexExport = 'indexExport';
module.exports = {
indexExport,
};
},
'./src/sumDouble.js': (
module,
__unused_webpack_exports,
__webpack_require__
) => {
const { double } = __webpack_require__('./src/double.js');
function sumDouble(a, b) {
return double(a + b);
}
module.exports = {
sumDouble,
};
},
};
// 2.模块缓存系统
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
// 1.检查缓存
var cachedModule = __webpack_module_cache__[moduleId];
// 2.如果缓存中存在,则直接返回缓存模块的导出对象
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 3.如果缓存中不存在,则创建新的模块对象
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// 4.执行模块的代码
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 5.返回模块的导出对象
return module.exports;
}
// 3.入口模块执行
var __webpack_exports__ = __webpack_require__('./src/index.js');
})();
代码执行逻辑详细分析 🔍
🎯 整体执行流程
这段代码的执行可以分为以下几个关键阶段:
第1阶段:初始化阶段
(() => {
// 1.模块定义区域
var __webpack_modules__ = { ... };
// 2.模块缓存系统
var __webpack_module_cache__ = {};
作用:创建模块定义对象和缓存系统,为后续模块加载做准备。
第 2 阶段:入口模块加载
var __webpack_exports__ = __webpack_require__('./src/index.js');
作用:从入口模块开始执行,触发整个依赖链的加载。
🔄 详细执行步骤分析
步骤 1:调用入口模块
__webpack_require__('./src/index.js');
执行过程:
-
检查缓存:
__webpack_module_cache__['./src/index.js']
不存在 -
创建模块:
module = { exports: {} }
-
执行模块:调用
__webpack_modules__['./src/index.js'](module, module.exports, __webpack_require__)
步骤 2:index.js
模块执行
// index.js 模块代码
const { sumDouble } = __webpack_require__('./src/sumDouble.js');
console.log(sumDouble(1, 2));
var indexExport = 'indexExport';
module.exports = { indexExport };
执行过程:
-
遇到 require:
__webpack_require__('./src/sumDouble.js')
被调用 -
暂停执行:
index.js
暂停,等待sumDouble.js
加载完成
步骤 3:sumDouble.js
模块加载
// sumDouble.js 模块代码
const { double } = __webpack_require__('./src/double.js');
function sumDouble(a, b) {
return double(a + b);
}
module.exports = { sumDouble };
执行过程:
-
检查缓存:
__webpack_module_cache__['./src/sumDouble.js']
不存在 -
创建模块:
module = { exports: {} }
-
遇到 require:
__webpack_require__('./src/double.js')
被调用 -
暂停执行:
sumDouble.js
暂停,等待double.js
加载完成
步骤 4:double.js
模块加载
// double.js 模块代码
function double(a) {
return a * 2;
}
module.exports = { double };
执行过程:
-
检查缓存:
__webpack_module_cache__['./src/double.js']
不存在 -
创建模块:
module = { exports: {} }
-
执行模块:定义
double
函数 -
设置导出:
module.exports = { double }
-
返回结果:返回
{ double }
给sumDouble.js
步骤 5:sumDouble.js
继续执行
// 现在 double 函数已经可用
function sumDouble(a, b) {
return double(a + b); // double(1 + 2) = double(3) = 6
}
module.exports = { sumDouble };
执行过程:
-
继续执行:
double
函数已经加载完成 -
定义函数:
sumDouble
函数被定义 -
设置导出:
module.exports = { sumDouble }
-
返回结果:返回
{ sumDouble }
给index.js
步骤 6:index.js
继续执行
// 现在 sumDouble 函数已经可用
console.log(sumDouble(1, 2)); // 输出:6
var indexExport = 'indexExport';
module.exports = { indexExport };
执行过程:
-
继续执行:
sumDouble
函数已经加载完成 -
调用函数:
sumDouble(1, 2)
执行,返回 6 -
输出结果:
console.log(6)
输出 6 -
设置导出:
module.exports = { indexExport }
-
返回结果:返回
{ indexExport }
给入口
🏠 搬家比喻完整版
想象这是一个三层楼的搬家过程:
阶段 | 搬家比喻 | 代码执行 | 状态 |
---|---|---|---|
初始化 | 准备搬家工具 | 创建模块定义和缓存 | 准备就绪 |
开始搬家 | 从 3 楼开始 | 调用 __webpack_require__('./src/index.js')
|
开始执行 |
3 楼需要 2 楼 | 3 楼需要客厅的东西 | require('./src/sumDouble.js') |
暂停 3 楼 |
2 楼需要 1 楼 | 客厅需要厨房的东西 | require('./src/double.js') |
暂停 2 楼 |
1 楼执行 | 厨房先收拾 |
double.js 执行完成 |
1 楼完成 |
2 楼继续 | 客厅拿到厨房的东西 |
sumDouble.js 执行完成 |
2 楼完成 |
3 楼继续 | 3 楼拿到客厅的东西 |
index.js 执行完成 |
3 楼完成 |
搬家完成 | 所有楼层都收拾好 | 输出结果:6 | 全部完成 |
🔍 关键执行特点
- 同步执行
- 模块加载是同步的,必须等待依赖模块加载完成
- 不会出现异步加载的情况
- 依赖链式加载
index.js → sumDouble.js → double.js
- 从最底层开始,逐层向上返回
- 缓存机制
- 每个模块只执行一次
- 后续调用直接返回缓存结果
- 作用域隔离
- 每个模块都有独立的作用域
- 通过
module.exports
和require
通信
💡 执行结果
最终执行结果:
-
控制台输出:
6
-
模块导出:
{ indexExport: 'indexExport' }
- 缓存状态:三个模块都被缓存
这段代码展示了 Webpack
模块系统的核心机制:
- ✅ 模块化:每个模块独立执行
- ✅ 依赖管理:自动处理模块间的依赖关系
- ✅ 缓存优化:避免重复加载
- ✅ 同步执行:确保依赖关系正确
搬家比喻:就像三层楼的搬家,从顶层开始,逐层向下收集需要的东西,最后完成整个搬家过程!🏠
输出的文件名配置
真实的项目中,output
的输出文件名配置通常是动态的,根据入口文件的名称和哈希值来生成。
而哈希值有三种类型:
-
hash
:基于整个项目的构建内容生成全局唯一哈希值,同一次构建过程中所有文件的hash
值相同,适用于开发环境或需要强制更新所有资源的场景 -
chunkhash
:根据入口文件(chunk
)及其依赖模块的内容生成独立哈希值,不同入口文件的chunkhash
互不影响,用于入口JS
文件命名,例如[name].[chunkhash].js
-
contenthash
:基于文件自身内容生成哈希值,仅当文件内容变化时才会更新,适用于抽离的CSS
文件或静态资源,避免因关联文件修改导致不必要的缓存失效,例如[name].[contenthash].css
entry: {
page1: './src/page1.js',
page2: './src/page2.js',
},
output: {
filename: '[name].[chunkhash:8].bundle.js',
path: path.resolve(__dirname, 'dist'),
}
src
建立四个文件,2 个入口文件,2 个依赖文件:
// src/page1.js
var page1 = require('./page1-a');
console.log('page1');
console.log(page1);
// src/page2.js
var page2 = require('./page2-a');
console.log('page2');
console.log(page2);
// src/page1-a.js
module.exports = 'page1-a';
// src/page2-a.js
module.exports = 'page2-a';
执行 build
之后:
// dist/page1.18656d4d.bundle.js
// dist/page2.18656d4d.bundle.js
如果只有 page1.js
进行修改,那么 page1.bundle.js
的哈希值会变化,而 page2.bundle.js
的哈希值不会变化。
loader
的配置 - 文件类型转换
📖 官方文档:Webpack Loaders | Loader API
Webpack
中的 Loader
是用于将非 JavaScript
模块(如 CSS
、图片、TypeScript
等)转换为 Webpack
能处理的模块的工具。它们像"翻译员"一样,通过链式调用对文件进行多步转换。
webpack-simple-demo
项目中,我们使用了 ts-loader
来将 ts
文件转换为 js
文件。
webpack.config.js
中,配置了 extensions
和 module.rules
。extensions
配置了文件的扩展名,module.rules
配置了 loader
的规则。rules
中,我们配置了 ts-loader
来将 ts
文件转换为 js
文件。test
属性是正则表达式,用于匹配文件。use
属性是 loader
的名称。exclude
属性是排除的文件。include
属性是包含的文件。include
和 exclude
是互斥的,只能配置一个。
先配置 package.json
:
"devDependencies": {
"typescript": "^5.0.0",
"ts-loader": "^9.4.0",
"@types/node": "^20.0.0",
}
然后配置 webpack.config.js
:
// 模块解析
resolve: {
extensions: ['.ts', '.js'],
},
// 模块规则
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
src
的文件结构如下:
// src/page1.ts
import page1A from './page1-a';
console.log(page1A);
// src/page1-a.ts
export default 'page1-a';
// src/page2.ts
import page2A from './page2-a';
console.log(page2A);
// src/page2-a.ts
export default 'page2-a';
先执行 pnpm install
,再执行 npm run build
之后:
// dist/page1.95962465.bundle.js
// dist/page2.18656d4d.bundle.js
// dist/page1-a.d.ts
// dist/page2-a.d.ts
// dist/page1.d.ts
// dist/page2.d.ts
这里每个 ts
文件都生成了一个 d.ts
文件,这个文件是类型定义文件,用来定义模块的类型。
如果一个文件需要多个 loader
解析,比如 less
文件,那么需要配置多个 loader
,且 loader
的执行顺序是从右到左,也就是数组的最后一个 loader
先执行:
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
}
这里我们配置了三个 loader
,分别是 style-loader
、css-loader
和 less-loader
:
-
less-loader
用来将Less
语法转换为普通CSS
,例如:@color: red;
→color: red;
-
css-loader
处理转换后的css
文件,解析@import
语句,处理url(./image.png)
等资源路径,若启用modules
,会生成类名哈希(如.class_1234
) -
style-loader
将最终CSS
代码通过JavaScript
动态插入DOM
:document.head.innerHTML += '<style>body { color: red; }</style>';
plugin
的配置
📖 官方文档:Webpack Plugins | Plugin API
Webpack
中的 plugin
(插件)是用于扩展 Webpack
功能的模块,通过在构建流程中注入钩子来实现资源处理、优化等自定义任务。与 loader
(专注于文件转换)不同,plugin
作用于 Webpack
的整个编译生命周期,能够执行更广泛的任务,如打包优化、资源管理、环境变量注入等。
webpack-simple-demo
项目中,我们使用了 HtmlWebpackPlugin
来举例说明 plugin
的配置。
HtmlWebpackPlugin
是 Webpack
生态中的核心插件,主要用于自动生成 HTML
文件并管理打包资源的注入。
先配置 package.json
:
"devDependencies": {
"html-webpack-plugin": "^5.5.3",
}
然后配置 webpack.config.js
:
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
// 为 page1 生成 HTML
new HtmlWebpackPlugin({
template: './src/templates/page1.html', // 模板文件
filename: 'page1.html', // 输出文件名
chunks: ['page1'], // 需要注入到当前HTML的JS模块
title: 'Page1', // 标题
meta: { // 元数据
viewport: 'width=device-width, initial-scale=1',
description: 'Page1 页面'
},
inject: 'body', // 资源注入位置
minify: { // 生产环境压缩
removeComments: true,
collapseWhitespace: true
}
}),
// 为 page2 生成 HTML
new HtmlWebpackPlugin({
template: './src/templates/page2.html',
filename: 'page2.html',
chunks: ['page2'],
title: 'Page2',
meta: {
viewport: 'width=device-width, initial-scale=1',
description: 'Page2 页面'
},
inject: 'body',
minify: {
removeComments: true,
collapseWhitespace: true
}
})
],
然后在 src
里 templates
里建立两个 html
文件:
// src/templates/page1.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= htmlWebpackPlugin.options.title %></title>
<meta
name="description"
content="<%= htmlWebpackPlugin.options.meta.description %>"
/>
</head>
<body>
<div class="container">
<h1>Page1</h1>
<div class="nav">
<a href="page1.html">Page1</a>
<a href="page2.html">Page2</a>
</div>
</div>
</body>
</html>
// src/templates/page2.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= htmlWebpackPlugin.options.title %></title>
<meta
name="description"
content="<%= htmlWebpackPlugin.options.meta.description %>"
/>
</head>
<body>
<div class="container">
<h1>Page2</h1>
<div class="nav">
<a href="page1.html">Page1</a>
<a href="page2.html">Page2</a>
</div>
</div>
</body>
</html>
先执行 pnpm install
,再执行 npm run build
之后:
// dist/page1.html 里面自动注入<script defer src="page1.c4e91d29.bundle.js"></script>
// dist/page2.html 里面自动注入<script defer src="page2.c4e91cb1.bundle.js"></script>
这里生成了两个 HTML
文件,分别是 page1.html
和 page2.html
。可以拖动浏览器查看效果。
开发服务器
📖 官方文档:Webpack Dev Server | Dev Server API
Webpack
的开发服务器是用于在开发过程中提供实时预览和调试功能的工具。它允许开发者在浏览器中实时查看代码变化,并提供自动刷新和热更新功能,从而加快开发速度和提升开发体验。
webpack-simple-demo
项目中,我们使用了 webpack-dev-server
来举例说明开发服务器的配置。
先配置 package.json
:
"scripts": {
"serve": "webpack serve --config webpack.config.js"
},
"devDependencies": {
"webpack-dev-server": "^4.15.1",
}
然后配置 webpack.config.js
:
module.exports = {
// 开发服务器配置
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
compress: true,
port: 3000,
hot: true,
open: true,
historyApiFallback: {
rewrites: [
{ from: '/', to: '/page1.html' },
{ from: /^\/page1/, to: '/page1.html' },
{ from: /^\/page2/, to: '/page2.html' },
],
},
},
};
然后执行 npm run serve
之后,浏览器会自动打开 http://localhost:3000
就能看到页面了。
配置区别
一般开发环境配置和生产环境配置的配置是不同的,比如开发环境配置会包含开发服务器配置,而生产环境配置会包含代码压缩配置。
webpack-simple-demo
项目中,我们使用了 webpack-merge
来举例说明配置合并的配置。
先配置 package.json
:
"devDependencies": {
"webpack-merge": "^5.8.0",
}
然后配置 webpack.config.base.js
:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口文件
entry: {
page1: './src/page1.ts',
page2: './src/page2.ts',
},
// 模块解析
resolve: {
extensions: ['.ts', '.js'],
},
// 模块规则
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
// 插件配置
plugins: [
// 为 page1 生成 HTML
new HtmlWebpackPlugin({
template: './src/templates/page1.html',
filename: 'page1.html',
chunks: ['page1'],
title: 'Page1 - TypeScript Webpack Demo',
meta: {
viewport: 'width=device-width, initial-scale=1',
description: 'Page1 页面 - TypeScript Webpack 示例',
},
}),
// 为 page2 生成 HTML
new HtmlWebpackPlugin({
template: './src/templates/page2.html',
filename: 'page2.html',
chunks: ['page2'],
title: 'Page2 - TypeScript Webpack Demo',
meta: {
viewport: 'width=device-width, initial-scale=1',
description: 'Page2 页面 - TypeScript Webpack 示例',
},
}),
],
};
然后配置 webpack.config.dev.js
:
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.config.base');
// @ts-ignore
module.exports = merge(baseConfig, {
// 模式
mode: 'development',
// 开发工具
devtool: 'eval-cheap-module-source-map',
// 输出配置(开发环境)
output: {
filename: '[name].bundle.js',
path: require('path').resolve(__dirname, 'dist'),
clean: true,
},
// 开发服务器配置
devServer: {
static: {
directory: require('path').join(__dirname, 'dist'),
},
compress: true,
port: 3000,
hot: true,
open: true,
historyApiFallback: {
rewrites: [
{ from: '/', to: '/page1.html' },
{ from: /^\/page1/, to: '/page1.html' },
{ from: /^\/page2/, to: '/page2.html' },
],
},
},
// 开发环境优化
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: 10,
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true,
},
},
},
},
plugins: [],
});
然后配置 webpack.config.prod.js
:
const { merge } = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const baseConfig = require('./webpack.config.base');
// @ts-ignore
module.exports = merge(baseConfig, {
// 模式
mode: 'production',
// 开发工具
devtool: 'source-map',
// 输出配置(生产环境)
output: {
filename: '[name].[contenthash:8].bundle.js',
path: require('path').resolve(__dirname, 'dist'),
clean: true,
},
// 生产环境优化
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info', 'console.debug'],
},
mangle: {
safari10: true,
},
},
extractComments: false,
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: 10,
enforce: true,
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true,
enforce: true,
},
},
},
},
// 性能配置
performance: {
hints: 'warning',
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
});
开发环境的主要特性:
-
模式:使用
development
-
开发工具:使用
eval-cheap-module-source-map
-
输出配置:使用
[name].bundle.js
- 热更新:支持代码修改后自动刷新
- 开发服务器:内置开发服务器
- 路由支持:支持单页应用路由
- 自动打开:启动后自动打开浏览器
这样构建出来的文件:文件不压缩,便于调试;包含源码映射;快速构建;文件大小较大。
生产环境的主要特性:
-
模式:使用
production
-
开发工具:使用
source-map
-
输出配置:使用
[name].[contenthash:8].bundle.js
-
代码压缩:使用
TerserPlugin
-
文件哈希:使用
contenthash
-
性能优化:使用
splitChunks
-
源码映射:使用
source-map
-
控制台清理:移除
console.log
等调试代码
这样构建出来的文件:代码压缩,文件更小;移除调试代码;文件哈希命名;性能优化。
开发一个自己的 loader
- md
转 html
📖 官方文档:Writing a Loader | Loader Examples
💡 实践示例:查看 webpack-simple-demo 中的
md2html-loader.js
实现
loader
本质是一个导出函数的 Node.js
模块,接收源文件内容并返回 js
代码字符串或 Buffer
类型数据。
基本模板如下:
module.exports = function (source) {
// source里最后含有module.exports=xx
return source;
};
现在想实现,能通过 import xx from './xx.md'
的方式引入 md
文件,并转换为 html
代码,然后插入到 html
文件中。
首先写一个 md2html-loader.js
:
// 简单的 Markdown 解析器(基础功能)
function simpleMarkdownParser(text) {
return (
text
// 标题
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// 粗体
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
// 斜体
.replace(/\*(.*)\*/gim, '<em>$1</em>')
// 代码块
.replace(/```([\s\S]*?)```/gim, '<pre><code>$1</code></pre>')
// 行内代码
.replace(/`([^`]*)`/gim, '<code>$1</code>')
// 链接
.replace(/\[([^\]]*)\]\(([^)]*)\)/gim, '<a href="$2">$1</a>')
// 换行
.replace(/\n/gim, '<br>')
);
}
module.exports = function (source) {
const options = this.getOptions() || {};
console.log('获取use的options配置', options);
// 解析 Markdown 为 HTML
const html = simpleMarkdownParser(source);
// 开发环境下输出转换结果
if (process.env.NODE_ENV === 'development') {
console.log('📝 Markdown 转换结果:');
console.log('原文:', source);
console.log('HTML:', html);
}
// 返回 JavaScript 模块
return `module.exports = ${JSON.stringify(html)}`;
};
然后配置 webpack.config.js
:
module.exports = {
module: {
rules: [
{
test: /\.md$/,
use: {
loader: path.resolve(__dirname, 'md2html-loader'),
options: {
breaks: true, // 支持换行符
headerIds: true, // 生成标题 ID
mangle: false, // 不混淆 HTML 属性
},
},
exclude: /node_modules/,
},
],
},
};
然后再 src
里创建一个 page1-a.md
文件:
# page1-a
我是 page1-a 文件
再在 page1.js
文件中引入 page1-a.md
文件:
// page1 入口文件
import page1A from './page1-a';
import page1Md from './page1-a.md';
function renderMd() {
const div = document.createElement('div');
div.innerHTML = page1Md;
document.body.appendChild(div);
}
renderMd();
console.log(page1A);
然后执行 npm run serve
之后,就能在浏览器看到 md
文件的内容了。
开发 plugin
的前置知识
📖 官方文档:Writing a Plugin | Plugin API
💡 实践示例:查看 webpack-simple-demo 中的
file-list-plugin.js
实现
plugin
本质是一个导出函数的 Node.js
模块,接收 Compiler
对象,然后通过 Compiler
对象的钩子函数,在构建过程中执行自定义任务。
基本模板如下:
class MyPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
const hooks = compiler.hooks;
// compile编译开始时触发
hooks.compile.tap('MyPlugin', (compilation, callback) => {});
// emit生成资源到输出目录前
hooks.emit.tap('MyPlugin', (compilation, callback) => {
console.log('MyPlugin');
});
// done编译完成后触发
hooks.done.tap('MyPlugin', (compilation, callback) => {});
}
}
module.exports = MyPlugin;
使用的时候:
const MyPlugin = require('./my-plugin');
module.exports = {
plugins: [new MyPlugin({ text: '1' })],
};
compiler
和 compilation
compiler
和 compilation
是 webpack
的两大核心对象,它们在插件开发中扮演着重要角色。
compiler
compiler
:代表 Webpack
的完整运行环境,从启动到关闭全程存在,仅实例化一次,包含全局配置(如 options
、loaders
、plugins
)和生命周期钩子(如 run
、done
),插件常通过 compiler.hooks
监听构建事件(如修改配置、触发新编译)
compiler
的核心属性:
-
options
:包含webpack
配置(如mode
、entry
、output
) -
loaders
:包含所有loader
配置 -
plugins
:包含所有plugin
配置 -
hooks
:包含所有钩子函数,插件常通过compiler.hooks
监听构建事件,常用的钩子函数有:compile
、emit
、done
,详见:webpack.js.org/api/compile…
compiler
的核心方法:
-
run
:启动编译 -
watch
:监听文件变化 -
compile
:触发编译 -
make
:创建compilation
-
emit
:触发资源输出 -
done
:编译完成
compilation
compilation
:代表单次编译过程,每次文件变动或重新构建时创建新实例,包含模块依赖图(modules
)、代码分块(chunks
)和输出资源(assets
)等动态数据,插件通过 compilation
操作具体模块(如 emitAsset
添加文件)或优化编译结果
compilation
的核心属性:
-
modules
:包含所有模块依赖图 -
chunks
:包含所有代码分块 -
assets
:包含所有输出资源
compilation
的核心方法:
-
addEntry
:添加入口 -
addChunk
:添加代码分块 -
addAsset
:添加输出资源 -
emitAsset
:触发资源输出 -
done
:编译完成
联系
当文件修改触发重新构建时:
-
Compiler
持续存在,调用compile()
方法 - 创建新的
Compilation
实例处理模块解析和资源生成 - 通过
compilation.assets
修改输出内容后,由Compiler
完成最终写入
这种设计实现了配置与编译状态的隔离,确保 watch
模式下仅需重建 Compilation
而非整个环境。
开发一个自己的 plugin
💡 实践示例:查看 webpack-simple-demo 中的
file-list-plugin.js
完整实现
现在想实现,输出一个 md
文件,里面的内容是打包的文件数量和文件的 list
。
首先写一个 file-list-plugin.js
:
class FileListPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
const hooks = compiler.hooks;
// emit生成资源到输出目录前
hooks.emit.tap('FileListPlugin', (compilation, callback) => {
const fileNames = Object.keys(compilation.assets);
const content = fileNames.map((filename) => `- ${filename}`).join('\n');
compilation.assets[this.options.outputFile] = {
source: function() {
return `文件数量是 ${fileNames.length},\n文件列表是:\n${content}`;
},
size: function() {
return Buffer.byteLength(this.source(), 'utf8');
}
};
callback && callback();
});
}
}
module.exports = FileListPlugin;
然后配置 webpack.config.js
:
module.exports = {
plugins: [new FileListPlugin({ outputFile: 'assets.md' })],
};
然后执行 npm run build:dev
之后,就能在 dist
目录下看到 assets.md
文件了。
📚 相关资源
官方文档
- Webpack 官网 - 官方英文文档
- Webpack 中文文档 - 官方中文文档
- Webpack 概念 - 核心概念详解
- Webpack 配置 - 配置选项说明
核心概念文档
- Entry 和 Output - 入口和输出配置
- Loaders - 加载器概念
- Plugins - 插件系统
- Mode - 开发/生产模式
开发工具
- Webpack Dev Server - 开发服务器
- Source Maps - 源码映射
- Hot Module Replacement - 热模块替换
高级主题
- Code Splitting - 代码分割
- Tree Shaking - 摇树优化
- Caching - 缓存策略
- Performance - 性能优化
实践项目
-
webpack-simple-demo - 本文档的完整示例项目
- 包含所有代码示例
- 自定义 Loader 和 Plugin 实现
- 完整的 Webpack 配置
- TypeScript 支持
- 开发和生产环境配置
社区资源
- Awesome Webpack - Webpack 资源集合
- Webpack Examples - 官方示例
- Webpack 中文社区 - 中文社区资源
🎯 总结
通过本文档,你学会了:
- Webpack 基础概念 - 理解了模块打包的核心思想
- 配置文件编写 - 掌握了基础、开发、生产环境的配置
- Loader 开发 - 学会了如何编写自定义加载器
- Plugin 开发 - 掌握了插件系统的使用方法
- 实际项目应用 - 通过 webpack-simple-demo 项目实践
继续深入学习 Webpack,建议:
Happy Coding! 🚀