Webpack详解
Webpack详解
文章目录
- Webpack详解
- 一、配置
- 1.Entry(入口)
- 2. Output(输出)
- 3. Loader(加载器)
- 4. Plugin(插件)
- 5. Module(模块)
- 6. Dependency Graph(依赖图)
- 二、Webpack 使用的模块规范
- 1.AMD
- 2.UMD(通用模块定义)
- 三、Webpack 的构建流程(内部工作原理 / 打包过程)
- 1.Tapable
- 1.1 定义
- 1.2 为什么 Webpack 需要 Tapable?
- 1.3 Tapable 提供的钩子
- 1.4 Tapable 是如何控制 Webpack 构建流程的?
- 四、Webpack 的使用场景
一、配置
1.Entry(入口)
entry
是 Webpack 构建的起点,即 Webpack 开始构建依赖图的第一个模块。从 entry
指定的模块出发,Webpack 会递归地分析该模块所依赖的其他模块,最终构建出一个完整的依赖关系图。
作用:
- 定义了 Webpack 打包的起始文件
- 决定了依赖图的“根节点”
- 通常对应应用的主 JS 文件,比如
index.js
或app.js
常见配置方式:
1.单入口(Single Entry)—— 字符串形式
适用场景: 项目只有一个入口文件,比如一个单页应用(SPA),只有一个 index.js
作为主入口。
配置方式:
module.exports = {entry: './src/index.js', // 单入口:字符串形式
};
含义:
- Webpack 会从
./src/index.js
开始分析依赖,构建依赖图。 - 打包生成的文件通常也只有一个(除非你配置了多个 chunk,比如通过 SplitChunksPlugin)。
2.多入口(Multiple Entry)—— 对象形式(推荐用于多页面应用 / 多模块)
适用场景: 有多个入口文件,比如:
- 多页面应用(MPA),每个页面有独立的 JS 入口,如
index.js
、about.js
、contact.js
- 应用中存在多个独立功能模块,需要分别打包
配置方式:
module.exports = {entry: {main: './src/index.js',about: './src/about.js',contact: './src/contact.js',},
};
含义:
- 每个键值对代表一个入口点,键(如
main
、about
)是 chunk 的名称,值是对应的入口文件路径 - Webpack 会为每个入口生成一个独立的依赖图,最终输出多个 bundle(通常也会配合多个 html 文件,比如通过
HtmlWebpackPlugin
)
输出示例(配合 output 配置):
output: {filename: '[name].bundle.js', // [name] 会被替换为 main、about、contactpath: path.resolve(__dirname, 'dist'),
}
最终会生成:main.bundle.js
,about.bundle.js
,contact.bundle.js
3.多入口 —— 数组形式(不常用,有特定用途)
配置方式:
module.exports = {entry: ['./src/index.js', './src/another.js'], // 数组形式
};
含义:
- 这种写法是将多个文件合并为一个 chunk,即多个模块会一起打包进一个 bundle
- Webpack 会把数组中的所有模块都作为入口模块的依赖,最终只生成一个 bundle
- 通常用于:将多个依赖文件一起打包进主入口(比如 polyfill + app.js),但不推荐用于构建多个独立页面或功能模块
注意:
✅ 数组形式的
entry
仍然是单入口(只有一个 chunk),只是把多个文件“一起打包”而已,不是多入口!
2. Output(输出)
output
是告诉 Webpack 如何处理打包后的资源,包括打包生成的文件名、输出路径、库的导出方式等。
-
filename
:输出文件名(支持 [name]、[hash] 等占位符)。 -
path
:输出目录(需使用 path.resolve 生成绝对路径)。 -
publicPath
:资源引用路径(如 CDN 地址)。 -
library
/libraryTarget
: 打包为库时使用(比如导出为 UMD、全局变量等) -
chunkFilename
: 非入口 chunk 的命名方式
常见配置:
const path = require('path');module.exports = {output: {filename: 'bundle.js', // 输出文件名path: path.resolve(__dirname, 'dist'), // 输出目录(必须是绝对路径)}
};
多入口时,可以使用占位符 [name]
:
output: {filename: '[name].bundle.js', // 比如 main.bundle.js, admin.bundle.jspath: path.resolve(__dirname, 'dist'),
}
3. Loader(加载器)
Loader 是 Webpack 用来预处理模块(文件)的工具。因为 Webpack 本身只理解 JavaScript,当你要处理如 .css
、.png
、.jsx
等非 JS 文件时,就需要使用对应的 loader 将它们转换成 Webpack 能够处理的模块。
本质上是一个函数,接收源文件内容作为输入,返回转换后的内容(通常是 JS 模块代码)。你可以自己编写自定义 loader。
核心特点:
- 转换作用:将非 JS 文件转换为 JS 模块(或可被打包的资源)
- 链式调用:多个 loader 可以组合使用,执行顺序从右到左 / 从下到上
- 模块化:每个 loader 处理一种特定类型的文件
常见 Loader 示例:
module.exports = {module: {rules: [{test: /\.js$/, // 匹配 .js 文件exclude: /node_modules/,use: 'babel-loader' // 将 ES6+ 转为 ES5},{test: /\.css$/,use: ['style-loader', 'css-loader'] // 先 css-loader 解析 CSS,再 style-loader 注入样式},{test: /\.(png|jpg|gif)$/,type: 'asset/resource' // webpack 5 内置资源模块,处理图片}]}
};
Webpack 5 开始推荐使用 Asset Modules(
asset/resource
,asset/inline
,asset/source
,asset
)替代原来的file-loader
、url-loader
等。
4. Plugin(插件)
Plugin 是 Webpack 的扩展机制,用于在打包过程中执行更广泛的任务,比如优化、资源管理、环境变量注入、打包分析等。插件可以操作 Webpack 构建生命周期的各个阶段,功能比 Loader 更强大和全局化。
与 Loader 不同,Loader 主要用于转换某一类文件(模块)的内容,而 Plugin 则作用于整个构建流程,可以控制打包的各个阶段,甚至修改输出结果。
核心特点:
- 是一个具有
apply(compiler)
方法的 JavaScript 对象 - 可以监听 Webpack 提供的钩子(hooks),介入构建过程
- 用于完成 Loader 无法完成的任务,如打包优化、资源管理、注入环境变量等
- 可以操作 Webpack 的内部实例(如 compiler、compilation 对象)
常见 Plugin 示例:
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {plugins: [new HtmlWebpackPlugin({template: './src/index.html' // 自动生成 HTML 并自动引入打包后的 JS}),new CleanWebpackPlugin() // 清理 dist 目录]
};
自定义插件示例:
class MyPlugin {apply(compiler) {compiler.hooks.done.tap('MyPlugin', () => {console.log('打包完成!');});}
}
5. Module(模块)
在 Webpack 中,一切皆模块(Everything is a module)。无论是 JS、CSS、图片、字体,还是其他资源,Webpack 都把它们看作是一个模块,并通过依赖关系进行管理。
Webpack 支持的模块类型包括:
- ES Module (
import ... from ...
) - CommonJS (
require(...)
) - AMD
- UMD
- 全局变量形式的脚本(通过一些 loader 也可处理)
模块特点:
- 每个模块有自己独立的作用域
- 模块之间通过
import/require
建立依赖关系 - Webpack 会分析这些依赖,构建出一个完整的依赖图
6. Dependency Graph(依赖图)
依赖图(Dependency Graph)是 Webpack 最核心的机制之一。它是 Webpack 根据你的 entry
文件,递归分析所有模块之间的依赖关系后,构建出来的一张“模块依赖关系网”。
构建过程:
- 从
entry
开始,分析该模块依赖了哪些其他模块(比如通过import
或require
引入的) - 再分析这些被依赖模块的依赖,递归进行
- 最终形成一棵 / 一张 模块依赖树 / 图
作用:
- Webpack 根据这个依赖图,知道哪些模块需要被打包
- 它决定了最终打包生成的代码结构与内容
- 是 Webpack 实现模块化打包的基础
简单来说:没有依赖图,Webpack 就不知道要打包哪些代码。
二、Webpack 使用的模块规范
Webpack 本身不限定于某一种模块规范,它通过解析不同模块语法,支持以下常见模块系统:
- ES Modules (ESM) –
import/export
(推荐,也是现代前端的主流方式) - CommonJS (CJS) –
require/module.exports
(Node.js 的模块规范) - AMD –
define/require
(常用于浏览器端异步模块加载,如 RequireJS) - UMD – 通用模块定义,兼容 AMD、CommonJS 和全局变量方式
- 全局变量(无模块化) – 比如直接通过
<script>
引入的库,通过expose-loader
或插件也可处理
1.AMD
前端最早的模块化规范之一,由 RequireJS(2009 年)推广普及,解决浏览器环境中 异步加载模块 的问题。
为什么需要 AMD?
在 ES6 模块(import/export
)出现前,前端代码通常通过 <script>
标签直接引入 JS 文件,这种同步加载方式会导致:
- 页面渲染被阻塞(必须等待所有脚本下载并执行完成)。
- 模块间依赖关系复杂(需手动管理加载顺序,如
jQuery
必须在Bootstrap
前加载)。
AMD 通过异步加载和显式声明依赖解决了这些问题。
AMD 的核心规则
1.模块定义:使用 define
函数定义模块,第一个参数是模块名(可选),第二个参数是依赖数组,第三个参数是工厂函数(返回模块导出的内容)。
// 定义一个名为 "math" 的模块,依赖 "jquery"
define("math", ["jquery"], function($) {// 模块逻辑const add = (a, b) => a + b;return { add }; // 导出对象
});
2.依赖声明:工厂函数的参数与依赖数组一一对应(如 ["jquery"]
对应 $
),AMD 会自动异步下载依赖的模块文件,并在所有依赖加载完成后执行工厂函数。
3.模块加载:使用 require
函数加载模块(同样由 RequireJS 提供),支持异步回调。
// 加载 "math" 模块,依赖加载完成后执行回调
require(["math"], function(math) {console.log(math.add(1, 2)); // 输出 3
});
AMD 的特点
- 纯前端规范:仅适用于浏览器环境,不涉及 Node.js。
- 异步优先:依赖加载不阻塞页面渲染,适合浏览器端的动态模块管理。
- RequireJS 主导:AMD 规范由 RequireJS 库实现,是早期前端模块化的主流方案(尤其在 2015 年 ES6 模块普及前)。
2.UMD(通用模块定义)
UMD 是前端为解决 多环境兼容性 而设计的模块化规范(约 2014 年),目标是让同一个模块代码能同时运行在 浏览器、Node.js 或支持其他模块系统(如 AMD、CommonJS)的环境中。
为什么需要 UMD?
前端模块化规范曾长期分裂:
- 浏览器端有 AMD(RequireJS)、CommonJS(早期 Node.js 风格,但浏览器不直接支持)。
- Node.js 只支持 CommonJS(
require/module.exports
)。 - 库开发者若想让模块同时被浏览器和 Node.js 使用,需编写多套代码。
UMD 通过环境检测和动态适配解决了这一问题。
UMD 的核心逻辑
UMD 本质是一个“包装函数”,通过检测当前运行环境,选择对应的模块暴露方式。典型代码结构如下:
(function (root, factory) {// 检测是否为 AMD 环境(如 RequireJS)if (typeof define === "function" && define.amd) {define(["dependency"], factory); // 按 AMD 规范定义模块} // 检测是否为 Node.js/CommonJS 环境else if (typeof module === "object" && module.exports) {module.exports = factory(require("dependency")); // 按 CommonJS 规范导出} // 浏览器全局环境(无模块系统)else {root.myModule = factory(root.dependency); // 直接挂载到全局对象(如 window)}
})(this, function (dependency) {// 模块核心逻辑const func = () => { /* ... */ };return { func }; // 导出内容
});
UMD 的特点
- 多环境兼容:同一份代码可在浏览器(AMD/全局变量)、Node.js(CommonJS)中运行。
- 库开发的常用方案:早期前端库(如部分 jQuery 插件、工具库)常用 UMD 打包,避免用户手动处理模块系统差异。
- 灵活性:通过简单的代码包装即可实现跨环境,无需依赖额外工具(现代打包工具如 Webpack/Rollup 已内置类似能力)。
三、Webpack 的构建流程(内部工作原理 / 打包过程)
Webpack 的构建流程是一个非常精细的过程,主要可以分为如下几个阶段 👇:
注意:Webpack 构建流程是基于 Tapable 钩子机制 控制的,它通过一系列生命周期钩子来驱动整个打包流程。
- 初始化:读取配置,创建 Compiler
- 编译:从 entry 开始递归分析依赖,构建 Dependency Graph,用 Loader 处理模块
- 优化:执行 Tree Shaking、代码分割、作用域提升等优化
- 生成:将模块打包为最终文件,计算 Hash,写入磁盘
- 完成:输出打包结果,触发完成钩子
1.Tapable
1.1 定义
Tapable 是一个轻量级的库,用于实现插件系统与事件驱动的架构,它通过“钩子(Hooks)”机制,允许开发者在代码执行过程中的特定节点“注入”自定义逻辑。
Webpack 内部大量使用了 Tapable 来管理其构建流程中的各个阶段,使得 Webpack 的插件机制变得非常灵活和强大。
🎯 更通俗的理解:
想象成一个事件系统 / 发布-订阅机制,它定义了很多不同类型的“钩子”,你可以在这些钩子上“注册”(tap)你的逻辑(也就是插件要做的事),当 Webpack 运行到某个阶段时,就会“触发”(call)这些钩子,从而执行你注册的逻辑。
1.2 为什么 Webpack 需要 Tapable?
Webpack 是一个高度可扩展的构建工具,它的核心流程(比如解析模块、编译、优化、生成资源等)非常复杂,但它并不直接实现所有功能,而是通过 插件机制 把这些流程的各个阶段暴露出来,让开发者或插件作者可以在特定节点插入自己的逻辑。
为了实现这种灵活的插件系统与生命周期控制,Webpack 使用了 Tapable 这个库来提供:
- 各种类型的“钩子”(Hooks),用于定义事件点
- 提供 “tap”(注册)、“call”(触发)等 API,让插件可以介入构建过程
- 支持同步 / 异步的插件逻辑执行
1.3 Tapable 提供的钩子
Tapable 提供了多种不同类型的 Hook,用于支持同步、异步、串行、并行等不同场景的插件介入方式。
下面是常见的 Hook 类型(都是 Tapable 提供的类):
钩子类型 | 触发方式 | 说明 | 适用场景 |
---|---|---|---|
SyncHook | 同步串行 | 按注册顺序同步依次执行 | 适用于同步任务,比如初始化阶段 |
SyncBailHook | 同步串行,可中断 | 如果某个插件返回非 undefined 值,则停止后续执行 | 比如解析模块时,一旦命中缓存就不再继续 |
SyncWaterfallHook | 同步串行,可传递结果 | 每个插件返回的值,会作为参数传给下一个插件 | 比如某些需要累积结果的场景 |
SyncLoopHook | 同步循环 | 如果插件返回 true ,则重复执行该插件 | 比如某些需要不断重试的操作 |
AsyncParallelHook | 异步并行 | 多个插件同时异步执行 | 适用于可并行处理的任务 |
AsyncParallelBailHook | 异步并行,可中断 | 异步并行,某个插件返回非 undefined 可中断 | |
AsyncSeriesHook | 异步串行 | 多个异步插件按顺序依次执行 | 最常见,比如编译过程、生命周期钩子 |
AsyncSeriesBailHook | 异步串行,可中断 | 异步串行,某个插件返回非 undefined 则停止后续 | |
AsyncSeriesWaterfallHook | 异步串行,可传递结果 | 异步串行,前一个插件的返回值传给下一个 | 比如需要逐步处理并传递数据 |
🧠 Webpack 内部大量使用这些 Hook 类,比如
compiler.hooks.run
是一个AsyncSeriesHook
,它控制 Webpack 开始运行时的生命周期。
1.4 Tapable 是如何控制 Webpack 构建流程的?
核心思想:
Webpack 将构建流程中的各个关键节点抽象为一个个 Tapable 的 Hook(钩子),插件作者可以通过 tap 方法在这些钩子上注册自己的逻辑,当 Webpack 执行到对应阶段时,会通过 call 方法触发这些钩子,从而执行插件逻辑。
1.Webpack 核心对象:Compiler 和 Compilation
- Compiler:代表整个 Webpack 构建过程,是全局唯一的,负责管理整个构建流程
- Compilation:代表一次具体的编译过程,管理模块的编译、依赖图构建、优化等
这两个对象上都挂载了大量的 Tapable Hooks,插件可以通过它们介入到构建的各个阶段。
2.插件如何介入构建流程?
一个 Webpack 插件本质上是一个具有 apply(compiler)
方法的 JavaScript 对象。在 apply
方法中,插件通过 compiler.hooks.xxx.tap(...)
注册到 Webpack 的某个生命周期钩子上。
四、Webpack 的使用场景
场景 | 说明 |
---|---|
单页应用 (SPA) | React / Vue 等框架项目,模块化开发,热更新,代码分割 |
多页应用 (MPA) | 多个 HTML 页面,每个页面独立入口,配合 HtmlWebpackPlugin |
组件库 / 工具库开发 | 打包为 UMD / ESM / CommonJS,支持多环境、Tree Shaking |
静态资源处理 | 图片、字体、样式等资源的模块化打包与优化 |
工程化与开发体验 | HMR、devServer、source map、环境变量等 |
微前端 / 模块联邦 | Webpack 5 模块共享方案,适合大型拆分项目 |