《深入浅出Webpack》前端实战系列 05:核心原理揭秘——从架构流程到Loader/Plugin开发全解
本文深度拆解Webpack的核心工作原理,从整体架构、运行流程到输出文件结构逐一剖析,同时手把手教你开发自定义Loader和Plugin,掌握Webpack底层扩展能力,解决实战中无现成解决方案的定制化需求。学完本文,你将彻底理解Webpack的黑盒运行逻辑,能独立开发符合业务需求的Loader和Plugin。
【本篇核心收获】
- 掌握Webpack核心概念(Entry/Module/Chunk/Loader/Plugin)及整体工作流程
- 理解Webpack输出文件(bundle.js)的底层实现逻辑,包括模块加载、缓存、异步加载机制
- 掌握Loader的开发规范、进阶用法及本地调试方法,能开发解决实际问题的自定义Loader
- 理解Plugin的工作机制,掌握Compiler/Compilation核心对象及事件流用法,能开发基础Plugin
- 明晰Webpack构建流程中各阶段的核心事件及扩展切入点
一、Webpack工作原理全解析
想要开发自定义的Loader或Plugin,首先必须吃透Webpack的核心工作原理,理解其整体架构和运行流程,才能精准找到扩展切入点。
1.1 核心概念
在分析Webpack原理前,需先掌握以下5个核心概念(加粗为核心定义,必须掌握):
- Entry:入口,Webpack执行构建的第一步将从Entry开始,可抽象成输入。
- Module:模块,在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始,递归找出所有依赖的模块。
- Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于将模块的原内容按照需求转换成新内容。
- Plugin:扩展插件,在Webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事件的发生,在特定的时机做对应的事情。
1.2 整体运行流程
Webpack的运行流程是串行的过程,从启动到结束依次执行以下7个步骤:
- 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数。
- 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,通过执行对象的run方法开始执行编译。
- 确定入口:根据配置中的entry找出所有入口文件。
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再将每个Chunk转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。
从宏观上,Webpack的构建流程可分为三大阶段:
- 初始化:启动构建,读取与合并配置参数,加载Plugin,实例化Compiler。
- 编译:从Entry发出,针对每个Module串行调用对应的Loader去翻译文件的内容,再找到该Module依赖的Module,递归地进行编译处理。
- 输出:将编译后的Module组合成Chunk,将Chunk转换成文件,输出到文件系统中。
若开启监听模式,构建流程会循环执行,具体流程如图1所示: 
1.3 各阶段核心事件
Webpack在构建的每个阶段都会广播特定事件,插件可监听这些事件实现扩展。以下是各阶段的核心事件及解释:
(1)初始化阶段
| 事件名 | 解释 |
|---|---|
| 初始化参数 | 从配置文件和Shell语句中读取与合并参数,得出最终的参数。在这个过程中还会执行配置文件中的插件实例化语句 new Plugin() |
| 实例化 Compiler | 用上一步得到的参数初始化Compiler实例,Compiler负责文件监听和启动编译。在Compiler实例中包含了完整的Webpack配置,全局只有一个Compiler实例 |
| 加载插件 | 依次调用插件的apply方法,让插件可以监听后续的所有事件节点。同时向插件传入compiler实例的引用,以方便插件通过compiler调用Webpack提供的API |
| environment | 开始应用Node.js风格的文件系统到compiler对象,以方便后续的文件寻找和读取 |
| entry-option | 读取配置的Entries,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备 |
| after-plugins | 调用完所有内置的和配置的插件的apply方法 |
| after-resolvers | 根据配置初始化resolver,resolver负责在文件系统中寻找指定路径的文件 |
(2)编译阶段
| 事件名 | 解释 |
|---|---|
| run | 启动一次新的编译 |
| watch-run | 和run类似,区别在于它是在监听模式下启动编译,在这个事件中可以获取是哪些文件发生了变化从而导致重新启动一次新的编译 |
| compile | 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象 |
| compilation | 当Webpack以开发模式运行时,每当检测到文件的变化,便有一次新的Compilation被创建。一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation对象也提供了很多事件回调给插件进行扩展 |
| make | 一个新的Compilation创建完毕,即将从Entry开始读取文件,根据文件的类型和配置的Loader对文件进行编译,编译完后再找出该文件依赖的文件,递归地编译和解析 |
| after-compile | 一次Compilation执行完成 |
| invalid | 当遇到文件不存在、文件编译错误等异常时会触发该事件,该事件不会导致Webpack退出 |
其中compilation事件是编译阶段的核心,该阶段内部还会触发以下子事件:
| 事件名 | 解释 |
|---|---|
| build-module | 使用对应的Loader去转换一个模块 |
| normal-module-loader | 在用Loader转换完一个模块后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便Webpack在后面对代码进行分析 |
| program | 从配置的入口模块开始,分析其AST,当遇到require等导入其他模块的语句时,便将其加入依赖的模块列表中,同时对新找出的依赖模块递归分析,最终弄清楚所有模块的依赖关系 |
| seal | 所有模块及其依赖的模块都通过Loader转换完成,根据依赖关系开始生成Chunk |
(3)输出阶段
| 事件名 | 解释 |
|---|---|
| should-emit | 所有需要输出的文件已经生成,询问插件有哪些文件需要输出,有哪些不需要输出 |
| emit | 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出的内容 |
| after-emit | 文件输出完毕 |
| done | 成功完成一次完整的编译和输出流程 |
| failed | 如果在编译和输出的流程中遇到异常,导致Webpack退出,就会直接跳转到本步骤,插件可以在本事件中获取具体的错误原因 |
模块小结:Webpack构建流程分为初始化、编译、输出三大阶段,各阶段会广播特定事件供Plugin扩展;核心是通过Loader处理模块转换,通过Plugin监听事件修改构建结果,监听模式下会循环执行编译和输出阶段。
二、Webpack输出文件深度分析
理解Webpack输出文件(bundle.js)的结构,能帮你搞清楚“为什么模块能在浏览器运行”“代码分割的底层逻辑”等核心问题。
2.1 基础bundle.js结构解析
以最简单的Webpack项目构建出的bundle.js为例,其核心结构是一个立即执行函数,代码如下:
// webpackBootstrap 启动函数
// modules 即存放所有模块的数组,数组中的每个元素都是一个函数
(function(modules) {
// 安装过的模块都存放在这里面
// 作用是将已经加载过的模块缓存在内存中,提升性能
var installedModules = {};
// 去数组中加载一个模块,moduleId 为要加载模块在数组中的 index
// 作用和 Node.js 中的 require 语句相似
function __webpack_require__(moduleId) {
// 如果需要加载的模块已经被加载过,就直接从缓存中返回
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 如果缓存中不存在需要加载的模块,就新建一个模块,并将它存在缓存中
var module = installedModules[moduleId] = {
// 模块在数组中的 index
i: moduleId,
// 该模块是否已经加载完毕
l: false,
// 该模块的导出值
exports: {}
};
// 从 modules 中获取 index 为 moduleId 的模块对应的函数
// 再调用这个函数,同时将函数需要的参数传入
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 将这个模块标记为已加载
module.l = true;
// 返回这个模块的导出值
return module.exports;
}
// Webpack 配置中的 publicPath,用于加载被分割出去的异步代码
__webpack_require__.p = "";
// 使用 __webpack_require__ 去加载 index 为 0 的模块,并且返回该模块导出的内容
// index 为 0 的模块就是 main.js 对应的文件,也就是执行入口模块
// __webpack_require__.s 的含义是启动模块对应的 index
return __webpack_require__(__webpack_require__.s = 0);
})
/************************************************************************/
// 所有的模块都存放在一个数组里,根据每个模块在数组的 index 来区分和定位模块
([
/* 0 */
(function(module, exports, __webpack_require__) {
// 通过 __webpack_require__ 规范导入 show 函数,show.js 对应的模块
// index 为 1
const show = __webpack_require__(1);
// 执行 show 函数
show('Webpack');
}),
/* 1 */
(function(module, exports) {
function show(content){
window.document.getElementById('app').innerHTML = 'Hello,' + content;
}
// 通过 CommonJS 规范导出 show 函数
module.exports = show;
})
]);其核心逻辑可简化为:
(function(modules) {
// 模拟 require 语句
function __webpack_require__() {
// 执行存放所有模块数组中的第 0 个模块
__webpack_require__(0);
}
})([/*存放所有模块的数组*/])关键解读:
- bundle.js能在浏览器运行的核心:通过
__webpack_require__模拟Node.js的require语句,实现浏览器端的模块加载。 - 模块合并的原因:浏览器无法像Node.js那样快速加载本地模块文件,将所有模块合并到一个文件中,减少网络请求次数。
- 缓存优化:
__webpack_require__会将已加载的模块缓存到installedModules中,重复加载时直接读取缓存,提升性能。
2.2 异步加载场景下的输出文件
当使用异步加载(如import('./show'))时,Webpack会输出两个文件:入口文件bundle.js和异步加载文件0.bundle.js。
(1)0.bundle.js内容
// 加载在本文件(0.bundle.js)中包含的模块
webpackJsonp(
// 在其他文件中存放的模块的 ID
[0],
// 本文件所包含的模块
{
// show.js 所对应的模块
"./show.js":
(function(module, exports) {
function show(content) {
window.document.getElementById('app').innerHTML = 'Hello,' + content;
}
module.exports = show;
})
}
);(2)bundle.js的变化
bundle.js新增了__webpack_require__.e(加载异步Chunk)和webpackJsonp(安装异步模块)两个核心函数,核心代码如下:
/**
- 用于从异步加载的文件中安装模块
- @param chunkIds 异步加载的文件中存放的需要安装的模块对应的 Chunk ID
- @param moreModules 异步加载的文件中存放的需要安装的模块列表
- @param executeModules 在异步加载的文件中存放的需要安装的模块都安装成功后,需要执行的模块对应的 index
*/
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// 将 moreModules 添加到 modules 对象中
// 将所有 chunkIds 对应的模块都标记成已经加载成功
var moduleId, chunkId, i = 0, resolves = [], result;
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
while(resolves.length) {
resolves.shift()();
}
};
// 缓存已经安装的模块
var installedModules = {};
// 存储每个 Chunk 的加载状态:
// 键为 Chunk 的 ID,值为 0 代表已经加载成功
var installedChunks = {
1: 0
};
// 模拟 require 语句,和基础版一致
function __webpack_require__(moduleId) {
// ... 省略和基础版一样的内容
}
/**
- 用于加载被分割出去的需要异步加载的 Chunk 对应的文件
- @param chunkId 需要异步加载的 Chunk 对应的 ID
- @returns {Promise}
*/
__webpack_require__.e = function requireEnsure(chunkId) {
// 从上面定义的 installedChunks 中获取 chunkId 对应的 Chunk 的加载状态
var installedChunkData = installedChunks[chunkId];
// 如果加载状态为 0,则表示该 Chunk 已经加载成功了,直接返回 resolve Promise
if(installedChunkData === 0) {
return new Promise(function(resolve) {
resolve();
});
}
// installedChunkData 不为空且不为 0 时,表示该 Chunk 正在网络加载中
if(installedChunkData) {
// 返回存放在 installedChunkData 数组中的 Promise 对象
return installedChunkData[2];
}
// installedChunkData 为空,表示该 Chunk 还没有加载过,去加载该 Chunk 对应的文件
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// 通过 DOM 操作,向 HTML head 中插入一个 script 标签去异步加载 Chunk 对应的 JavaScript 文件
var head = document.getElementsByTagName['head'](0);
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
// 文件的路径由配置的 publicPath、chunkId 拼接而成
script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
// 设置异步加载的最长超时时间
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
// 在 script 加载和执行完成时回调
function onScriptComplete() {
// 防止内存泄露
script.onerror = script.onload = null;
clearTimeout(timeout);
// 去检查 chunkId 对应的 Chunk 是否安装成功,安装成功时才会存在于 installedChunks 中
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
return promise;
};
// 加载并执行入口模块,和基础版一致
return __webpack_require__(__webpack_require__.s = 0);
// 存放所有没有经过异步加载的,随着执行入口文件加载的模块
[
// main.js 对应的模块
(function(module, exports, __webpack_require__) {
// 通过 __webpack_require__.e 异步加载 show.js 对应的 Chunk
__webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
// 执行 show 函数
show('Webpack');
});
})
]关键解读:
__webpack_require__.e:动态创建script标签,加载异步Chunk对应的文件。webpackJsonp:将异步加载的模块添加到全局模块列表中,并标记Chunk加载完成。- 代码分割的底层逻辑:无论是异步加载还是CommonsChunkPlugin提取公共代码,核心都是通过
__webpack_require__.e和webpackJsonp实现Chunk的加载和安装。
注:本实例完整代码可参考:http://webpack.wuhaolin.cn/5-2输出文件分析.zip
模块小结:Webpack输出文件的核心是通过__webpack_require__模拟Node.js模块加载机制;异步加载/代码分割则通过动态加载script标签+webpackJsonp完成,本质是将模块拆分为多个Chunk,按需加载。
三、自定义Loader开发实战
Loader是Webpack的模块转换器,当社区没有满足需求的Loader时,可自行开发。开发Loader的核心是“职责单一”,只处理一种转换逻辑。
3.1 Loader的核心职责
Loader的核心职责是将模块的原内容转换为新内容,支持链式执行(如SCSS文件处理:sass-loader → css-loader → style-loader)。
示例:处理SCSS文件的Webpack配置
module.exports = {
module: {
rules: [
{
// 增加对 SCSS 文件的支持
test: /\.scss$/,
// SCSS 文件的处理顺序为先 sass-loader,再 css-loader,再 style-loader
use: [
'style-loader',
{
loader: 'css-loader',
// 向 css-loader 传入配置项
options: {
minimize: true,
}
},
'sass-loader'
]
}
]
}
};3.2 Loader开发基础
Loader是一个Node.js模块,需导出一个函数,函数的入参是模块原内容,返回值是转换后的内容。
(1)最简Loader示例
module.exports = function(source) {
// source 为 compiler 传递给 Loader 的一个文件的原内容
// 该函数需要返回处理后的内容,这里为了简单起见,直接将原内容返回了,相当于该 Loader 没有做任何转换
return source;
};(2)调用Node.js API的Loader示例
const Sass = require('node-sass');
module.exports = function(source) {
return Sass.renderSync(source);
};3.3 Loader进阶用法
(1)获取Loader的options
通过loader-utils库获取用户传入的options参数:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取用户为当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
};(2)返回多结果(如Source Map)
通过this.callback返回转换后的内容+Source Map(用于调试):
module.exports = function(source) {
// 通过 this.callback 告诉 Webpack 返回的结果
this.callback(null, source, sourceMaps);
// 当使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
// 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中
return;
};this.callback的完整参数:
this.callback(
// 当无法转换原内容时,为 Webpack 返回一个 Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 用于通过转换后的内容得出原内容的 Source Map,以方便调试
sourceMap?: SourceMap,
// 如果本次转换为原内容生成了 AST 语法树,则可以将这个 AST 返回,
// 以方便之后需要 AST 的 Loader 复用该 AST,避免重复生成 AST,提升性能
abstractSyntaxTree?: AST
);避坑指南:Source Map生成耗时,可通过
this.sourceMap判断当前环境是否需要生成,仅在开发环境生成,提升构建速度。
(3)同步与异步Loader
- 同步Loader:转换逻辑同步执行,直接return或
this.callback返回结果(如基础示例)。 - 异步Loader:转换逻辑异步执行(如网络请求),需通过
this.async()获取回调函数:
module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};(4)处理二进制数据
默认情况下,Loader接收的是UTF-8字符串;若需处理二进制文件(如file-loader),需设置module.exports.raw = true:
module.exports = function(source) {
// 在 exports.raw == true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
source instanceof Buffer === true;
// Loader 返回的类型也可以是 Buffer 类型的
// 在 exports.raw != true 时,Loader 也可以返回 Buffer 类型的结果
return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports.raw = true;(5)缓存加速
Webpack默认缓存Loader的处理结果(文件/依赖未变化时,不重复执行Loader);若需关闭缓存,调用this.cacheable(false):
module.exports = function(source) {
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};3.4 其他核心Loader API
除上述API外,Loader还可调用以下常用API:
| API | 作用 |
|---|---|
this.context | 当前处理文件的所在目录(如处理/src/main.js时,值为/src) |
this.resource | 当前处理文件的完整请求路径(含querystring,如/src/main.js?name=1) |
this.resourcePath | 当前处理文件的路径(如/src/main.js) |
this.resourceQuery | 当前处理文件的querystring |
this.target | Webpack配置中的Target(如web、node) |
this.loadModule | 加载指定文件的处理结果(this.loadModule(request, callback)) |
this.resolve | 模拟require,获取文件完整路径(this.resolve(context, request, callback)) |
this.addDependency | 为当前文件添加依赖(依赖变化时重新执行Loader) |
this.addContextDependency | 为当前文件添加目录依赖 |
this.clearDependencies | 清除当前文件的所有依赖 |
this.emitFile | 输出文件(this.emitFile(name, content, sourceMap)) |
补充:更多Loader API可参考Webpack官网:https://webpack.js.org/api/loaders/
3.5 本地Loader调试方法
开发Loader时,无需发布到Npm,可通过以下两种方式本地调试:
(1)Npm link
步骤:
- 确保Loader的package.json配置正确;
- 在Loader根目录执行
npm link,将模块注册到全局; - 在项目根目录执行
npm link loader-name,将全局模块链接到项目node_modules。
(2)ResolveLoader配置
修改Webpack的resolveLoader.modules,指定Loader查找目录:
module.exports = {
resolveLoader: {
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: ['node_modules', './loaders/']
}
};说明:配置后,Webpack先从node_modules找Loader,找不到则从
./loaders/找。
3.6 实战:comment-require-loader开发
需求:将JavaScript注释中的// @require '../style/index.css'转换为require('../style/index.css');,适配Fis3语法的JS文件。
(1)使用配置
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['comment-require-loader'],
// 针对采用了 fis3 CSS 导入语法的 JavaScript 文件,通过 comment-require-loader 转换
include: [path.resolve(__dirname, 'node_modules/imui')]
}
]
}
};(2)实现代码
function replace(source) {
// 使用正则表达式将 //@require '../style/index.css' 转换成 require('./style/index.css');
return source.replace(/\/\/ *@require +(('|").+('|")).*/, 'require($1);');
}
module.exports = function(content) {
return replace(content);
};注:本实例完整代码可参考:https://github.com/gwuhaolin/comment-require-loader
模块小结:Loader开发需遵循“职责单一”原则,支持同步/异步、二进制处理、Source Map输出;本地调试可通过Npm link或ResolveLoader配置,实战中需结合正则/Node.js API实现具体转换逻辑。
四、自定义Plugin开发实战
Plugin是Webpack的扩展插件,通过监听构建流程中的事件,修改构建结果。相比Loader,Plugin能介入Webpack的整个生命周期,功能更强大。
4.1 Plugin基础结构
Plugin是一个类,需实现apply方法(接收Compiler对象),使用时通过new Plugin()实例化。
(1)基础Plugin示例
class BasicPlugin {
// 在构造函数中获取用户为该插件传入的配置
constructor(options) {}
// Webpack 会调用 BasicPlugin 实例的 apply 方法为插件实例传入 compiler 对象
apply(compiler) {
compiler.plugin('compilation', function(compilation) {});
}
}
// 导出 Plugin
module.exports = BasicPlugin;(2)使用配置
const BasicPlugin = require('./BasicPlugin.js');
module.exports = {
plugins: [
new BasicPlugin(options)
]
};4.2 Compiler与Compilation核心对象
开发Plugin的核心是理解Compiler和Compilation:
- Compiler:全局唯一,包含Webpack所有配置(options/loaders/plugins),代表Webpack从启动到关闭的整个生命周期。
- Compilation:每次编译都会创建一个实例,包含当前编译的模块、资源、变化的文件等,代表一次单独的编译过程。
核心区别:Compiler管“整个生命周期”,Compilation管“单次编译”。
4.3 事件流机制
Webpack基于Tapable实现事件流机制,Compiler/Compilation都继承自Tapable,支持广播/监听事件:
(1)事件广播与监听
/**
- 广播事件
- event-name 为事件名称,注意不要和现有的事件重名
- params 为附带的参数
*/
compiler.apply('event-name', params);
/**
- 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
- 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name', function(params) {});(2)异步事件处理
部分事件是异步的,需调用回调函数通知Webpack继续执行:
compiler.plugin('emit', function(compilation, callback) {
// 插件处理逻辑
// 处理完毕后执行 callback 以通知 Webpack
// 避坑指南:不执行 callback 会导致构建流程卡住
callback();
});(3)开发Plugin注意事项
- 可通过Compiler/Compilation广播自定义事件,供其他插件监听;
- 所有插件共享同一个Compiler/Compilation实例,修改其属性会影响后续插件;
- 异步事件必须调用回调函数,否则构建流程会阻塞。
4.4 常用API(待补充)
Plugin开发中常用的Compiler/Compilation API包括:
- Compiler API:
compiler.plugin(监听事件)、compiler.apply(广播事件)、compiler.options(访问Webpack配置)等。 - Compilation API:
compilation.modules(访问模块列表)、compilation.assets(访问资源列表)等。
注:核心需掌握:Plugin通过监听Webpack内置事件(如compile、compilation、emit、done),调用Compiler/Compilation提供的API修改构建结果。
模块小结:Plugin基于事件流机制扩展Webpack,核心是Compiler(全局生命周期)和Compilation(单次编译)对象;开发时需监听对应事件,异步事件必须调用回调函数,避免构建阻塞。
【核心逻辑复盘】
- Webpack构建流程分为初始化(读取配置、实例化Compiler、加载Plugin)、编译(解析Entry、递归编译Module、调用Loader)、输出(组合Chunk、写入文件系统)三大阶段,各阶段广播特定事件供Plugin扩展。
- 输出文件核心是通过
__webpack_require__模拟Node.js模块加载机制,实现浏览器端模块运行;异步加载/代码分割依赖__webpack_require__.e(动态加载文件)和webpackJsonp(安装异步模块),本质是Chunk的拆分与按需加载。 - Loader是模块转换器,遵循“职责单一”原则,支持同步/异步、二进制处理、Source Map输出,本地调试可通过Npm link或ResolveLoader配置,核心是处理模块内容的转换。
- Plugin基于Tapable事件流机制,通过监听Compiler/Compilation的事件实现扩展;Compiler代表整个构建生命周期,Compilation代表单次编译过程,异步事件必须调用回调函数确保流程推进。
