Skip to content
Webpack Loader 与 Plugin
概述
Webpack 是一个模块打包器。它的核心流程包括:解析配置、创建 Compiler 实例、从入口递归构建模块依赖图、通过 Loader 转换非 JavaScript 文件、将每个文件封装为 Module 对象、根据依赖与配置合并 Chunk,并在构建生命周期的各个阶段通过 Plugin 执行自定义逻辑,最后将资源写入磁盘。
Webpack 的整个工作过程由 Tapable 驱动。Tapable 是一个事件库,它提供了多种同步与异步钩子,Webpack 内部的 Compiler 与 Compilation 对象上都挂载了大量这样的钩子。常见的钩子类型有:
SyncHook:同步串行,不关心返回值。SyncBailHook:同步串行,遇到非undefined返回值即停止。SyncWaterfallHook:同步串行,上一个插件的返回值作为下一个插件的参数。SyncLoopHook:同步循环,直到所有插件返回undefined。AsyncParallelHook:异步并行。AsyncParallelBailHook:异步并行,出现非undefined返回值时停止。AsyncSeriesHook:异步串行。AsyncSeriesBailHook:异步串行,出现非undefined返回值时停止。AsyncSeriesWaterfallHook:异步串行瀑布流。
理解这些钩子是开发 Plugin 的基础,因为 Plugin 本质上就是将回调注册到这些钩子上。
Loader
基本概念
Loader 是一个接收源文件内容、返回转换后内容的函数,运行在 Node.js 环境中。其职责包括:语法转换(TypeScript → JavaScript、SCSS → CSS、Markdown → HTML)、文件预处理(注入 polyfill 或运行时代码)以及资源处理(图片压缩、字体转换等)。
在执行顺序上,多个 Loader 会组成一个处理管道:use 数组中靠后的 Loader 先拿到原始内容,处理后交给前一个 Loader,即“从右到左、从下到上”执行。同时,每个 Loader 可以提供 pitch 方法,pitch 阶段从左到右执行,normal 阶段从右到左执行。这种机制允许 Loader 在正式处理前提前返回或共享数据。
常用 Loader
样式处理
css-loader
解析 CSS 中的@import与url()语句,将其转换为 CommonJS 模块。可以通过modules选项启用 CSS Modules 以实现局部作用域。style-loader
将 CSS 通过<style>标签注入 DOM,适用于开发环境。在生产构建中通常会替换为MiniCssExtractPlugin.loader以生成独立的 CSS 文件。sass-loader
将 SCSS/SASS 编译为 CSS,依赖sass(Dart Sass)或node-sass。一般与css-loader、style-loader或mini-css-extract-plugin.loader链式使用。less-loader
将 Less 编译为 CSS,用法与sass-loader类似。postcss-loader
通过 PostCSS 对 CSS 进行后处理。常用场景包括自动添加浏览器前缀(Autoprefixer)、启用 CSS Modules 以及使用下一代 CSS 语法插件。mini-css-extract-plugin.loader
由MiniCssExtractPlugin提供,用于将 CSS 从 JavaScript 包中提取成独立的.css文件,代替style-loader。
JavaScript / TypeScript 编译
babel-loader
利用 Babel 将 ES6+、TypeScript、JSX 等代码转译为浏览器兼容的 JavaScript。通过预设 (preset) 与插件灵活配置。ts-loader
调用 TypeScript 编译器(tsc)转译.ts文件,并默认执行类型检查。适合需要完整 TS 错误检查的项目。esbuild-loader
使用 esbuild(Go 编写)进行快速的 JavaScript / TypeScript 转译,速度远快于 Babel 和ts-loader,但功能覆盖范围较窄。swc-loader
基于 Rust 的 SWC 编译器,同样以极快速度转译 JS/TS,可作为 Babel 的替代方案。
资源处理
Webpack 5 内置了 Asset Modules,大部分场景可替代 file-loader、url-loader 和 raw-loader。当仍需显式使用 Loader 时:
file-loader
将文件输出到构建目录并返回其公共 URL。Webpack 5 推荐使用type: 'asset/resource'替代。url-loader
将小文件编码为 Data URL(base64),大于限制的文件则回退到file-loader。对应 Webpack 5 的type: 'asset'或'asset/inline'。raw-loader
将文件以字符串形式导入。Webpack 5 中可使用type: 'asset/source'代替。image-webpack-loader
基于 imagemin 进行图片压缩,适用于优化构建产物体积。
代码质量
eslint-loader
已废弃,建议改用ESLintWebpackPlugin。stylelint-webpack-plugin
对样式文件运行 Stylelint 检查。
模板及其他
html-loader
解析 HTML 文件中引用的图片等资源,将其转为可被 Webpack 处理的模块。markdown-loader
将 Markdown 编译为 HTML 或 JavaScript 模块。vue-loader
解析 Vue 单文件组件(.vue),提取其中的<template>、<script>、<style>,并交给对应的 Loader 处理。需要配合VueLoaderPlugin使用。svg-sprite-loader
将多个 SVG 图标合并为 SVG sprite,减少 HTTP 请求。thread-loader
将后续的 Loader 放入 worker 池中并行执行,用于加速重负载的 Loader 处理。
开发自定义 Loader
Loader 的基本结构是一个导出函数的 Node.js 模块。函数中的 this 上下文提供了 Loader API。
同步 Loader
javascript
module.exports = function (source) {
const options = this.getOptions();
const result = someTransform(source, options);
return result;
};通过 this.getOptions() 获取用户在 webpack.config.js 中传入的配置,然后对源码进行处理并返回结果。
异步 Loader
javascript
module.exports = function (source) {
const callback = this.async();
someAsyncTransform(source, (err, result) => {
if (err) return callback(err);
callback(null, result);
});
};调用 this.async() 通知 Webpack 该 Loader 为异步模式,处理结束后调用返回的 callback。
Raw Loader(二进制)
javascript
module.exports = function (source) {
// source 为 Buffer
const optimized = imagemin(source);
return optimized;
};
module.exports.raw = true;通过设置 module.exports.raw = true,Loader 接收到的 source 将是一个 Buffer。
Pitch Loader
javascript
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42;
// 若在此 return 一个值,则跳过后续 loader 的 pitch 与 normal 阶段
};pitch 阶段可用于在 Loader 正式处理前注入共享数据或提前终止管道。
示例:env-replace-loader
将源代码中的占位符替换为环境变量或配置中的值:
javascript
module.exports = function (source) {
const options = this.getOptions({});
const replacements = options.replacements || {};
let result = source;
for (const [key, value] of Object.entries(replacements)) {
const regex = new RegExp(`__${key}__`, 'g');
result = result.replace(regex, value);
}
return result;
};配置示例:
javascript
{
test: /\.js$/,
use: {
loader: 'env-replace-loader',
options: {
replacements: {
API_URL: process.env.API_URL || 'https://api.default.com',
VERSION: require('./package.json').version
}
}
}
}构建时,源文件中所有 __API_URL__ 和 __VERSION__ 占位符会被替换为对应的实际值。
示例:i18n-auto-loader
从源代码中提取中文字符串并替换为国际化函数调用,同时生成 JSON 翻译文件:
javascript
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
module.exports = function (source) {
const ast = parser.parse(source, { sourceType: 'module' });
const messages = {};
let counter = 0;
traverse(ast, {
StringLiteral(path) {
if (/[\u4e00-\u9fa5]/.test(path.node.value)) {
const key = `i18n_key_${counter++}`;
messages[key] = path.node.value;
path.replaceWith(
t.callExpression(t.identifier('t'), [t.stringLiteral(key)])
);
}
}
});
if (Object.keys(messages).length > 0) {
const messagesJson = JSON.stringify(messages, null, 2);
this.emitFile('i18n-extracted.json', messagesJson);
}
return generate(ast).code;
};该 Loader 将源码中的中文字符串替换为 t('i18n_key_0') 这样的调用,同时将提取到的文本映射写入 i18n-extracted.json 文件。实际项目中还需配合运行时 t 函数实现翻译能力。
注意点
- Loader 运行在 Node.js 环境,可以使用文件系统等 API,但不能访问浏览器 API。
- Loader 只能处理单个文件的内容,无法直接获取模块依赖图;跨文件的逻辑应通过 Plugin 实现。
- 链式调用时,每个 Loader 的输出通过
return或this.callback()传递给下一个 Loader。this.callback还可同时传递 source map 和 AST。 - 从 Webpack 5 开始,应使用
this.getOptions()获取配置,避免直接读取this.query。 - 显式调用
this.cacheable()并正确声明文件与目录依赖 (this.addDependency、this.addContextDependency),能让 Webpack 在 watch 模式下准确地增量构建。
Plugin
基本概念
Plugin 是一个实现了 apply 方法的类(或符合该约定的对象)。apply 方法接收 compiler 实例,通过在其钩子上注册回调来介入整个构建流程。
javascript
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
// 在资源输出前执行自定义逻辑
});
}
}Plugin 可以访问 compilation 对象,从而查看或修改模块图、Chunk、资源列表,并能在构建的任意阶段添加、修改或删除资源,生成额外文件,甚至注入运行时代码。
常用 Plugin
资源输出
HtmlWebpackPlugin
基于模板生成 HTML 文件,并自动将打包输出的 JS、CSS 等资源以<script>和<link>标签注入。支持多页应用配置。MiniCssExtractPlugin
将 CSS 从 JavaScript 包中提取为独立的.css文件。需配合其提供的 loader 使用。实际使用构建时基本都会用到。CopyWebpackPlugin
将指定的文件或整个目录复制到输出目录,常用于部署静态资源。CompressionWebpackPlugin
生成 gzip 或 brotli 压缩版本(.gz、.br)的资源,便于服务器开启静态压缩。
代码分割与优化
SplitChunksPlugin(内置)
通过optimization.splitChunks配置抽取公共模块、第三方库或异步模块,减小重复代码。TerserWebpackPlugin(内置)
使用 Terser 压缩与混淆 JavaScript。Webpack 5 在生产模式下默认启用。CssMinimizerWebpackPlugin
对 CSS 文件进行压缩,可配合MiniCssExtractPlugin使用。BundleAnalyzerPlugin
生成交互式可视化报告,帮助分析各模块在打包产物体积中的占比。ModuleConcatenationPlugin(内置)
开启作用域提升(Scope Hoisting),将模块合并到同一个闭包中,减少函数包裹并提升运行速度。
运行时注入
DefinePlugin(内置)
在编译时替换源代码中的全局常量,例如注入process.env.NODE_ENV。ProvidePlugin(内置)
自动加载模块。例如将$映射为jquery,代码中无需显式import。BannerPlugin(内置)
在每个生成的 JS/CSS 文件头部添加注释(如版本、版权声明)。NormalModuleReplacementPlugin(内置)
根据条件替换请求模块,常用于环境配置替换。
开发体验
HotModuleReplacementPlugin(内置)
启用模块热替换(HMR),在应用运行时局部更新模块而不丢失状态。ForkTsCheckerWebpackPlugin
将 TypeScript 类型检查放到独立进程中执行,使构建与类型检查并行,提升性能。ReactRefreshWebpackPlugin
为 React 组件提供 Fast Refresh 能力,在保留组件状态的同时更新渲染。ESLintWebpackPlugin/StylelintWebpackPlugin
在构建过程中运行 ESLint 或 Stylelint 检查,尽早暴露代码质量问题。
环境与部署
EnvironmentPlugin(内置)/DotenvWebpackPlugin
管理环境变量,将.env文件中的键值对注入到代码中。WebpackManifestPlugin
生成一个 JSON 文件,记录资源名称与最终输出文件名(含哈希)的映射关系,便于服务端引用。SentryWebpackPlugin
将构建生成的 source map 上传到 Sentry,辅助线上错误定位。
Compiler 与 Compilation 钩子
Compiler 钩子贯穿整个构建生命周期,主要钩子按执行顺序大致为:
environment → afterEnvironment → entryOption → afterPlugins → afterResolvers → initialize → beforeRun → run → watchRun → normalModuleFactory → contextModuleFactory → beforeCompile → compile → thisCompilation → compilation → make → finishMake → afterCompile → shouldEmit → emit → afterEmit → done
其中常用的有:
entryOption:读取入口配置后触发,可在此修改 entry。compile:开始编译前触发。compilation:compilation对象创建后触发,通常在这里为后续的 compilation 钩子注册回调。make:开始从 entry 递归构建模块依赖图。emit:输出生成资源到 output 目录之前,可在此添加、修改或删除资源。afterEmit:资源写入磁盘之后。done:整个编译流程结束。
Compilation 代表一次完整的模块构建与资源生成过程。watch 模式下每次重新编译都会创建新的 compilation 实例。常用的 compilation 钩子包括:
buildModule/succeedModule/failedModule:模块构建相关。optimize/optimizeModules/optimizeChunks/optimizeTree:优化阶段。processAssets:Webpack 5 新增,用于替代已废弃的 asset 相关钩子,允许在不同处理阶段对资源进行操作。afterProcessAssets:资源处理结束。
processAssets 钩子
processAssets 接收一个 stage 配置,用于精确控制在哪个阶段处理资源:
javascript
const { Compilation } = require('webpack');
compilation.hooks.processAssets.tap(
{
name: 'MyPlugin',
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
(assets) => {
// assets 的 key 为文件名,value 为 Source 对象
for (const [filename, source] of Object.entries(assets)) {
// source.source() 返回字符串内容
// source.size() 返回字节大小
}
}
);常用阶段常量包括:PROCESS_ASSETS_STAGE_PRE_PROCESS、PROCESS_ASSETS_STAGE_OPTIMIZE、PROCESS_ASSETS_STAGE_ADDITIONS、PROCESS_ASSETS_STAGE_REPORT 等。使用这些阶段可以避免自定义 Plugin 之间的顺序冲突。
开发自定义 Plugin
示例:BuildInfoPlugin
生成一个包含构建元信息的 JSON 文件:
javascript
class BuildInfoPlugin {
constructor(options = {}) {
this.filename = options.filename || 'build-info.json';
}
apply(compiler) {
compiler.hooks.emit.tapAsync('BuildInfoPlugin', (compilation, callback) => {
const buildInfo = {
version: require('./package.json').version,
buildTime: new Date().toISOString(),
hash: compilation.hash,
modules: compilation.modules.size,
chunks: compilation.chunks.size,
assets: Object.keys(compilation.assets),
};
const json = JSON.stringify(buildInfo, null, 2);
compilation.assets[this.filename] = {
source: () => json,
size: () => json.length,
};
callback();
});
}
}该 Plugin 在资源输出前将当前构建信息写入 compilation.assets,从而生成 build-info.json 文件。
示例:UnusedFilesPlugin
检测项目中未被引用的文件:
javascript
const glob = require('glob');
const path = require('path');
class UnusedFilesPlugin {
constructor(options = {}) {
this.options = {
directory: options.directory || 'src',
pattern: options.pattern || '**/*.@(js|jsx|ts|tsx|css|scss|png|jpg|svg)',
...options,
};
}
apply(compiler) {
compiler.hooks.afterEmit.tapAsync(
'UnusedFilesPlugin',
(compilation, callback) => {
const projectRoot = compiler.context;
const allFiles = glob.sync(this.options.pattern, {
cwd: path.join(projectRoot, this.options.directory),
absolute: true,
});
const usedFiles = new Set();
compilation.modules.forEach((module) => {
if (module.resource) {
usedFiles.add(module.resource);
}
});
const unused = allFiles.filter((f) => !usedFiles.has(f));
if (unused.length > 0) {
console.warn(
`\n[UnusedFilesPlugin] 发现 ${unused.length} 个未被引用的文件:`
);
unused.forEach((f) =>
console.warn(` - ${path.relative(projectRoot, f)}`)
);
}
callback();
}
);
}
}该 Plugin 在所有资源输出完毕后,对比项目目录下的所有文件与 compilation 中记录的已使用文件,将差值打印到控制台。
注意点
- 避免在 Plugin 钩子回调中执行过重的同步操作,可能导致构建卡顿。必要时可使用异步钩子或将任务分包。
- 在
compilation钩子上注册的回调要注意内存泄漏问题:watch 模式下每次编译都会创建新的compilation,若在上一次compilation上注册的事件未被及时释放,容易造成内存堆积。通常应在compiler.hooks.compilation中对每个compilation使用一次性事件,或在compilation结束时清理。 - Webpack 5 中推荐使用
compilation.hooks.processAssets并配合阶段常量来操弄资源,避免依赖已废弃的compilation.hooks.optimizeChunkAssets、additionalAssets等钩子,使 Plugin 在不同环境下行为更稳定。 - 修改
compilation.assets时,赋予的对象必须包含source()和size()方法,或者使用 Webpack 提供的RawSource、ConcatSource等 Source 类。
Loader 与 Plugin 的协作
Loader 处理单个文件到模块的“翻译”,Plugin 编排整个构建流程中的“事件”。实际工程中两者常协同工作:
vue-loader解析.vue文件,VueLoaderPlugin负责将解析出的各部分模块注入 Webpack 的模块处理管道。css-loader处理 CSS 依赖,MiniCssExtractPlugin将 CSS 从 JS bundle 中提取为单独文件。babel-loader转译 JSX,ReactRefreshWebpackPlugin注入 React Fast Refresh 的运行时代码。
当需要决定实现方式时,可参考以下判断:
- 需求仅针对某类型文件的内容转换 → Loader。
- 需求需要访问模块依赖图、Chunk 信息、整体资源列表 → Plugin。
- 需求需要在构建的开始、结束或特定生命周期阶段执行逻辑 → Plugin。
- 需求需要输出额外文件 → Plugin。
调试与测试
Loader 测试
通过搭建一个 Webpack 编译器实例,对指定的 fixture 文件应用 Loader,然后检查输出内容:
javascript
const compiler = require('./test-utils/compiler');
test('env-replace-loader', async () => {
const stats = await compiler('test/fixture.js', {
loader: {
test: /\.js$/,
use: {
loader: path.resolve(__dirname, '../src/env-replace-loader.js'),
options: { replacements: { API_URL: 'https://test.api.com' } }
}
}
});
const output = stats.toJson().modules[0].source;
expect(output).not.toContain('__API_URL__');
expect(output).toContain('https://test.api.com');
});Plugin 测试
Plugin 的测试可直接使用 Webpack 的 Node API 运行一次真实编译,然后对 stats.compilation 进行断言:
javascript
const webpack = require('webpack');
const MyPlugin = require('../src/my-plugin');
test('plugin generates build info', (done) => {
const compiler = webpack({
entry: './test/fixture.js',
plugins: [new MyPlugin()]
});
compiler.run((err, stats) => {
expect(err).toBeNull();
const assets = Object.keys(stats.compilation.assets);
expect(assets).toContain('build-info.json');
done();
});
});参考链接
Webpack 官方文档
https://webpack.js.org/Loader API
https://webpack.js.org/api/loaders/Plugin API
https://webpack.js.org/api/plugins/Tapable 仓库
https://github.com/webpack/tapableCompiler Hooks
https://webpack.js.org/api/compiler-hooks/Compilation Hooks
https://webpack.js.org/api/compilation-hooks/Asset Modules
https://webpack.js.org/guides/asset-modules/编写一个 Loader
https://webpack.js.org/contribute/writing-a-loader/编写一个 Plugin
https://webpack.js.org/contribute/writing-a-plugin/
