为了账号安全,请及时绑定邮箱和手机立即绑定

从零开始用Rust实现Webpack(五):支持自定义JS插件

参考了 mini-webpack,我用 Rust 从零开始实现了一个简单的 Webpack。这让我对 Webpack 的理解更加深入,同时也提升了我的 Rust 技能。真是两全其美!

代码库:https://github.com/ParadeTo/rs-webpack

本文对应的 Pull Request 为:https://github.com/ParadeTo/rs-webpack/pull/6

上一篇文章实现了 Rust 端的插件系统,但如何将用户用 JavaScript 开发的插件集成到 rs-webpack 中的问题尚未解决。本文将介绍如何实现。

如果我开发了一个 JavaScript 插件,怎样才能让它正常工作?

    module.exports = class MyPlugin {
        apply(compiler) {
            compiler.hooks.beforeRun.tap('myplugin', (compiler) => {
                console.log("在运行前,编译器对象", compiler)
            })
        }
    }

点击进入全屏。退出全屏。

接下来,既然我们之前已经提到过 rspack,那么我们就继续沿用它的思路。研究之后,我发现它的方法大致如图所示。

图片描述

我们的自定义 JS 插件 myPlugin 将从 rs-webpack-cli 传递给 rs-webpack-core。在 rs-webpack-core 中,它使用由 rspack 团队开发的 @rspack/lite-tapable 库来创建一个 beforeRun 钩子。

    export class Compiler {
        // bindingRsWebpack 是一个 BindingRsWebpack 实例
        bindingRsWebpack: BindingRsWebpack
        // hooks 是一个包含各种钩子的配置对象
        hooks: {
            // beforeRun 是一个 SyncHook,用于在运行前执行同步操作
            beforeRun: liteTapable.SyncHook<[string]>;
        }
        ...
    }

进入全屏,退出全屏

在初始化 Compiler 时,它会遍历所有插件并执行它们的 apply 方法:

    constructor(props: RawConfig) {
        const {plugins} = props
        plugins.forEach(plugin => {
            plugin.apply(this)
        })
    }

切换到全屏 退出全屏

然后,通过一系列操作,它将一个名为 register_before_run_taps 的函数进行包装,并将这个函数传递给 Rust。register_before_run_taps 这个函数封装了调用 beforeRun 钩子的 call 函数。如下:

    this.registers = {
        registerBeforeRunTaps: this.#createHookRegisterTaps(
            RegisterJsTapKind.BeforeRun,
            () => this.hooks.beforeRun,
            queried => (native: string) => {
                // beforeRun.call 
                queried.call(native);
            }
        ),
    }
    // 创建 BindingRsWebpack 实例,传入 props 和 this.registers 对象
    this.bindingRsWebpack = new BindingRsWebpack(props, this.registers)

进入全屏 退出全屏

执行完此函数后,返回一个数组,数组中的每个元素都可以充当 Rust 里 before_run 钩子的拦截器(仅实现了 call 方法这一部分)。

    #[async_trait]
    impl Interceptor<BeforeRunHook> for RegisterBeforeRunTaps {
      async fn call(
        &self,
        hook: &BeforeRunHook,
      ) -> rswebpack_error::Result<Vec<<BeforeRunHook as Hook>::Tap>> {
        // 如果某些不可跳过的注册存在,并且这些注册不是不可跳过的,返回一个空的Vec。
        if let Some(non_skippable_registers) = &self.inner.non_skippable_registers {
          if !non_skippable_registers.is_non_skippable(&RegisterJsTapKind::BeforeRun) {
            return Ok(Vec::new());
          }
        }
        // 调用内部的call_register方法,获取js_taps。
        let js_taps = self.inner.call_register(hook).await?;
        // 将获取到的js_taps进行处理,创建一个新的Vec。
        let js_taps = js_taps
          .iter()
          .map(|t| Box::new(BeforeRunTap::new(t.clone())) as <BeforeRunHook as Hook>::Tap)
          .collect();
        Ok(js_taps)
      }
    }

全屏 退出

在 rswebpack_binding 中,这些拦截器是通过 JsHooksAdapterPlugin 应用的。

    impl Plugin for JsHooksAdapterPlugin {
      fn 名称(&self) -> &'static str {
        "rspack.JsHooksAdapterPlugin"
      }

      fn 应用(&self, _ctx: PluginContext<&mut ApplyContext>) -> rswebpack_error::Result<()> {
        _ctx
          .context
          .compiler_hooks
          .before_run
          .intercept(self.register_before_run_taps.clone());
      }
    }

全屏显示;退出全屏

PS:每次Hook的call函数被调用时,拦截器中的call函数也会被执行。例如,在以下示例中:

    const hook = new SyncHook(['arg1', 'arg2'])

    hook.tap('test', (...args) => {
      控制台输出('打印 "test" 和参数', ...args)
    })

    hook.intercept({
      // 在调用时触发
      call: (...args) => {
        控制台输出('执行拦截器调用', ...args)
      },
    })

    hook.call('a1', 'a2')

    // 输出
    执行拦截器调用 a1 a2
    打印 "test" 和参数 a1 a2

全屏 退出全屏

当 Rust 中的 before_run 调用 call 时,这些拦截器的 call 函数也会被执行,然后这些拦截器包裹的 beforeRun.call 也会在 JS 侧被执行,从而触发 myPlugin 中相应 Tap 函数的执行。

通过这些步骤,整个插件系统就完成了。完整的更改可以在这里查看 here。我不会一一讲解代码,但是按照我们之前提到的顺序,你应该就能理解了。

这套文章的初衷是通过重新实现来加深对 webpack 的理解程度。然而,我发现我的 Rust 技能还不够熟练,还不足以实现 Plugin 系统。大部分时间都花在了 Rspack 的集成上。

在这个过程中,我意识到有很多地方我理解得不是很透彻,我打算把这些地方记下来,以后再学学。

  • Napi,比如ThreadsafeFunction。把Napi和Node.js结合可以做很多事情。以后我会试着找一些例子。
  • Rust的异步处理以及tokio。
  • Rust中的并发编程:多线程、通道等。
  • Rust中的宏编程编写和调试都很费劲。

麻烦您给我点个赞,非常感谢!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消