【每日一面】webpack loader 原理

【每日一面】webpack loader 原理

是大神呀 2 2025-11-17

基础问答

问: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 模块规范。

高级特性

  1. 缓存机制:webpack 默认会缓存 Loader 的执行结果,当源文件内容、Loader 配置、依赖内容等未发生变化的时候,可以直接复用缓存结果。

  2. 并行执行:webpack 是单线处理的,但是我们可以通过 loader 支持(thread-loader)并行处理,但是处理上是有一些限制的,具体可以参考 thread-loader 文档,虽然有一些限制,但是这在大型项目的构建中很有用,有相当的性能提升。

  3. 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 执行结束。

  4. 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,这也是一个选型,这里给出一个表格来快速对比二者区别:

对比维度LoaderPlugin
核心定位专注于单个文件内容的处理与转换,解决 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 生成失败,构建终止)

面试追问

  1. 我们项目中一般都是用 less/scss 等 css 预处理器,Loader 配置的时候采用的哪些?怎么配置?

    一般使用 less-loader​ 、css-loader​ 、style-loader 来解析处理,配置方式为:

    module.exports = {
      module: {
        rules: [
          {
            test: /\.less$/,
    		use: ['style-loader', 'css-loader', 'less-loader']
          }
        ]
      }
    };
    
  2. 这三个 Loader 之间怎么配合处理文件的?

    less-loader 将 less 转换成对应的 css 内容,再将这个 css 内容传给 css-loader 处理,最后将 css-loader 的处理结果提供给 style-loader 内联到 HTML 中。

  3. 我现在写代码的时候由于一些问题,需要为每个文件都引入 @/utils/autoGenerateEnum 文件,我要怎么做才好?

    写一个自定义 Loader,为源文件的开头自动添加一段代码引入该文件,在 webpack 的 Loader 规则中,将这个自定义 Loader 放在最下方。

    module.exports = function (source) {
    	const newSource = "import '@/utils/autoGenerateEnum';" + source;
    	this.callback(null, newSource);
    }
    
  4. 为什么要放在最下方?

    因为 Loader 的链式执行顺序是自下到上,从右到左的执行顺序。

  5. 你这里的 this,是这个匿名函数的内容吧?你在哪里定义了 callback?

    单独以一个函数来看 Loader 的代码,这段代码是有问题的,callback 不存在,但是他是在 webpack 处理文件过程中调用的,调用时,webpack 会通过 call/apply 函数来将 webpack 的上下文注入到 Loader 中,使 Loader 可以访问 webpack 的上下文。

    所以,这个 callback 函数实际上是 webpack 提供的。

  6. 具体场景:在项目中,使用 babel-loader 处理 js 的时候,构建速度慢怎么处理?

    两个方向,一是减少重复的转换,二是能够并行处理文件

    1. 开启缓存:缓存编译结果,后续文件变更时才重新编译
    2. 开启并行:使用 thread-loader​ 开启单独的线程处理 babel-loader 转换
    3. 排除已编译的依赖:配置 exclude 排除 node_modules 中的包,因为这些第三方依赖都已经编译过了。
  7. webpack 提供异步处理 Loader,为什么要这样,有什么场景吗?

    如果 Loader 的操作很耗时,同步操作会卡住构建流程,此时可以换用异步处理,让这些耗时的操作在后台执行,主要场景:

    1. 远端变量注入:如内网域名等敏感信息,可能会根据内网的域名登记发生变更,为了能够同步的获取到内网信息,可以在编译时请求接口去获取对应的信息。
    2. 本地IO操作:如解析本地的 JSON 文件,将信息替换到代码文件中。
    3. 依赖外部命令:如果我们需要注入 git 信息,需要执行 git 命令获取,这个命令就比较耗时。
    4. 加密信息:如对敏感信息加密使用到了第三方的异步加密库,就只能异步处理了。