【每日一面】webpack loader 原理
基础问答
问:webpack 的 loader 了解吗,有什么作用?为什么 webpack 会需要 loader?
答:webpack 本身仅能够识别 JavaScript 和 JSON 文件,但实际项目开发中会用到 CSS、Less、图片、Vue 组件等多种格式资源,Loader 就是用来解决这个问题的。Loader 将非 JS/JSON 资源转换成 webpack 支持的模块,再交由 webpack 进行依赖分析和打包。
其核心工作流程是:接收源文件内容作为输入,通过特定转换逻辑处理后,输出新的文件内容提供给下一个 Loader(或其他工具使用)。
扩展延伸
定义
Loader 是 webpack 的核心插件机制,专门用于转换非 JavaScript/JSON 类型的文件,让这些文件变成 webpack 可以处理的模块。
如果没有 Loader,webpack 将无法打包 css、图片等资源。
工作流程
大致可分为三个阶段:
- 解析阶段:Webpack 遇到非 JS/JSON 文件时,根据
module.rules匹配对应的 Loader 规则,确定需要执行的 Loader 链。 - 执行阶段:按 “从右到左” ,“从下到上”的顺序执行 Loader 链,每个 Loader 接收上一个 Loader 的输出作为输入(第一个 Loader 接收源文件内容)。例如
use: ['style-loader', 'css-loader', 'postcss-loader']中,实际执行顺序为 postcss-loader → css-loader → style-loader。 - 输出阶段:最后一个 Loader 返回 Webpack 可识别的格式的资源,通常是 JS 模块,Webpack 会将其纳入依赖图,提供后续处理。
注意:Loader 运行在 Node.js 环境,支持 CommonJS 模块规范。
高级特性
-
缓存机制:webpack 默认会缓存 Loader 的执行结果,当源文件内容、Loader 配置、依赖内容等未发生变化的时候,可以直接复用缓存结果。
-
并行执行:webpack 是单线处理的,但是我们可以通过 loader 支持(thread-loader)并行处理,但是处理上是有一些限制的,具体可以参考 thread-loader 文档,虽然有一些限制,但是这在大型项目的构建中很有用,有相当的性能提升。
-
pitch/normal 机制:loader 除了可以导出一个 Normal 函数(即默认导出的函数视为 Normal 函数),还可以同时导出一个 pitch 函数,他可以改变 loader 的执行顺序(和 Normal 函数执行顺序相反),这里我给一个例子。
// A-loader.js module.exports = function (content) { // Normal 函数 console.log('A 正常函数执行'); return content; }; module.exports.pitch = function () { // pitch 函数 console.log('A.pitch 执行'); // 若返回非 undefined,会跳过后续 Loader 的 pitch 和正常函数 // return '跳过 B、C 的处理'; };pitch 函数核心用途是拦截资源处理,比如 style-loader 会利用 pitch 函数提前注入 css 挂载逻辑,而不需要等待后续 Loader 执行结束。
-
Loader Context:Loader 函数的
this 就是 webpack 注入的上下文对象,提供很多重要的能力,是NormolModule.createLoaderContext函数在调用 Loader 前创建的。
自定义 Loader
Loader 实际就是一个函数,用于处理一些文件,本身没什么复杂的,Loader 接收 3 个参数:
source :资源文件内容,在 Loader 执行阶段中,我们说每个 Loader 接收的是上一个 Loader 的处理结果,但是对于第一个 Loader,接收的是原始文件。
sourceMap :(可选)代码的 sourcemap 结构。
meta :(可选)其他在 Loader 链中传递的参数,可以是任何内容,一般是用于 Loader 之间的协作。
这里实现一个替换内网域名的 Loader:
// 直接导出Loader函数,接收content(文件内容)和map(SourceMap)
module.exports = function(content, map, meta) {
// 1. 字符串替换:baidu.com → ***
const result = content.toString().replace(/baidu\.com/g, '***');
// 2. 输出结果+SourceMap,保证调试正常,meta 在 Loader 之间传递信息,一般是 AST,防止每个 Loader 都重复解析。
this.callback(null, result, map, meta);
};
将内网信息替换掉之后,发布便不会泄露内网相关的信息了。
和 Plugin 的区别
面试中常常会问,原因是这个在我们的项目维护中也是非常有用的,我们有时候可能会编写一些业务项目定制的 Loader 或 Plugin,但是选 Loader 还是选 Plugin,这也是一个选型,这里给出一个表格来快速对比二者区别:
| 对比维度 | Loader | Plugin |
|---|---|---|
| 核心定位 | 专注于单个文件内容的处理与转换,解决 Webpack 无法识别非 JS/JSON 资源的问题 | 专注于整个构建流程的干预与扩展,在构建过程中提供功能补充(如优化、生成、监控) |
| 处理粒度 | 每次仅处理一个独立文件(如单独转换某個 CSS 文件、某张图片) | 作用于整个构建流程(如对所有打包后的 JS 文件进行压缩、为所有 HTML 注入脚本) |
| 运行时机 | 模块解析阶段 | Webpack 构建的全阶段,Webpack 的生命周期 |
| 配置方式 | 通过 module.rules 配置:需指定 test(匹配文件)、use(Loader 列表),按规则匹配执行 | 通过 plugins 数组配置:需实例化插件类(如 new HtmlWebpackPlugin()),全局生效 |
| 上下文信息 | 仅能访问当前处理文件的局部上下文(this 仅包含当前文件路径、参数等),无法操作全局构建状态 | 可访问 Webpack 完整的全局上下文(Compiler/Compilation 对象),能修改构建依赖图、输出文件等全局信息 |
| 依赖关系 | 链式多个 Loader 处理同一文件时,按 “从右到左” 顺序执行,后一个 Loader 的输出作为前一个的输入(如 style-loader 依赖 css-loader 的输出) | Webpack 生命周期,插件间通过钩子执行顺序依赖(如 TerserPlugin 需在代码生成后执行,依赖 optimizeChunkAssets 钩子),无固定执行顺序,由钩子触发时机决定 |
| 配置复杂度 | 简单 | 中等 / 复杂 |
| 典型使用场景 | 1. 样式处理:css-loader(解析 CSS)、sass-loader(编译 SCSS)2. 语法转换:babel-loader(ES6+ 转 ES5)、ts-loader(TS 转 JS)3. 资源处理:asset-loader(处理图片 / 字体)、raw-loader(读取文件为字符串) | 1. 资源生成:HtmlWebpackPlugin(生成 HTML 文件)、CopyWebpackPlugin(拷贝静态资源)2. 代码优化:TerserPlugin(压缩 JS)、CssMinimizerPlugin(压缩 CSS)3. 环境配置:DefinePlugin(注入环境变量)、HotModuleReplacementPlugin(开启热更新)4. 质量监控:ESLintPlugin(代码校验)、BundleAnalyzerPlugin(打包体积分析) |
| 错误影响范围 | 单个 Loader 报错仅导致当前文件处理失败,不影响其他文件的构建(如某张图片处理失败,不影响 JS 文件打包) | 插件报错可能导致整个构建流程中断(如 HtmlWebpackPlugin 模板路径错误,会导致所有 HTML 生成失败,构建终止) |
面试追问
-
我们项目中一般都是用 less/scss 等 css 预处理器,Loader 配置的时候采用的哪些?怎么配置?
一般使用
less-loader 、css-loader 、style-loader来解析处理,配置方式为:module.exports = { module: { rules: [ { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] } ] } }; -
这三个 Loader 之间怎么配合处理文件的?
less-loader 将 less 转换成对应的 css 内容,再将这个 css 内容传给 css-loader 处理,最后将 css-loader 的处理结果提供给 style-loader 内联到 HTML 中。
-
我现在写代码的时候由于一些问题,需要为每个文件都引入
@/utils/autoGenerateEnum文件,我要怎么做才好?写一个自定义 Loader,为源文件的开头自动添加一段代码引入该文件,在 webpack 的 Loader 规则中,将这个自定义 Loader 放在最下方。
module.exports = function (source) { const newSource = "import '@/utils/autoGenerateEnum';" + source; this.callback(null, newSource); } -
为什么要放在最下方?
因为 Loader 的链式执行顺序是自下到上,从右到左的执行顺序。
-
你这里的 this,是这个匿名函数的内容吧?你在哪里定义了 callback?
单独以一个函数来看 Loader 的代码,这段代码是有问题的,callback 不存在,但是他是在 webpack 处理文件过程中调用的,调用时,webpack 会通过
call/apply函数来将 webpack 的上下文注入到 Loader 中,使 Loader 可以访问 webpack 的上下文。所以,这个 callback 函数实际上是 webpack 提供的。
-
具体场景:在项目中,使用
babel-loader处理 js 的时候,构建速度慢怎么处理?两个方向,一是减少重复的转换,二是能够并行处理文件
- 开启缓存:缓存编译结果,后续文件变更时才重新编译
- 开启并行:使用
thread-loader 开启单独的线程处理babel-loader转换 - 排除已编译的依赖:配置 exclude 排除 node_modules 中的包,因为这些第三方依赖都已经编译过了。
-
webpack 提供异步处理 Loader,为什么要这样,有什么场景吗?
如果 Loader 的操作很耗时,同步操作会卡住构建流程,此时可以换用异步处理,让这些耗时的操作在后台执行,主要场景:
- 远端变量注入:如内网域名等敏感信息,可能会根据内网的域名登记发生变更,为了能够同步的获取到内网信息,可以在编译时请求接口去获取对应的信息。
- 本地IO操作:如解析本地的 JSON 文件,将信息替换到代码文件中。
- 依赖外部命令:如果我们需要注入 git 信息,需要执行 git 命令获取,这个命令就比较耗时。
- 加密信息:如对敏感信息加密使用到了第三方的异步加密库,就只能异步处理了。


浙公网安备 33010602011771号