基本概念
- Loader:对单个资源(文件)做转换的函数(把一个文件从一种内容转换为另一种内容),在 module 层面运行。
- Plugin:在整个构建过程的生命周期里插入钩子逻辑(修改编译器、生成资源、注入行为等),在 compiler/compilation 层面运行。
概念说明
- Loader
- 输入:某个源文件(例如
.js、.css、.png)的内容(通常是字符串或 Buffer),也可以是上一个 loader 产生的结果文件。 - 输出:处理后的模块内容(通常是 JS 源码字符串、source map、或者返回多个值)。
- 用途举例:Babel 转码(
babel-loader)、把 CSS 转成 JS 模块(css-loader+style-loader或mini-css-extract-plugin)或把图片转成 base64(以前的url-loader)等。 - 运行时机:模块解析/构建时,按
module.rules中的匹配依序(从右到左/从下到上)调用 loader 流水线(内部由 loader-runner 这个库调用这些 loader 函数)。
- 输入:某个源文件(例如
- Plugin
- 输入/输出:不是对单文件变换,而是对构建过程(compiler、compilation)进行操作。
- 用途举例:生成 HTML(
html-webpack-plugin)、抽取 CSS(mini-css-extract-plugin)、定义全局常量(DefinePlugin)、并行类型检查(ForkTsCheckerWebpackPlugin)、热更新(HotModuleReplacementPlugin)等。 - 运行时机:在 webpack 的生命周期钩子上(底层使用 Tapable 这个库中的 Hooks 管理生命周期的阶段执行顺序)被调用,比如
compiler.hooks.emit、compilation.hooks.seal等。
loader 的工作原理
- 调用方式与顺序
- 当一个文件被匹配到
module.rules.use(或loader)时,webpack 会把这个文件交给 loader 链处理。链按从右到左或从下到上的顺序执行:最后一个 loader 最先执行(先对原始资源做最底层转换),第一个 loader 最后执行,最终输出 JS 模块。 - 例如
use: ['style-loader','css-loader','postcss-loader']:先执行postcss-loader(处理 CSS),然后css-loader(把 CSS 转成 JS 导出),最后style-loader(把样式注入到 DOM)。
- 当一个文件被匹配到
- 同步 vs 异步
- loader 默认是同步的(同步返回可以直接使用
return或者this.callback(null, recource));若需异步处理(例如异步读取文件、运行外部工具),要在 loader 里调用const callback = this.async(),随后异步完成后callback(null, result, sourceMap)。
- loader 默认是同步的(同步返回可以直接使用
- LoaderContext(this)
- 在 loader 中
this指向 LoaderContext,含有诸多有用属性/方法:this.async()、this.cacheable()、this.resourcePath、this.getOptions()、this.emitWarning()、this.emitError()、this.addDependency()等。比如this.cacheable()用来告诉 webpack 此 loader 的结果可被缓存(现在很多 bake-in 已默认 cache)。
- 在 loader 中
- Pitching Loader
- loader 分为 PitchingLoader 和 NormalLoader,他们的执行顺序是相反的。PitchingLoader 的优先级更高。如果希望改变执行顺序,可以通过 enforce 配置(pre, normal, inline, post)。
- loader 可以定义
pitch方法,webpack 会先按顺序先调用每个 loader 的 pitch(从左到右),若某个 pitch 返回非 undefined,则后续 loader 的 normal phase 不会执行,直接进入回退阶段。pitch 用于拦截并改变执行流程。
- source map 支持
- loader 在输出源码时应尽量返回 source map(第三个参数)来保持调试体验。
plugin 的工作原理
- Tapable 与生命周期
- webpack 的 plugin 基于 Tapable 实现,通过
compiler.hooks和compilation.hooks提供大量钩子(compiler 钩子 | webpack 中文文档),这些钩子大致可以分为同步和异步两大类。插件通过compiler.hooks.someHook.tap(或tapAsync,tapPromise)来注册回调。 - 典型钩子:
beforeRun,run,compile,thisCompilation,compilation,emit,afterEmit,done等。要根据插件目标选择合适的钩子。
- webpack 的 plugin 基于 Tapable 实现,通过
- Compiler vs Compilation
- compiler:表示 webpack 的整个配置 / 生命周期(针对一次完整的 webpack 实例)。
- compilation:表示一次构建(compile)过程,包含模块解析、生成 chunk、生成 assets 等。多个 compilation 可能由同一 compiler 触发(例如 watch 模式中的每次增量构建)。
- Hook 的使用过程:
- 创建 Hook 对象
- 注册 Hook 中的事件
- 触发事件
- Hook 被注册到 Webpack 生命周期的过程:
- 在 Webpack 的 createCompiler 方法中,注册了所有的插件
- 注册插件时,会调用插件函数或者插件对象的 apply 方法
- 插件对象会接收 compiler 对象,可以通过 compiler 对象来注册 Hook 的事件
- 某些插件也会传入 compilation 对象,也可以监听 compilation 的 Hook 事件
常见 loader
babel-loader:JS/TS 转译(配合@babel/core)。ts-loader/esbuild-loader/swc-loader:TypeScript 或替代编译器。css-loader:把 CSS 转成 JS imports(解析@import/url())。style-loader:把 CSS 注入 DOM(开发用)。mini-css-extract-plugin.loader:用于生产环境把 CSS 抽取成单文件。postcss-loader:运行 PostCSS(autoprefixer、cssnano 等)。sass-loader/less-loader:预处理器。file-loader/url-loader(Webpack5 可用 asset modules 替代,已内置):处理图片/字体等资源。html-loader:把 HTML 文件作为模块处理(常配合 html-webpack-plugin)。thread-loader:把后面的 loader 放到 worker 线程中执行(加速 heavy loader)。
常见 plugin
HtmlWebpackPlugin:生成 HTML 文件并注入 script/css。DefinePlugin:在编译时定义全局常量(例如process.env.NODE_ENV)。MiniCssExtractPlugin:将 CSS 从 JS 中抽离到单独文件(生产用)。HotModuleReplacementPlugin:启用 HMR。CleanWebpackPlugin:构建前清理输出目录。CopyWebpackPlugin:复制静态资源到输出目录。ForkTsCheckerWebpackPlugin:在单独进程做 TypeScript 类型检查(配合 ts-loader)。TerserPlugin:压缩 JS(通常在 optimization.minimizer 中)CssMinimizerPlugin:压缩 CSS。BundleAnalyzerPlugin(webpack-bundle-analyzer):分析打包产物体积。ProvidePlugin:自动注入模块(例如$映射到 jQuery)。
编写一个简单 loader(示例)
同步 loader(最基础):
// my-loader.js
// source 资源文件内容;map sourcemap 相关数据;meta 一些元数据
module.exports = function(source, map, meta) {// this is loader contextconst options = this.getOptions?.() || {};// 对 source 做变换const result = source.replace(/FROM_LOWER/g, 'from lower');// 返回处理后的 sourcereturn result;
}
异步 loader(使用 this.async):
module.exports = function(source) {const callback = this.async();someAsyncTransform(source, (err, result) => {if (err) return callback(err);callback(null, result);});
}
Pitch loader:
module.exports.pitch = function(remainingRequest) {// 当某些条件满足时返回结果,阻止后续 normal 阶段执行if (someCondition) {return `module.exports = require(${JSON.stringify('!' + remainingRequest)})`;}
}
编写一个简单 plugin(示例)
最小插件骨架:
class MyPlugin {constructor(options) { this.options = options; }// 注意 apply 只是挂钩的入口点,所有工作都是在特定钩子里做的。apply(compiler) {// emit钩子:输出 asset 到 output 目录之前执行。compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {// 在 compilation.assets 上添加或修改资源compilation.assets['banner.txt'] = {source: () => 'hello world',size: () => 11};callback();});}
}
module.exports = MyPlugin;
用 Promise / tapPromise:
compiler.hooks.emit.tapPromise('MyPlugin', async (compilation) => {const data = await fetchSomething();compilation.assets['data.json'] = {source: () => JSON.stringify(data),size: () => Buffer.byteLength(JSON.stringify(data))};
});