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` 内部处理逻辑 }
原理
- Rspack中,Rust和Node如何通信?何时进行交互?
- 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是一个比较值得尝试的应用。
冲啊!