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是一个比较值得尝试的应用。

冲啊!

参考资料

相关推荐
前端郭德纲1 分钟前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
王解6 分钟前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里7 分钟前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱7 分钟前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster26 分钟前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python
baozhengw28 分钟前
UniAPP快速入门教程(一)
前端·uni-app
nameofworld38 分钟前
前端面试笔试(二)
前端·javascript·面试·学习方法·数组去重
帅比九日1 小时前
【HarmonyOS NEXT】实战——登录页面
前端·学习·华为·harmonyos
摇光931 小时前
promise
前端·面试·promise
麻花20132 小时前
WPF学习之路,控件的只读、是否可以、是否可见属性控制
服务器·前端·学习