前端Rust开发WebAssembly与Swc插件快速入门
前言
现代前端对速度的追求已经进入二进制工具时代,Rust 开发成为每个人的必修课。
一般我们将常见的前端 Rust 开发分为以下几类,难度由上至下递增:
-
开发 wasm 。
-
开发 swc 插件。
-
开发代码处理工具。
我们将默认读者具备最简单的 Rust 知识,进行快速入门介绍。
正文
开发 wasm
意义
开发 wasm 的意义在于利用浏览器运行 wasm 的优势,在 wasm 中进行大量复杂的计算、音视频、图像处理等,当你有此类需求,可以优先考虑使用 Rust 开发 wasm 分发至浏览器。
初始化
我们使用 wasm-pack 构建 wasm ,参考 wasm-pack > Quickstart 得到一个模板起始项目。
实战 case
使用 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
}
构建
# devwasm-pack build --verbose --out-dir pkg --out-name index --dev# releasewasm-pack build --verbose --out-dir pkg --out-name index --release
这将在当前目录的 pkg/*
下生成 wasm 产物与 index.js
等胶水代码,导入 index.js
便即开即用,非常方便。
运行 wasm
为了支持直接导入 .wasm
文件,我们需要 webpack 5 的 asyncWebAssembly
特性支持,此处以在 Umi 4 项目中调试为例,创建一个 Umi 4 Simple 模板项目:
pnpm create umi wasm-demo
参考 FAQ > 怎么用 WebAssembly 配置开启 wasm 支持:
// .umirc.tsexport 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 等优化策略。
开发 swc 插件
意义
现代前端高效构建往往将 babel 替代为 swc 化,为了替代 babel 插件实现代码转换,开发 swc 插件成为了一门必修课。
初始化
参考 swc > Create a project ,我们用 swc 脚手架初始化得到一个插件的模板起始项目。
实战 case
我们编写一个最简单的功能,将所有的 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 }))
}
在编写过程中,以下文档可供参考:
-
Swc :Implementing a plugin
-
Swc :Rust docs
构建
# devcargo build --target wasm32-wasi# releasecargo build --target wasm32-wasi --release
通过构建,你可以在当前目录下得到 wasm 形式的 swc 插件产物。
运行 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 得到一个模板起始项目。
实战 case
此处同样我们实现一个:将所有 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))}
}
构建
# devnapi build --platform# releasenapi 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
速度都是很快的,秒级之争没有意义。
以上。
相关文章:
前端Rust开发WebAssembly与Swc插件快速入门
前言 现代前端对速度的追求已经进入二进制工具时代,Rust 开发成为每个人的必修课。 一般我们将常见的前端 Rust 开发分为以下几类,难度由上至下递增: 开发 wasm 。 开发 swc 插件。 开发代码处理工具。 我们将默认读者具备最简单的 Rus…...

【C++ 学习 ⑧】- STL 简介
目录 一、什么是 STL? 二、STL 的版本 三、STL 的 6 大组件和 13 个头文件 四、学习 STL 的 3 个境界 五、STL 的缺陷 参考资料: STL教程:C STL快速入门(非常详细) (biancheng.net)。 C STL是什么,有…...

论文笔记--Deep contextualized word representations
论文笔记--Deep contextualized word representations 1. 文章简介2. 文章概括3 文章重点技术3.1 BiLM(Bidirectional Language Model)3.2 ELMo3.3 将ELMo用于NLP监督任务 4. 文章亮点5. 原文传送门 1. 文章简介 标题:Deep contextualized word representations作者…...

【MySQL高级篇笔记-性能分析工具的使用 (中) 】
此笔记为尚硅谷MySQL高级篇部分内容 目录 一、数据库服务器的优化步骤 二、查看系统性能参数 三、统计SQL的查询成本:last_query_cost 四、定位执行慢的 SQL:慢查询日志 1、开启慢查询日志参数 2、查看慢查询数目 3、慢查询日志分析工具…...

大学生数学建模题论文
大学生数学建模题论文篇1 浅论高中数学建模与教学设想 论文关键词:数学建模 数学 应用意识 数学建模教学 论文摘要:为增强学生应用数学的意识,切实培养学生解决实际问题的能力,分析了高中数学建模的必要性,并通过对高中…...
论文阅读 —— 滤波激光SLAM
文章目录 FAST-LIO2FAST-LIOIMUR2LIVER3LIVEEKFLINS退化摘要第一句 FAST-LIO2 摘要: 本文介绍了FAST-LIO2:一种快速、稳健、通用的激光雷达惯性里程计框架。 FAST-LIO2建立在高效紧耦合迭代卡尔曼滤波器的基础上,有两个关键的新颖之处&#…...

JavaScript键盘事件
目录 一、keydown:按下键盘上的任意键时触发。 二、keyup:释放键盘上的任意键时触发。 三、keypress:在按下并释放能够产生字符的键时触发(不包括功能键等)。 四、input:在文本输入框或可编辑元素的内容…...

opengl灯光基础:2.1 光照基础知识
光照: 光照以不同的方式影响着我们看到的世界,有时甚至是以很戏剧化的方式。当手电筒照射在物体上时,我们希望物体朝向光线的一侧看起来更亮。我们所居住的地球上的点,在中午朝向太阳时候被照得很亮,但随着地球的自转…...

大屏时代:引领信息可视化的新潮流
在信息时代的浪潮下,数据已经成为推动各行各业发展的重要动力。然而,海量的数据如何快速、直观地呈现给用户,成为了一个亟待解决的难题。在这样的背景下,可视化大屏应运而生,以其出色的表现力和交互性成为信息展示的佼…...

ChatGTP全景图 | 背景+技术篇
引言:人类以为的丰功伟绩,不过是开端的开端……我们在未来100年取得的技术进步,将远超我们从控制火种到发明车轮以来所取得的一切成就。——By Sam Altman 说明:ChatGPT发布后,我第一时间体验了它的对话、翻译、编程、…...

计算机专业学习的核心是什么?
既然是学习CS,那么在这里,我粗浅的把计算机编程领域的知识分为三个部分: 基础知识 特定领域知识 框架和开发技能 基础知识是指不管从事任何方向的软件工程师都应该掌握的,比如数据结构、算法、操作系统。 特定领域知识就是你…...

基于springboot地方旅游系统的设计与实现
摘 要 本次设计内容是基于Springboot的旅游系统的设计与实现,采用B/S三层架构分别是Web表现层、Service业务层、Dao数据访问层,并使用Springboot,MyBatis二大框架整合开发服务器端,前端使用vue,elementUI技术&…...
一些学习资料链接
组件化和CocoaPods iOS 组件化的三种方案_迷曳的博客-CSDN博客 CocoaPods 私有化 iOS组件化----Pod私有库创建及使用 - 简书 CocoaPods1.9.1和1.8 使用 出现CDN: trunk URL couldnt be downloaded: - 简书 cocoapod制作私有库repo - 简书 【ios开发】 上传更新本地项目到…...

Webpack打包图片-JS-Vue
1 Webpack打包图片 2 Webpack打包JS代码 3 Babel和babel-loader 5 resolve模块解析 4 Webpack打包Vue webpack5打包 的过程: 在webpack的配置文件里面编写rules,type类型有多种,每个都有自己的作用,想要把小内存的图片转成bas…...

进程控制(Linux)
进程控制 fork 在Linux中,fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。 返回值: 在子进程中返回0,父进程中返回子进程的PID,子进程创建失败返回-1。 …...

C Primer Plus第十四章编程练习答案
学完C语言之后,我就去阅读《C Primer Plus》这本经典的C语言书籍,对每一章的编程练习题都做了相关的解答,仅仅代表着我个人的解答思路,如有错误,请各位大佬帮忙点出! 由于使用的是命令行参数常用于linux系…...

又名管道和无名管道
一、进程间通信(IPC,InterProcess Communication) 概念:就是进程和进程之间交换信息。 常用通信方式 无名管道(pipe) 有名管道 (fifo) 信号(signal) 共…...

操作系统复习4.1.0-文件管理结构
定义 一组有意义的信息的集合 属性 文件名、标识符、类型、位置、大小、创建时间、上次修改时间、文件所有者信息、保护信息 操作系统向上提供的功能 创建文件、删除文件、读文件、写文件、打开文件、关闭文件 这6个都是系统调用 创建文件 创建文件时调用Create系统调用…...

【嵌入式烧录/刷写文件】-2.6-剪切/保留Intel Hex文件中指定地址范围内的数据
案例背景: 有如下一段HEX文件,保留地址范围0x9140-0x91BF内的数据,删除地址范围0x9140-0x91BF外的数据。 :2091000058595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F70717273747576775F :2091200078797A7B7C7D7E7F808182838485868788898A…...
JavaScript表单事件(下篇)
目录 八、keydown: 当用户按下键盘上的任意键时触发。 九、keyup: 当用户释放键盘上的键时触发。 十、keypress: 当用户按下键盘上的字符键时触发。 十一、focusin: 当表单元素或其子元素获得焦点时触发。 十二、focusout: 当表单元素或其子元素失去焦点时触发。 十三、c…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...

python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...

python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...

CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...

学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...

el-switch文字内置
el-switch文字内置 效果 vue <div style"color:#ffffff;font-size:14px;float:left;margin-bottom:5px;margin-right:5px;">自动加载</div> <el-switch v-model"value" active-color"#3E99FB" inactive-color"#DCDFE6"…...

Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...

WPF八大法则:告别模态窗口卡顿
⚙️ 核心问题:阻塞式模态窗口的缺陷 原始代码中ShowDialog()会阻塞UI线程,导致后续逻辑无法执行: var result modalWindow.ShowDialog(); // 线程阻塞 ProcessResult(result); // 必须等待窗口关闭根本问题:…...
comfyui 工作流中 图生视频 如何增加视频的长度到5秒
comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗? 在ComfyUI中实现图生视频并延长到5秒,需要结合多个扩展和技巧。以下是完整解决方案: 核心工作流配置(24fps下5秒120帧) #mermaid-svg-yP…...

实战设计模式之模板方法模式
概述 模板方法模式定义了一个操作中的算法骨架,并将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法中的某些步骤。简单来说,就是在一个方法中定义了要执行的步骤顺序或算法框架,但允许子类…...