Rspack构建之极致体验

TL;DR

得益于Rust的惊人速度,NAPI-RS支持Rust构建预编译Node.js原生扩展,SWC解析、编译、打包JS,类似配置下,Rspack能极大提升构建速度。

对于中后台的巨石应用,Rspack值得一试。

在某中后台项目中,按照基本配置,即Vue框架三件套等在项目内构建,也就是没有配置external的情况下,Rspack、Vite、Wepack的BUILD构建速度分别是4.54s、13s、34.5s。

配置项很好上手,与webpack.config.js大同小异。

rspack.config.js

js 复制代码
const rspack = require('@rspack/core');
const { VueLoaderPlugin } = require('vue-loader'); // version: ^15.11.1
var path = require('path');

function resolve(dir) {
  return path.join(__dirname, '.', dir);
}
const config = {
  context: __dirname,
  entry: {
    main: './src/main.js',
  },
  devServer: {
    client: {
      overlay: true,
      logging: 'info',
    },
    historyApiFallback: true,
    port: 'auto',
    proxy: {
      '/index.php': {
        target: 'https://fin-invoicep-stg.huolala.work',
        changeOrigin: true,
      },
    },
  },
  devtool: false,
  plugins: [
    new VueLoaderPlugin(),
    new rspack.HtmlRspackPlugin({
      template: 'public/index.html',
    }),
    new rspack.CopyRspackPlugin({
      patterns: [
        {
          from: 'public',
          globOptions: {
            ignore: ['**/index.html'],
          },
        },
      ],
    }),
  ],
  experiments: {
    css: false, // 此时需要关闭 `experiments.css` 以适配 `vue-loader` 内部处理逻辑
  },
  resolve: {
    alias: {
      vue$: 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    },
    extensions: ['*', '.js', '.vue', '.json'],
  },
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['vue-style-loader', 'css-loader'],
      },
      {
        test: /.vue$/,
        use: [
          {
            loader: 'vue-loader',
            options: {
              experimentalInlineMatchResource: true,
              loaders: {
                less: ['vue-style-loader', 'css-loader', 'less-loader'],
              },
            },
          },
        ],
      },
      {
        test: /.less$/,
        use: ['vue-style-loader', 'css-loader', 'less-loader'],
        type: 'javascript/auto',
      },
      {
        test: /.(png|jpg|gif)$/,
        type: 'asset/inline',
      },
      {
        test: /.(eot|svg|ttf|woff|woff2)$/,
        type: 'asset/resource',
      },
    ],
  },
};

module.exports = config;

Tips

  • Require is undefined, 需要改成import引入
  • css解析报错,需要配置 experiments: { css: false, // 此时需要关闭 `experiments.css` 以适配 `vue-loader` 内部处理逻辑 }

原理

  1. Rspack中,Rust和Node如何通信?何时进行交互?
  2. Why so fast?

特性

  • 基于Rust
  • SWC替换babel构建
  • 基于NAPI-RS, Rust和JS高效通信

rspack-cli

同其他打包工具类似的流程,经历了registerCommands--->createCompiler---> compiler.run一系列流程。

在rspack-cli内部,其关键compiler流程如下:

createCompiler

整合rspack.config.js中各种参数,并且触发相应阶段的hooks

ini 复制代码
const compiler = await cli.createCompiler(rspackOptions, "build", errorHandler);
   if (!compiler)
   return;
   if (cli.isWatch(compiler)) {
      return;
   }
   else {
      compiler.run(errorHandler);
   }

compiler.build

调用build函数,并统计打包时长、触发done钩子

kotlin 复制代码
this.build((err) => {
  if (err) {
    return finalCallback(err);
  }
  this.compilation.startTime = startTime;
  this.compilation.endTime = Date.now();
  const stats = new Stats_1.Stats(this.compilation);
  this.hooks.done.callAsync(stats, (err) => {
    if (err) {
      return finalCallback(err);
    } else {
      return finalCallback(null, stats);
    }
  });
});

build函数:

javascript 复制代码
  build(callback: (error: Error | null) => void) {
        this.#getInstance((error, instance) => {
            if (error) {
                return callback && callback(error);
            }
            const unsafe_build = instance?.unsafe_build;
            const build_cb = unsafe_build?.bind(instance) as typeof unsafe_build;
            build_cb?.(error => {
                if (error) {
                    callback(error);
                } else {
                    callback(null);
                }
            });
        });
    }

从此处,进入rspack rust编译流程。

javascript 复制代码
#getInstance(
        callback: (error: Error | null, instance?: binding.Rspack) => void
    ): void {
        // ...etc..
        const instanceBinding: typeof binding = require("@rspack/binding");

        this.#_instance = new instanceBinding.Rspack(
            rawOptions,
            this.builtinPlugins,
            {    
             // different hooks, like beforeCompile afterCompile finishMake make shouldEmit emit afterEmit
                assetEmitted: this.#assetEmitted.bind(this),
            },
            createThreadsafeNodeFSFromRaw(this.outputFileSystem),
            runLoaders.bind(undefined, this)
        );

        callback(null, this.#_instance);
    }

其源码中crates/node_binding利用NAPI-RS承担了Rust和JS通信的职责。

NAPI-RS

一个使用 Rust 构建预编译 Node.js 原生扩展的框架

极简版:

rust 复制代码
// libs.rs
#![deny(clippy::all)]

use napi_derive::napi;

#[napi]
// 定义一个plus_100的函数
pub fn plus_100(input: u32) -> u32 {
  input + 100
}

// build.rs
extern crate napi_build;

fn main() {
  napi_build::setup();
}

// package.json
"napi": {
    "name": "package-template", // package-name
    "triples": {
      "defaults": true,
      "additional": [
        "x86_64-unknown-linux-musl",
        "aarch64-unknown-linux-gnu",
        "i686-pc-windows-msvc",
        "armv7-unknown-linux-gnueabihf",
        "aarch64-apple-darwin",
        "aarch64-linux-android",
        "x86_64-unknown-freebsd",
        "aarch64-unknown-linux-musl",
        "aarch64-pc-windows-msvc",
        "armv7-linux-androideabi"
      ]
    }
  },

经过napi build --platform --release --pipe "prettier -w"打包后会生成package-template.android-arm64.node文件,再在js文件中调用就可以使用plus_100方法了。

javascript 复制代码
const nativeBinding = require('./package-template.android-arm64.node')
const { plus100 } = nativeBinding

rspack node_binding

此处暴露出的Rspack.build方法,被上文提到的compiler.build阶段调用。

rust 复制代码
#[napi]
impl Rspack {
  #[napi(constructor)]
  pub fn new() {}
  
  #[napi(
    catch_unwind,
    js_name = "unsafe_build",
    ts_args_type = "callback: (err: null | Error) => void"
  )]
  pub fn build(&self, env: Env, f: JsFunction) -> Result<()> {
      let handle_build = |compiler: &mut Pin<Box<rspack_core::Compiler<_>>>| {
      // Safety: compiler is stored in a global hashmap, so it's guaranteed to be alive.
      let compiler: &'static mut Pin<Box<rspack_core::Compiler<AsyncNodeWritableFileSystem>>> =
        unsafe { std::mem::transmute::<&'_ mut _, &'static mut _>(compiler) };

      callbackify(env, f, async move {
        compiler
          .build()
          .await
          .map_err(|e| Error::new(napi::Status::GenericFailure, format!("{e}")))?;
        tracing::info!("build ok");
        Ok(())
      })
    };
    unsafe { COMPILERS.borrow_mut(&self.id, handle_build) }
  }
 }

Why so fast

rspack_core模块中大量使用SWC API做词法、语法解析、编译、打包等,并且相比于Babel,SWC是基于Rust的高性能JS/TS解析器。

比如在文首的rspack.config.js中,我们使用的rspack.HtmlRspackPlugin插件,在rspack_plugin_html这个模块中,使用了swc_core、swc_html、swc_html_minifier等模块。

rust 复制代码
use swc_core::common::{sync::Lrc, FileName, FilePathMapping, SourceFile, SourceMap, GLOBALS};
use swc_html::{
  ast::Document,
  codegen::{
    writer::basic::{BasicHtmlWriter, BasicHtmlWriterConfig},
    CodeGenerator, CodegenConfig, Emit,
  },
  parser::{error::Error, parse_file_as_document, parser::ParserConfig},
};
use swc_html_minifier::minify_document;
pub use swc_html_minifier::option::MinifyOptions;

impl<'a> HtmlCompiler<'a> {
  pub fn new(config: &'a HtmlRspackPluginOptions) -> Self {
    Self { config }
  }

  pub fn parse_file(&self, path: &str, source: String) -> Result<TWithDiagnosticArray<Document>> {
    let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
    let fm = cm.new_source_file(FileName::Custom(path.to_string()), source);

    let mut errors = vec![];
    let document = parse_file_as_document(fm.as_ref(), ParserConfig::default(), &mut errors);
    // ...
    document
      .map(|doc| doc.with_diagnostic(diagnostics))
      .map_err(|e| html_parse_error_to_traceable_error(e, &fm))
  }

  pub fn codegen(&self, ast: &mut Document) -> anyhow::Result<String> {
    // ...
    if self.config.minify {
      // Minify can't leak to user land because it doesn't implement `ToNapiValue` Trait
      GLOBALS.set(&Default::default(), || {
        minify_document(ast, &MinifyOptions::default());
      })
    }
    // ...
    gen.emit(ast).map_err(|e| anyhow::format_err!(e))?;
    Ok(output)
  }
}

总而言之,对于构建速度较慢的巨石中后台应用,目前来看rspack是一个比较值得尝试的应用。

冲啊!

参考资料

相关推荐
anyup_前端梦工厂2 小时前
了解几个 HTML 标签属性,实现优化页面加载性能
前端·html
前端御书房2 小时前
前端PDF转图片技术调研实战指南:从踩坑到高可用方案的深度解析
前端·javascript
2301_789169542 小时前
angular中使用animation.css实现翻转展示卡片正反两面效果
前端·css·angular.js
风口上的猪20153 小时前
thingboard告警信息格式美化
java·服务器·前端
程序员黄同学3 小时前
请谈谈 Vue 中的响应式原理,如何实现?
前端·javascript·vue.js
爱编程的小庄4 小时前
web网络安全:SQL 注入攻击
前端·sql·web安全
宁波阿成5 小时前
vue3里组件的v-model:value与v-model的区别
前端·javascript·vue.js
柯腾啊5 小时前
VSCode 中使用 Snippets 设置常用代码块
开发语言·前端·javascript·ide·vscode·编辑器·代码片段
weixin_535854225 小时前
oppo,汤臣倍健,康冠科技,高途教育25届春招内推
c语言·前端·嵌入式硬件·硬件工程·求职招聘
扣丁梦想家5 小时前
设计模式教程:装饰器模式(Decorator Pattern)
java·前端·装饰器模式