基于本文回答

播面 播面

文解图解播客视频,多维讲透八股文
0
评论

Loader 的执行顺序是怎样的?为什么设计成从右到左(或从下到上)执行?

在 Webpack 中,Loader 的执行顺序和设计原理可以拆分为以下几个核心要点:


一、 Loader 的执行顺序

简而言之:从右到左,从下到上(从后往前)

1. 基础示例

如果在 webpack.config.js 中配置如下:

javascript
module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          'style-loader', // 第 3 个执行
          'css-loader',   // 第 2 个执行
          'sass-loader'   // 第 1 个执行
        ]
      }
    ]
  }
};

处理 .scss 文件时的实际执行流程是:

  1. sass-loader:将 SCSS 语法编译为 CSS。
  2. css-loader:解析 CSS 中的 @importurl(),将其转换为 CommonJS 模块。
  3. style-loader:创建 <style> 标签,将 CSS 注入到 HTML 的 <head> 中。

2. 进阶:完整的 Loader 执行生命周期(Pitch 阶段与 Normal 阶段)

Webpack 在处理 Loader 时,实际上分为两个阶段:Pitching 阶段Normal (执行) 阶段

plaintext
|- compiler
|  |- style-loader `pitch`
|    |- css-loader `pitch`
|      |- sass-loader `pitch`
|        |- 读取文件内容 (Resource)
|      |- sass-loader (normal execution)
|    |- css-loader (normal execution)
|  |- style-loader (normal execution)
  • Pitch 阶段(从左到右 / 从上到下): Webpack 会先依次调用 Loader 上的 pitch 方法(如果有)。
  • Normal 阶段(从右到左 / 从下到上): 真正接收文件内容并进行转换的阶段。
  • 短路机制: 如果某个 Loader 的 pitch 方法返回了值,Webpack 会跳过后续的 Pitch 和文件读取,直接倒退回执行前一个 Loader 的 Normal 阶段。

3. 优先级配置(enforce

除了数组中的顺序,还可以通过 enforce 属性人为改变 Loader 的类型和优先级:

  1. pre Loader(前置)
  2. normal Loader(普通,默认)
  3. inline Loader(行内,写在 import 语句里的)
  4. post Loader(后置)

Pitch 阶段执行顺序: post -> inline -> normal -> pre
Normal 阶段执行顺序: pre -> normal -> inline -> post


二、 为什么设计成“从右到左”?

这种看似“反直觉”的设计,其实有着深刻的数学和编程语言学依据:

1. 函数组合(Functional Composition)

这是最核心的原因。在函数式编程(Functional Programming)中,有一个经典的原理叫函数组合(compose

假设我们有三个函数:

  • f(x)f(x)
  • g(x)g(x)
  • h(x)h(x)

如果我们想先用 hh 处理数据,再用 gg,最后用 ff,数学上写作:
y=f(g(h(x)))y = f(g(h(x)))

在 JS 代码中表达这种嵌套关系就是:

javascript
styleLoader(cssLoader(sassLoader(sourceCode)));

可以看到,最先作用于原始代码(sourceCode)的 sassLoader 被包裹在最里层(最右侧),而最后执行的 styleLoader 在最外层(最左侧)

Webpack 的 Loader 链就是这种函数组合的体现:每个 Loader 都是一个纯函数,上一个 Loader 的输出作为下一个 Loader 的输入。

2. 符合 compose 函数的实现惯例

在许多前端函数库(如 Redux、Lodash、Ramda)中,compose 函数的实现逻辑就是从右向左执行的:

javascript
// Redux 中的 compose 实现简写
const compose = (...funcs) => {
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
};

// 执行 compose(f, g, h)(x) 相当于 f(g(h(x)))

注:如果想从左往右执行,在函数式编程中通常叫 pipe(管道)。 Webpack 选择了 compose 的思想,而不是 pipe

3. 语义化:靠近文件的 Loader 最先处理文件

从直观配置的角度来看:
[style-loader, css-loader, sass-loader]

  • sass-loader 紧贴着原始的 .scss 文件,所以它最先处理文件。
  • 处理完后向上/向左抛出,交给 css-loader
  • 最后抛给 style-loader 产出最终结果。

这种栈(Stack)结构的“后进先出”机制,保证了最底层的抽象(最基础的文件解析)最先被执行。


总结

  1. 顺序规则:Loader 在 Normal 阶段 从右往左(从下往上)执行;在 Pitch 阶段 从左往右执行。
  2. 设计原因:遵循函数式编程中的 函数组合(Compose) 理念,数学表达为 Style(Css(Sass(code))),使得前一个 Loader 的输出自然地成为下一个 Loader 的输入。