赞
踩
现代前端对速度的追求已经进入二进制工具时代,Rust 开发成为每个人的必修课。
一般我们将常见的前端 Rust 开发分为以下几类,难度由上至下递增:
开发 wasm 。
开发 swc 插件。
开发代码处理工具。
我们将默认读者具备最简单的 Rust 知识,进行快速入门介绍。
开发 wasm 的意义在于利用浏览器运行 wasm 的优势,在 wasm 中进行大量复杂的计算、音视频、图像处理等,当你有此类需求,可以优先考虑使用 Rust 开发 wasm 分发至浏览器。
我们使用 wasm-pack 构建 wasm ,参考 wasm-pack > Quickstart 得到一个模板起始项目。
使用 tsify 支持输出结构体的 TypeScript 类型,实现一个简单的加法运算:
# Cargo.toml 确保你含有这些依赖
[dependencies]
serde = { version = "1.0.163", features = ["derive"] }
tsify = "0.4.5"
use wasm_bindgen::prelude::*; use serde::{Serialize, Deserialize}; use tsify::Tsify; #[derive(Tsify, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Rect { pub width: u32, pub height: u32 } #[wasm_bindgen] pub fn plus(mut rect: Rect) -> Rect { rect.width += rect.height; rect }
# dev
wasm-pack build --verbose --out-dir pkg --out-name index --dev
# release
wasm-pack build --verbose --out-dir pkg --out-name index --release
这将在当前目录的 pkg/*
下生成 wasm 产物与 index.js
等胶水代码,导入 index.js
便即开即用,非常方便。
为了支持直接导入 .wasm
文件,我们需要 webpack 5 的 asyncWebAssembly
特性支持,此处以在 Umi 4 项目中调试为例,创建一个 Umi 4 Simple 模板项目:
pnpm create umi wasm-demo
参考 FAQ > 怎么用 WebAssembly 配置开启 wasm 支持:
// .umirc.ts export default { chainWebpack(config) { config.set('experiments', { ...config.get('experiments'), asyncWebAssembly: true }) const REG = /\.wasm$/ config.module.rule('asset').exclude.add(REG).end(); config.module .rule('wasm') .test(REG) .exclude.add(/node_modules/) .end() .type('webassembly/async') .end() }, }
之后便可在项目中直接导入刚刚打包好,在 pkg/*
的 wasm 即开即用:
import * as wasm from './path/to/pkg'
const ret = wasm.plus({
width: 1,
height: 2,
})
// { width: 3, height: 2 }
console.log('ret: ', ret);
注:
由于 wasm 文件可能较大,当你需要优化时,可将使用 .wasm
的组件手动 React.lazy(() => import('./Component'))
拆包,之后在 useEffect
中懒加载 await import('./path/to/pkg')
。
对于非 Umi 4 的 webpack 5 项目,请自行开启 experiments.asyncWebAssembly
即可一键支持 wasm 导入。
由于当下浏览器和 PC 设备性能已足够强大,更多场合下,运行 wasm 进行数据计算,传递数据花费的时间将 远远超出使用 JavaScript 进行同逻辑计算的时间 。
所以除音视频场景外,你很可能不需要 wasm ,而是优先考虑使用 Worker 等优化策略。
现代前端高效构建往往将 babel 替代为 swc 化,为了替代 babel 插件实现代码转换,开发 swc 插件成为了一门必修课。
参考 swc > Create a project ,我们用 swc 脚手架初始化得到一个插件的模板起始项目。
我们编写一个最简单的功能,将所有的 react
导入转换为 preact
:
# Cargo.toml 确保你含有这些依赖
[dependencies]
serde = "1.0.163"
serde_json = "1.0.96"
swc_core = { version = "0.76.39", features = ["ecma_plugin_transform"] }
use swc_core::ecma::{ ast::{Program, ImportDecl, ImportSpecifier}, visit::{as_folder, FoldWith, VisitMut, VisitMutWith}, }; use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] pub struct TransformPluginConfig { from: String, to: String, } pub struct TransformVisitor { config: TransformPluginConfig, } impl VisitMut for TransformVisitor { fn visit_mut_import_decl(&mut self, n: &mut ImportDecl) { n.visit_mut_children_with(self); if n.specifiers.len() == 1 { if let ImportSpecifier::Default(_) = n.specifiers[0] { if n.src.value == self.config.from { n.src = Box::new(self.config.to.clone().into()); } } } } } #[plugin_transform] pub fn process_transform(program: Program, metadata: TransformPluginProgramMetadata) -> Program { let config = serde_json::from_str( &metadata .get_transform_plugin_config() .unwrap() ) .expect("invalid config"); program.fold_with(&mut as_folder(TransformVisitor { config })) }
在编写过程中,以下文档可供参考:
# dev
cargo build --target wasm32-wasi
# release
cargo build --target wasm32-wasi --release
通过构建,你可以在当前目录下得到 wasm 形式的 swc 插件产物。
import { transformSync } from '@swc/core' const transform = async () => { const { code } = transformSync( ` import React from 'react' `, { jsc: { experimental: { plugins: [ [ require.resolve( './target/wasm32-wasi/debug/my_first_plugin.wasm' ), { from: 'react', to: 'preact', }, ], ], }, parser: { syntax: 'typescript', dynamicImport: true, tsx: true, }, target: 'es2015', minify: { compress: false, mangle: false, }, transform: { react: { runtime: 'automatic', throwIfNamespace: true, development: true, useBuiltins: true, }, }, }, module: { type: 'es6', ignoreDynamic: true, }, minify: false, isModule: true, sourceMaps: false, filename: 'index.tsx', } ) // import React from 'preact' console.log('code: ', code) } transform()
为了避免多平台差异,我们分发了 wasm32-wasi
目标的 wasm 包,好处是只需构建一次即可全平台通用,缺点是产物较大,同时 wasm 运行速度不如 .node
;但现代前端已无需担心只在本地编译阶段使用的包大小,如 Nextjs 单包依赖已达 40 M
以上,TypeScript 20 M
,你可以无需关心产物体积问题。
至此,我们介绍了如何借助 swc 插件实现 babel 插件的替代,在下文中,我们将继续深入,真正构建多平台分发的二进制包,同时不会做过多细节介绍,推荐只学习到此处为止。
目前最主流的前端 Rust 开发即是借助 Swc 来解析 JavaScript 、TypeScript 代码,从而实现代码信息提取、转换、编译等,我们会将 Rust 编译为 Node addon .node
文件,以获得远比 wasm 更快的运行速度。
使用 napi-rs 构建 ,参考 napi > Create project 得到一个模板起始项目。
此处同样我们实现一个:将所有 react
导入转换为 preact
的需求,所需要的依赖与模板代码如下:
# Cargo.toml 确保你含有这些依赖
[dependencies]
napi = { version = "2.12.2", default-features = false, features = ["napi4", "error_anyhow"] }
napi-derive = "2.12.2"
swc_common = { version = "0.31.12", features = ["sourcemap"] }
swc_ecmascript = { version = "0.228.27", features = ["parser", "visit", "codegen"] }
#[macro_use] extern crate napi_derive; use std::path::{Path, PathBuf}; use std::str; use swc_common::comments::SingleThreadedComments; use swc_common::{sync::Lrc, FileName, Globals, SourceMap}; use swc_ecmascript::ast; use swc_ecmascript::codegen::text_writer::JsWriter; use swc_ecmascript::parser::lexer::Lexer; use swc_ecmascript::parser::{EsConfig, Parser, StringInput, Syntax, TsConfig}; use swc_ecmascript::visit::{VisitMut, VisitMutWith}; #[napi] pub fn transform(code: String, options: ImportChange) -> String { let is_jsx = true; let is_typescript = true; let filename_path_buf = PathBuf::from("filename.tsx"); let syntax = if is_typescript { Syntax::Typescript(TsConfig { tsx: is_jsx, decorators: true, ..Default::default() }) } else { Syntax::Es(EsConfig { jsx: is_jsx, export_default_from: true, ..Default::default() }) }; let source_map = Lrc::new(SourceMap::default()); let source_file = source_map.new_source_file( FileName::Real(filename_path_buf.clone()), code.clone().into(), ); let comments = SingleThreadedComments::default(); let lexer = Lexer::new( syntax, Default::default(), StringInput::from(&*source_file), Some(&comments), ); let mut parser = Parser::new_from(lexer); let mut module = parser.parse_module().expect("failed to parse module"); swc_common::GLOBALS.set(&Globals::new(), || { let mut visitor = options; module.visit_mut_with(&mut visitor); }); let (code, _map) = emit_source_code( Lrc::clone(&source_map), Some(comments), &module, None, false, ) .unwrap(); code } #[napi(object)] pub struct ImportChange { pub from: String, pub to: String, } impl ImportChange { pub fn new(from: String, to: String) -> Self { Self { from, to } } } impl VisitMut for ImportChange { fn visit_mut_module_decl(&mut self, decl: &mut ast::ModuleDecl) { if let ast::ModuleDecl::Import(import_decl) = decl { if import_decl.src.value == self.from { import_decl.src = Box::new(self.to.clone().into()); } } } } pub fn emit_source_code( source_map: Lrc<SourceMap>, comments: Option<SingleThreadedComments>, program: &ast::Module, root_dir: Option<&Path>, source_maps: bool, ) -> Result<(String, Option<String>), napi::Error> { let mut src_map_buf = Vec::new(); let mut buf = Vec::new(); { let writer = Box::new(JsWriter::new( Lrc::clone(&source_map), "\n", &mut buf, if source_maps { Some(&mut src_map_buf) } else { None }, )); let config = swc_ecmascript::codegen::Config { minify: false, target: ast::EsVersion::latest(), ascii_only: false, omit_last_semi: false, }; let mut emitter = swc_ecmascript::codegen::Emitter { cfg: config, comments: Some(&comments), cm: Lrc::clone(&source_map), wr: writer, }; emitter.emit_module(program)?; } let mut map_buf = vec![]; let emit_source_maps = if source_maps { let mut s = source_map.build_source_map(&src_map_buf); if let Some(root_dir) = root_dir { s.set_source_root(Some(root_dir.to_str().unwrap())); } s.to_writer(&mut map_buf).is_ok() } else { false }; if emit_source_maps { Ok(( unsafe { str::from_utf8_unchecked(&buf).to_string() }, unsafe { Some(str::from_utf8_unchecked(&map_buf).to_string()) }, )) } else { Ok((unsafe { str::from_utf8_unchecked(&buf).to_string() }, None)) } }
# dev
napi build --platform
# release
napi build --release
一般情况下,我们通常会分发至以下 9
个平台:
"napi": { "triples": { "defaults": false, "additional": [ "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc", "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", "armv7-unknown-linux-gnueabihf" ] } }
通常本地只能编译自己平台的 .node
二进制文件,所以需要依赖 Github Actions CI 等云环境进行多平台的构建,并且在 CICD 中构造好 npm 包使用 Npm Token 发布,此部分内容往往是大量的模板代码与调试过程,请自行研究学习。
import { transform } from './index'
console.log(
// import React from 'preact'
transform(
`import React from 'react'`,
{ from: 'react', to: 'preact' }
)
)
直接导入 napi
生成的 index.js
胶水代码即可使用 .node
二进制包。
通过构建 .node
分发至不同平台是目前最高运行效率、最小下载体积的方法,但相对应需要手动管理所有 Rust 代码,且多平台构建也强依赖云环境,这提出了一些较高的要求。
随着对 napi
/ Rust 异步、并发编程 / Swc 的理解精进,你可以写出更高运行效率的代码,得到更快的执行速度。但最简单的代码依然够用,因为现代计算机性能已经足够快,1s 还是 10s 的争论没有意义。
在开发过程中,你可能会遇到各种 Rust 构建相关的问题,请自行研究并解决。
本文对 Rust 浅尝辄止,如希望更有所作为,你可以通过不断精进 Rust ,组织出更优雅的代码结构,实现更高的执行效率。
前端 AST 人尽皆知,如同开发 Babel 插件一样,开发 Rust Swc 插件已然成为现代前端的必修课,文本推荐只入门至 Swc 插件为止,已经能应对绝大多数场景。
另外,对于性能上无需过多追求,由于计算机的性能已经过剩,不管是 wasm 还是 .node
速度都是很快的,秒级之争没有意义。
以上。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。