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

冲啊!

参考资料

相关推荐
李鸿耀6 分钟前
仅用几行 CSS,实现优雅的渐变边框效果
前端
码事漫谈26 分钟前
解决 Anki 启动器下载错误的完整指南
前端
im_AMBER1 小时前
Web 开发 27
前端·javascript·笔记·后端·学习·web
蓝胖子的多啦A梦1 小时前
低版本Chrome导致弹框无法滚动的解决方案
前端·css·html·chrome浏览器·版本不同造成问题·弹框页面无法滚动
玩代码1 小时前
vue项目安装chromedriver超时解决办法
前端·javascript·vue.js
訾博ZiBo1 小时前
React 状态管理中的循环更新陷阱与解决方案
前端
StarPrayers.2 小时前
旅行商问题(TSP)(2)(heuristics.py)(TSP 的两种贪心启发式算法实现)
前端·人工智能·python·算法·pycharm·启发式算法
一壶浊酒..2 小时前
ajax局部更新
前端·ajax·okhttp
DoraBigHead3 小时前
React 架构重生记:从递归地狱到时间切片
前端·javascript·react.js
彩旗工作室4 小时前
WordPress 本地开发环境完全指南:从零开始理解 Local by Flywhee
前端·wordpress·网站