Rspack Loader 架构原理:从 Loader Runner 到 Rust Loader Pipeline

背景

在面试的时候,面试官常常会问:webpack 的 Loader 和 Plugin 有什么区别?哈哈,不知道你还能不能回答得了。发现没,Loader 和 Plugin 常被并列讨论,因为它们都是扩展构建流程、改变最终产物的核心机制,另外,估计很多人不知道,其实 Loader 的执行是通过插件机制来实现对模块的处理

在上一篇文章《Rspack 插件架构原理:从 Tapable 到 Rust Hook》中,我们深入剖析了 Rspack 如何兼容 webpack 的插件体系,并通过 Rust 实现高性能的 Hook 系统。

那么 Loader 是不是和 Plugin 类似,采用了类似的兼容策略?Rspack 又是如何在 Rust 中实现自己的 Loader 机制的?

在 Rspack 的 JavaScript 端 Compiler 构造函数中,有这样一行关键代码:

js 复制代码
new JsLoaderRspackPlugin(this).apply(this);

这个 JsLoaderRspackPlugin 在整个 Loader 架构设计中扮演着什么角色?当 Rust 层需要执行 JavaScript 编写的 Loader 时,底层是如何调用?

带着这些问题,本文将带你深入 Rspack 的 Loader 相关的源码,逐步拆解其 Loader 架构的设计原理。另外你还将会收获:

  • Rspack 是如何兼容 webpack Loader?
  • Rust Loader Pipeline 是什么?它和 JavaScript Loader Runner 有什么不同?
  • JavaScript Loader 是如何注册进来的?
  • 当 Rust 层执行到某个 Loader 时,如何判断是否需要切换到 JavaScript Loader?

本篇文章的编写思路是这样,先回顾 Loader 的基本概念,让你复习一下基础,然后通过绘制整体架构与数据流程图让你建立全局的视角,最后结合整体架构图的每个阶段从代码入口出发,逐层剖析 Rspack Loader 的运行机制。

什么是 Loader

引用 Rspack 官方定义:Loader 是一种模块转换器,用于将各种类型的资源模块转换为 Rspack 能够理解和打包的格式。通过配置不同的 Loader,你可以扩展 Rspack 的能力,使其支持 JSX、Markdown、Sass、Less 等非标准 JavaScript 模块。

简而言之,Loader 是一个带有副作用的内容转译器。它接收原始文件内容,经过处理后输出新的内容(或元数据),供后续构建流程使用。

和 Plugin 类似,Loader 也根据使用场景分为多种类型,Rspack 兼容 webpack 的 Loader 规范,支持以下几种常见形式:

同步 Loader(Sync Loader)

这是最基础的 Loader 类型,可通过 returnthis.callback() 同步返回结果:

js 复制代码
module.exports = function (source, map?, meta?) {  
  return someSyncOperation(source);
};

虽然 return 写法简洁,但 this.callback() 更灵活,支持传递错误、source map 和元数据:

js 复制代码
module.exports = function (source, map, meta) {  
  this.callback(null, someSyncOperation(source), map, meta);  
  // 调用 callback() 时需要返回 undefined,避免返回值冲突  
  return;
};

异步 Loader(Async Loader)

当需要执行异步操作(如读取文件、发起网络请求等)时,应使用异步 Loader。通过调用 this.async() 获取回调函数:

js 复制代码
module.exports = function (source, map, meta) {
  // 获取异步回调函数
  const callback = this.async();

  // 执行异步操作
  someAsyncOperation(source, function (err, result) {
    // 处理错误情况
    if (err) return callback(err);
    // 成功时返回处理结果
    callback(null, result, map, meta);
  });
};

异步回调同样支持传递 source map 和 meta 数据,确保构建信息完整传递。

ESM Loader

Rspack 支持使用 ES Module 语法编写 Loader。只需通过 export default 导出处理函数,并满足以下任一条件:

  • 文件扩展名为 .mjs
  • 或在 package.json 中设置 "type": "module"

例如:

js 复制代码
// loader.mjs
export default function (source) {
  return transform(source);
}

Raw Loader

默认情况下,Rspack 会将文件内容以 UTF-8 字符串形式传入 Loader。但在处理二进制资源(如图片、字体、音频)时,需要直接操作原始 Buffer。

此时,可在 Loader 模块中导出 raw: true,告知 Rspack 以 Buffer 形式传递输入:

js 复制代码
function rawLoader(content) {
  // content 是 Buffer 类型
  return processBinary(content);
}

rawLoader.raw = true;
module.exports = rawLoader;

Pitching Loader

Rspack 的 Loader 执行分为两个阶段:

  • Normal 阶段:从右向左依次执行每个 Loader 的主函数;
  • Pitch 阶段 :从左向右提前执行每个 Loader 的 pitch 方法(如果存在)。

Pitch 阶段常用于跳过后续 Loader(例如缓存命中时),或仅基于资源路径做预处理,而不依赖前序 Loader 的输出。

Pitch 函数签名如下:

js 复制代码
module.exports.pitch = function (remainRequest, precedingRequest, data) {
  // ...
};

包含三个参数:

  • remainingRequest:当前 Loader 之后尚未处理的请求链(包括资源路径和后续 Loader);
  • previousRequest:当前 Loader 之前已经处理过的请求链;
  • data:一个可在 pitchnormal 阶段之间共享的上下文对象。

这种双向执行机制赋予了 Loader 极高的灵活性,是高级资源处理的关键手段。

以上内容大部分是基于 Rspack 官方文档整理的。如你需深入了解,建议查阅 Rspack 官网 Loader 文档

整体架构概览

还记得在《Rspack 原理:webpack,我为什么不要你》文章中,我们介绍了 Rspack 的核心执行流程,主要分为初始化阶段、桥接阶段、构建阶段和生成阶段。Loader 的执行贯穿于这些阶段之中,通过插件机制实现对模块的处理。

下图展示了 Rspack 在整个构建过程中 Loader 执行流程的整体架构:

以上流程图呈现了从构建初始化到模块编译和 Loader 执行相关的关键步骤。我们对照流程图逐阶段解释下:

初始化阶段

此阶段主要完成 JavaScript 编译器(Compiler)的创建和基础插件的注册。执行流程如下:

  • 调用 createCompiler() 创建编译器实例;
  • 在 Compiler 构造函数里实例化 JsLoaderRspackPlugin 插件并加入到 this.#builtinPlugins 数组;
  • runLoaders 方法绑定至 Compiler 上,作为后续 Rust 层执行 JavaScript Loader 的统一入口。

桥接阶段

本阶段是 Rspack 实现 JavaScript 与 Rust 双端协同的关键环节,负责连接 JavaScript 层与 Rust 层。Rspack 利用该阶段传递了关键参数 this.#builtinPlugins。执行流程如下:

  • 创建 JsCompiler 实例;
  • 传入 this.#builtinPlugins 作为 JsCompiler 的第三个参数传递给 Rust 层;
  • 实例化 JsLoaderRunnerGetter,用于获取 JavaScript Loader;
  • 创建 Rust 层的 JsLoaderRspackPlugin,并注册内置的 Rust Loader 插件;
  • Rust 端的 JsLoaderRspackPluginapply() 方法中注册多个关键 Hooks(loader_should_yieldloader_yield 等),用于在编译流程中判断和执行 JavaScript Loader。

构建阶段

这是 Loader 实际执行的核心阶段,发生在模块构建过程中。执行流程如下:

  • 触发 compilation.make(),开始模块解析与构建;
  • 调用 NormalModule.build() 对每个模块进行处理;
  • 创建 RspackLoaderRunnerPlugin,负责协调 Loader 的执行逻辑;
  • 调用 run_loaders() 方法,启动 Loader 链的执行流程。

以上涉及的调用方法比较多,一下子可能不太好理解,不过没关系,这里你只需要有个全局概念就行,大概知道每个阶段都和 Loader 执行有哪些关系,接下来我会逐一剖析以上每个阶段关键方法的执行细节。

插件注册与桥接机制

JsLoaderRspackPlugin

在初始化阶段中,我们介绍到了 JsLoaderRspackPluginJsLoaderRspackPlugin 是 Rspack 实现 JavaScript Loader 兼容性的内置插件。它的作用是:为 Rust 构建引擎提供一个回调入口,用于在需要时执行用户编写的 JavaScript Loader。它的实例化发生在 Compiler 构造函数:

js 复制代码
class Compiler {
  constructor(context: string, options: RspackOptionsNormalized) {
    ...
    new JsLoaderRspackPlugin(this).apply(this);
    ...
  }
}

这行代码完成了两件事:

  • 创建插件实例;
  • 立即调用其 apply(compiler) 方法,将插件逻辑注入当前编译器上下文。

JsLoaderRspackPlugin 和传统的插件定义不一样,是通过 Rspack 内部的 create 工具函数生成的内置插件:

js 复制代码
export const JsLoaderRspackPlugin = create(
  BuiltinPluginName.JsLoaderRspackPlugin,
  (compiler: Compiler) => runLoaders.bind(null, compiler),
  /* Not Inheretable */
  "thisCompilation"
);

其中最关键的部分是第二个参数:

js 复制代码
(compiler: Compiler) => runLoaders.bind(null, compiler)

它返回一个 预绑定 compiler 实例的 runLoaders 函数,这个函数具有以下特点:

  • 通过 bind 方法实现函数预设参数,始终引用 compiler 参数;
  • 这个绑定后的函数后续会被传递给 Rust 侧,作为执行 JavaScript Loader 的回调入口;
  • 在 Rust 需要运行某个 JavaScript Loader 时,只需调用 compiler._runLoader(loaderContext),无需再显式传入 compiler

这种设计实现了闭包式上下文绑定,确保 JavaScript 回调始终作用于正确的编译器实例。

最终,JsLoaderRspackPlugin 会被注册到内部插件列表 this.#builtinPlugins 中:

js 复制代码
__internal__registerBuiltinPlugin(plugin: binding.BuiltinPlugin) {
  this.#builtinPlugins.push(plugin);
}

这个数组充当了 JavaScript 层与 Rust 层之间的桥梁。在创建底层 Rust 编译器实例时,this.#builtinPlugins 会作为 JsCompiler 的第三个参数传递给 Rust 端:

Rust 端如何处理插件?

JsLoaderRspackPlugin 插件传递到 Rust 端后,Rspack 如何将其与自身的构建流程集成?特别是,Rust 原生插件与 JavaScript Loader 是如何协同工作的?答案在于 Rspack 自研的 Rust Loader Pipeline 机制

在 Rust 端,被 JavaScript 端调用的 JsCompiler::new 接收插件列表并进行处理:

js 复制代码
#[napi]
impl JsCompiler {
  pub fn new(
    // ...
    builtin_plugins: Vec<BuiltinPlugin>,  // 接收插件列表
    // ...
  ) -> Result<Self> {
    // ...
    for bp in builtin_plugins { // 遍历处理每个插件
      bp.append_to(env, &mut this, &mut plugins)?; // 调用 append_to
    }
    // ...
  }
}

append_to 方法会处理每个插件,对于 JsLoaderRspackPluginappend_to 会执行以下操作:

js 复制代码
match name {
   ...
   BuiltinPluginName::JsLoaderRspackPlugin => {
     // 1. 将 JS 的 runLoaders 函数绑定到 compiler._runLoader
     compiler_object.set_named_property("_runLoader", self.options)?;
     // 2. 创建 JsLoaderRunnerGetter(用于跨语言调用)
     let loader_runner_getter = JsLoaderRunnerGetter::new(&env)?;
     // 3. 创建 Rust 插件实例并加入插件列表
     plugins.push(JsLoaderRspackPlugin::new(loader_runner_getter).boxed());
   }
   ...
}

注意以上的 compiler_object 是 JavaScript 的 compiler 实例。

我们直接看代码注释来理解,以上代码的执行引出了两个 Rust 关键组件:JsLoaderRunnerGetterJsLoaderRspackPlugin

由于 Rust 与 JavaScript 运行在不同的执行环境,Rspack 必须解决如何让 Rust 安全调用 JavaScript 函数的问题。解决方案是使用 NAPIThreadsafeFunction ,它允许在 Rust 的异步线程中安全地调用 JavaScript 函数。

JsLoaderRunnerGetter

JsLoaderRunnerGetter 负责从 JavaScript 端获取 runLoaders 函数,并封装为可在 Rust 中调用的形式:

js 复制代码
impl JsLoaderRunnerGetter {
  pub fn new(env: &Env) -> napi::Result<Self> {
    // 创建一个 NAPI threadsafe function
    // 这个函数可以在 Rust 的异步环境中安全地调用 JavaScript
    let mut ts_fn = ptr::null_mut();
    check_status!(
      unsafe {
        sys::napi_create_threadsafe_function(
          raw_env,
          ptr::null_mut(),
          ptr::null_mut(),
          async_resource_name,
          0,
          1,
          ptr::null_mut(),
          None,
          ptr::null_mut(),
          Some(napi_js_callback),  // 回调函数
          &mut ts_fn,
        )
      },
      "Failed to create threadsafe function"
    )?;
    Ok(Self { ts_fn })
  }

JsLoaderRunnerGetter 通过 new() 函数创建一个 ThreadsafeFunction 对象,并将其保存在 JsLoaderRunnerGetter 实例中。

napi_create_threadsafe_function 的其他参数涉及到 NAPIThreadsafeFunction 相关知识,这里我们先忽略,我们只需要关注 Some(napi_js_callback) 参数就行。

注意:此时并没有真正拿到 runLoaders 函数。这个 ThreadsafeFunction 的回调是 napi_js_callback,它会在未来的某个时刻被触发。

napi_js_callback 会在 JavaScript 线程 中执行,它的作用是从 JavaScript 获取 _runLoader

js 复制代码
extern "C" fn napi_js_callback(
  env: sys::napi_env,
  _js_callback: sys::napi_value,
  _context: *mut c_void,
  data: *mut c_void,
) {
  // 1. 获取 compiler 对象(通过 weak reference)
  let compiler_object = unsafe {
    let napi_value = ToNapiValue::to_napi_value(env, weak_reference.clone())?;
    Object::from_napi_value(env, napi_value)?
  };
  
  // 2. 从 compiler._runLoader 拿到 JavaScript 函数
  let run_loader = compiler_object
    .get_named_property::<Function<JsLoaderContext, Promise<JsLoaderContext>>>("_runLoader")?;
  
  // 3. 把这个 JavaScript 函数也包装成一个新的 ThreadsafeFunction(JsLoaderRunner),可以在 Rust 中调用
  let ts_fn: JsLoaderRunner = run_loader
    .build_threadsafe_function::<JsLoaderContext>()
    .weak::<true>()
    .callee_handled::<false>()
    .max_queue_size::<0>()
    .build()?;
    
  // 4. 把这个新的 ThreadsafeFunction 传回给 Rust
  Ok(ts_fn)
}

当我们在 Rust 中调用这个 ThreadsafeFunction 时,NAPI 会把 napi_js_callback 排队到 JavaScript 主线程执行。

在这个回调里,我们才真正访问 compiler._runLoader,并把它转换成另一个 可被 Rust 调用的 ThreadsafeFunction(即以上的 JsLoaderRunner)。

JsLoaderRunnerGetter.call() 会触发上述流程,拿到最终的 JsLoaderRunner

js 复制代码
pub async fn call(&self, compiler_id: &CompilerId) -> napi::Result<JsLoaderRunner> {
  // 1. 调用创建的 ThreadsafeFunction(即触发 napi_js_callback)
  unsafe {
    let _ = napi_call_threadsafe_function(
      self.ts_fn, // 这就是刚才通过 new() 创建的 ThreadsafeFunction
      Box::into_raw(Box::new(data)).cast(),
      sys::ThreadsafeFunctionCallMode::nonblocking,
    );
  }
  
  // 2. 等待结果
  let result = rx.await.to_napi_result()?;
  let loader_runner = result?;
  Ok(loader_runner)  // 返回可以在 Rust 中调用的 JavaScript 函数
}

后续就可以直接用它来执行 JavaScript Loader。

JsLoaderRspackPlugin

JsLoaderRspackPlugin 是 Rspack 中实现 JavaScript Loader 兼容性的调度插件。它本身不直接执行任何加载逻辑,而是通过 Hook 系统,在构建流程中动态判断是否需要切换到 JavaScript 环境,并借助 JsLoaderRunnerGetter 调用 JavaScript Loader,最终将执行结果同步回 Rust 构建上下文。

在插件初始化阶段,JsLoaderRspackPlugin 会向模块构建流程注册两个关键的 Hook:

js 复制代码
fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
  // Hook 1:判断当前 loader 是否需要让出控制权给 JavaScript
  ctx
    .normal_module_hooks
    .loader_should_yield
    .tap(scheduler::loader_should_yield::new(self));

  // Hook 2:实际执行 JavaScript Loader
  ctx
    .normal_module_hooks
    .loader_yield
    .tap(scheduler::loader_yield::new(self));

  Ok(())
}

解释下这两个关键 Hook:

  • loader_should_yield:判断当前 Loader 是否需要切换到 JavaScript 执行;
  • loader_yield:负责实际调用 JavaScript Loader 并处理其返回结果。

当 Rust Loader Pipeline 执行到某个 Loader 时,loader_should_yield 会被调用,用来判断是否需要切换到 JavaScript Loader:

js 复制代码
#[plugin_hook(NormalModuleLoaderShouldYield for JsLoaderRspackPlugin, tracing=false)]
pub(crate) async fn loader_should_yield(
  &self,
  loader_context: &LoaderContext<RunnerContext>,
) -> Result<Option<bool>> {
  match loader_context.state() {
    LoaderState::Pitching => {
      let current_loader = loader_context.current_loader();
      if current_loader.request().starts_with(BUILTIN_LOADER_PREFIX) {
        Ok(Some(false))  // 内置 Loader,由 Rust 直接处理
      } else {
        Ok(Some(true))   // 用户自定义 JavaScript Loader,需 yield 到 JavaScript Loader
      }
    }
    LoaderState::Normal => Ok(Some(
      !loader_context.current_loader().request().starts_with(BUILTIN_LOADER_PREFIX),
    )),
  }
}

Rust 是通过 #[plugin_hook] 宏注册 Hook。

这里的判断逻辑很简单:

  • 若 Loader 的请求路径以 builtin: 开头(例如 builtin:css-loader),则视为 Rust 原生内置 Loader,无需切换;
  • 否则,视为 JavaScript 编写的 Loader,需暂停 Rust 流程,交由 JavaScript 引擎执行。

一旦判定需要执行 JavaScript Loader,Rspack 就会调用 loader_yield,完成完整的跨语言调用流程:

js 复制代码
#[plugin_hook(NormalModuleLoaderShouldYield for JsLoaderRspackPlugin, tracing=false)]
pub(crate) async fn loader_yield(
  &self,
  loader_context: &mut LoaderContext<RunnerContext>,
) -> Result<()> {
  // 1. 获取或初始化 JavaScript Loader Runner
  let runner = runner
    .get_or_try_init(|| async {
      let compiler_id = self.compiler_id.get().unwrap();
      self.runner_getter.call(compiler_id).await  // 使用 JsLoaderRunnerGetter 获取
    })
    .await?;

  // 2. 调用 JavaScript 的 runLoaders 函数
  let new_cx = runner
    .call_async(loader_context.try_into()?)  // 调用 JavaScript 函数
    .await?
    .await?;

  // 3. 将执行结果合并回 Rust 上下文
  merge_loader_context(loader_context, new_cx)?;

  Ok(())
}

这个过程分为三步:

  • 获取 Loader Runner:调用 self.runner_getter.call(compiler_id),利用 NAPI ThreadsafeFunction 机制,在 JavaScript 主线程中获取 compiler._runLoader,并封装为可在 Rust 中安全调用的 JsLoaderRunner
  • 调用 JavaScript 函数:将 Rust 的 LoaderContext 转换为 JavaScript 的 JsLoaderContext,然后调用 JavaScript 的 runLoaders 函数;
  • 合并执行结果:将 JavaScript 执行的结果(contentsourceMapadditionalData 等)合并回 Rust 的 LoaderContext

JsLoaderRspackPlugin 通过以上三步,实现了 Rust 高性能 Loader 与 JavaScript Loader 生态兼容。

状态机驱动的 Rust Loader Pipeline

在深入理解 JavaScript Loader 的桥接机制后,其实我们差不多理解了 Rspack 的 Loader 执行机制了,接下来我们再来一起看看 Rspack 内置的 Rust Loader 是如何定义,以及 Rust Loader 的执行流程又是怎样的。

Rspack 构建了一套状态机驱动的 Loader Pipeline,统一管理所有 Loader(无论是 Rust 内置还是 JavaScript 编写的)的执行流程。这种设计不仅保证了执行顺序的正确性,还处理了 Rust 和 JavaScript 之间的切换。

状态机定义

Rspack 的 Loader Pipeline 使用状态机来管理执行流程,定义了五个状态:

js 复制代码
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum State {
  Init,             // 初始状态
  Pitching,         // 正在执行 pitch 阶段(从左到右)
  ProcessResource,  // 正在读取资源文件内容
  Normal,           // 正在执行 normal 阶段(从右到左)
  Finished,         // 执行完成
}

是不是感觉非常熟悉,其实和 JavaScript Loader 一样,以上每个状态都有明确的职责:

  • Init :Pipeline 的入口点,立即转换到 Pitching 状态;
  • Pitching :从左到右执行每个 Loader 的 pitch 方法;
  • ProcessResource :当所有 pitch 阶段完成后,读取原始资源文件内容;
  • Normal :从右到左执行每个 Loader 的 run 方法;
  • Finished:Pipeline 完成,退出循环。

状态的流转是由 transition() 方法控制,确保 Loader Pipeline 始终沿着合法路径推进:

js 复制代码
impl State {
  pub(crate) fn transition(&mut self, next: State) {
    *self = match (*self, next) {
      (State::Init, State::Pitching) => State::Pitching,
      (State::Pitching, State::ProcessResource) => State::ProcessResource,
      (State::Pitching, State::Normal) => State::Normal, // pitch 返回内容,跳过读取
      (State::ProcessResource, State::Normal) => State::Normal,
      (State::Normal, State::Finished) => State::Finished,
      _ => panic!("Unexpected loader runner state (current: {self:?}, next: {next:?})"),
    };
  }
}

以上代码通过显式枚举所有允许的状态转移路径,禁止任何非法跳转。例如:

  • 正常流程中,Pitching 之后应进入 ProcessResource(读取资源);
  • 但如果某个 Loader 的 pitch 函数返回了有效内容,Pipeline 就可以合法地从 Pitching 直接跳转到 Normal,从而跳过资源读取阶段。

任何未被列出的转换(如从 Init 直接到 Finished)都会触发 panic,保障执行流程的正确性。

Pipeline 执行流程

有了以上状态定义和状态流转规则,Pipeline 的核心执行逻辑位于 run_loaders_impl(),通过一个 loop 不断推进状态机:

js 复制代码
async fn run_loaders_impl<Context: Send>(
  cx: &mut LoaderContext<Context>,
  fs: Arc<dyn ReadableFileSystem>,
) -> Result<()> {
  loop {
    match cx.state {
      State::Init => {
        // 这就是前面介绍的 transition 状态转换
        cx.state.transition(State::Pitching);
      }
      State::Pitching => {
        // 从左到右执行每个 Loader 的 pitch
        // 如果遇到 JavaScript Loader,会通过 start_yielding() 切换到 JavaScript
        if cx.start_yielding().await? {
          //  如果 JavaScript Loader 的 pitch 返回了内容,直接跳到 Normal
          if cx.content.is_some() {
            cx.state.transition(State::Normal);
            cx.loader_index -= 1;
          }
          continue;
        }
        ...
      }
      State::ProcessResource => {
        // 读取资源文件内容
        process_resource(cx, fs.clone()).await?;
        ...
      }
      State::Normal => {
        // 从右到左执行每个 Loader 的 run
        // 如果遇到 JavaScript Loader,同样通过 start_yielding() 切换到 JavaScript
        ...
      }
      State::Finished => break,
    }
  }

  Ok(())
}

这个循环会一直执行,直到状态变为 Finished。在每个状态中,都会根据当前状态执行相应的逻辑,例如如果遇到 JavaScript Loader 时,会通过 start_yielding() 切换到 JavaScript Loader,并在适当的时候调用 transition 转换到下一个状态。

完整的调用链:以 NormalModule.build() 开始

看完了以上介绍内容,估计你脑子里已经有点凌乱了,哈哈,没关系,接下来我们以完整的调用链来帮助你梳理整个执行流程,把知识点连接起来。

还记得前面整体架构概览 中呈现的构建流程图吗?在 compilation.make 阶段,Rspack 会为每个模块触发构建,NormalModule.build() 是核心入口。该方法最终会调用 run_loaders(),启动整个 Loader Pipeline。

注意图中创建的 RspackLoaderRunnerPlugin ,等下会介绍到。

具体来说,run_loaders() 会进一步调用 run_loaders_impl()。在 run_loaders_impl 的循环处理中,每当遇到需要执行的 Loader 时,会执行如下逻辑:

js 复制代码
if cx.start_yielding().instrument(span).await? {
   // ...
}

其中 start_yielding() 方法会调用传入的 plugin

js 复制代码
async fn start_yielding(&mut self) -> Result<bool> {
  if let Some(plugin) = &self.plugin
    && plugin.should_yield(self).await?
  {
    plugin.clone().start_yielding(self).await?;
    return Ok(true);
  }
  Ok(false)
}

这里的 plugin 正是在 NormalModule.build() 阶段创建的 RspackLoaderRunnerPlugin(上图中的那个插件)。

RspackLoaderRunnerPlugin 实现了 LoaderRunnerPlugin trait,其 should_yield()start_yielding() 方法会分别触发 Rspack 的插件系统:

js 复制代码
async fn should_yield(&self, context: &LoaderContext<Self::Context>) -> Result<bool> {
  let res = self
    .plugin_driver
    .normal_module_hooks
    .loader_should_yield
    .call(context)
    .await?;

  if let Some(res) = res {
    return Ok(res);
  }

  Ok(false)
}

async fn start_yielding(&self, context: &mut LoaderContext<Self::Context>) -> Result<()> {
  self
    .plugin_driver
    .normal_module_hooks
    .loader_yield
    .call(context)
    .await
}

插件系统会调用所有注册的插件,包括 JsLoaderRspackPlugin。前面我们已经介绍过了 JsLoaderRspackPlugin 通过 #[plugin_hook] 宏注册了这两个 Hook:loader_yieldloader_should_yield

loader_yield 钩子被触发时,JsLoaderRspackPlugin 会通过 JsLoaderRunnerGetter 从 JavaScript 端获取 _runLoaders 函数,并将其包装成线程安全的函数供 Rust 端调用。

总结

这是 Rspack 原理分析系列第四篇文章,说实话 Rust 语法代码看着是真难受,我也很高兴你能坚持看到结尾,开篇我们提到了好几个问题,相信这时的你应该都能一一解答。如果觉得有什么地方遗漏、疑惑,欢迎评论讨论。

最初写 Rspack 原理系列是因为 Rspack 解决了我项目中构建速度慢的痛点,让我对 Rspack 产生了兴趣,后续可能还会继续专注于在这个前端工程化领域。此外,我也计划写一个关于 NestJS 架构设计 的系列,结合 DDD(领域驱动设计) 理念,系统梳理和分享这几年在项目中积累的经验。如果你对此感兴趣,欢迎持续关注。

相关推荐
hen3y25 分钟前
基于 jscodeshift 构建高效 Codemod 工具指南
前端·javascript
烛阴28 分钟前
代码的灵魂:C# 方法全景解析(万字长文,建议收藏)
前端·c#
龙国浪子29 分钟前
🎯 小说笔记编辑中的段落拖拽移动:基于 ProseMirror 的交互式重排技术
前端·electron
iFlow_AI40 分钟前
iFlow CLI快速搭建Flutter应用记录
开发语言·前端·人工智能·flutter·ai·iflow·iflow cli
兔子零102441 分钟前
前端开发实战笔记:为什么从 Axios 到 TanStack Query,是工程化演进的必然?
前端
面向div编程42 分钟前
Vite的知识点
前端
疯狂踩坑人1 小时前
【前端工程化】一文看懂现代Monorepo(npm)工程
前端·npm·前端工程化
JarvanMo1 小时前
Flutter:如何更改默认字体
前端
默海笑1 小时前
VUE后台管理系统:定制化、高可用前台样式处理方案
前端·javascript·vue.js