前端Rust二进制/wasm全平台构建流程简述

前言

开门见山,现代前端 Rust 构建基本分三大类,即 构建 .wasm 、构建 .node 二进制 、构建 swc 插件。

入门详见 《 前端Rust开发WebAssembly与Swc插件快速入门 》 。

对于单独开发某一类的流程,在上述参考文章中已有介绍,但对于一次开发后全平台构建发布,上述文章并未涉猎,基于此,本文将快速介绍一个最简的 全平台构建 (包括二进制 .node.wasm ) 前端 Rust 包的开发流程是怎样的。

注:我们默认读者已掌握构建三大类前端 Rust 包的知识。

正文

Rust workspace

以 workspace 组织代码仓库,核心逻辑全部独立为一个子包,参考如下:

bash 复制代码
 - crates
   - binding_node  # 基于 napi 分发 `.node` 二进制
   - binding_wasm  # 基于 wasm-pack 分发 `.wasm`
   - core          # 核心逻辑
   - ...           # 其他解耦
binding_node

其中 binding_node 为 node 构建出口,引用核心逻辑后暴露 API ,简式参考如下:

rust 复制代码
// binding_node/src/lib.rs

#[macro_use]
extern crate napi_derive;

use napi::{bindgen_prelude::AsyncTask, Env, Task};
use core::{core_process, IInput, IResult};

// ⬇️ 同步部分
#[napi]
pub fn method_sync(input: IInput) -> Result<IResult, anyhow::Error> {
    core_process(input)
}

// ⬇️ 异步部分
pub struct TaskExecutor {
    input: IInput,
}

pub struct ProcessTask {
    task: TaskExecutor,
}

impl Task for ProcessTask {
    type Output = IResult;
    type JsValue = IResult;

    fn compute(&mut self) -> napi::Result<Self::Output> {
        self.task.process().map_err(|err| napi::Error::from_reason(&err.to_string()))
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> napi::Result<Self::JsValue> {
        Ok(output)
    }
}

impl TaskExecutor {
    pub fn process(&self) -> Result<IResult, anyhow::Error> {
        core_process(self.input.clone())
    }
}

#[napi(ts_return_type="Promise<IResult>")]
pub fn method(input: IInput) -> AsyncTask<ProcessTask> {
    AsyncTask::new(ProcessTask {
        task: TaskExecutor { input },
    })
}

注:此处为最简函数式示例,涉及 异步信号、错误处理、错误打印、导出多方法的复杂类 等情况时请自行处理。

通常情况提供最普通的 同步函数 方法已足够。

类型处理

为了尽可能减少工作量,让 napi 自动生成 .d.ts 类型,需给结构对象加上 #[napi] 宏才能生成类型,但所有结构声明在 crates/core 核心逻辑包中,而我们的 napi 出口在 crates/binding_node

一种解法是条件编译,提供 feature = "node" 的特定模式,参考如下:

bash 复制代码
# core/Cargo.toml

[features]
default = []
node = ["napi", "napi-derive"]

[dependencies]
napi = { ..., optional = true }
napi-derive = { ..., optional = true }
rust 复制代码
// 条件编译宏

#[macro_export]
#[cfg(feature = "node")]
macro_rules! multi_env {
    ($(
        $items:item
    )*) => {
        use napi_derive::napi;
        $(
            #[napi(object)]
            $items
        )*
    };
}

使用参考:

rust 复制代码
multi_env! {
	pub struct IInput {
	    pub input: ...,
	}
	pub struct IResult {
	    pub output: ...,
	}
	// ...
}

综上,通过特定 node feature 方式,在引用时条件添加 #[napi] 来做到自动生成类型。

人工应对复杂类型

如 想自行管理导出方法类型的暴露情况、涉及 class 等复杂的类型 、无法自动识别生成 等情况,可以人工编写整份 .d.ts 文件,但十分耗费精力。

对于 异步情况等 引发的动态值转换,而无法自动识别类型的,可尝试 napi 默认自带的类型选项,如 #[napi(ts_return_type="...")] 等选项来辅助修改生成的类型,省时省力。

构建

对于多平台,我们通常需要依赖 GitHub Actions 来进行多平台构建,在 CI 中构建、测试后发布到 npm 。

现代常用构建对象 napi 列表如下:

ts 复制代码
    "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"
      ]
    },

最常用的即如上 8 个平台,酌情构建 armv7-unknown-linux-gnueabihf ,必要时采用 wasm 兜底即可。

此部分过于冗长且模板化,可参考 swc 等项目的构建 CI 来取用。

binding_wasm

其中 binding_wasm 为 wasm 构建出口,引用核心逻辑后暴露 API ,简式参考如下:

rust 复制代码
use wasm_bindgen::prelude::*;

use core::{core_process, IInput, IResult};

#[wasm_bindgen(js_name = "methodSync")]
pub fn method_sync(input: IInput) -> Result<IResult, JsError> {
    core_process(input).map_err(|err| JsError::new(&err.to_string()))
}

#[wasm_bindgen(typescript_custom_section)]
const INTERFACE_DEFINITIONS: &'static str = r#"
export function method(config: IInput): Promise<IResult>;
"#;

#[wasm_bindgen(skip_typescript)]
pub fn method(input: IInput) -> js_sys::Promise {
    wasm_bindgen_futures::future_to_promise(async {
        core_process(input)
            .map(|r| serde_wasm_bindgen::to_value(&r).unwrap())
            .map_err(|err| JsValue::from_str(&err.to_string()))
    })
}

注:此处为最简函数式示例,涉及 异步、错误处理、错误打印 等情况时请自行修订处理。

通过 serde-wasm-bindgen 来动态转换 JS 值与 Rust 中的结构。

通常情况提供最普通的 同步函数 方法已足够。

类型处理

为了尽可能减少工作量,我们使用 tsify 做自动类型生成,同上文中相同,采用条件编译,提供 feature = "wasm" 模式:

bash 复制代码
# core/Cargo.toml

[features]
default = []
wasm = ["tsify", "wasm-bindgen"]

[dependencies]
tsify = { ..., optional = true }
wasm-bindgen = { ..., optional = true}
rust 复制代码
// 条件编译宏

#[macro_export]
#[cfg(feature = "wasm")]
macro_rules! multi_env {
    ($(
        $items:item
    )*) => {
        use tsify::Tsify;
        use serde::{Deserialize, Serialize};
        use wasm_bindgen::prelude::*;
        $(
            #[derive(Tsify, Serialize, Deserialize)]
            #[tsify(into_wasm_abi, from_wasm_abi)]
            $items
        )*
    };
}

// 兜底用
#[macro_export]
#[cfg(all(not(feature = "wasm"), not(feature = "node"),))]
macro_rules! multi_env {
    ($($tokens:tt)*) => {
        $($tokens)*
    };
}

人工应对复杂类型

如 想自行管理导出方法类型的暴露情况、无法自动识别生成 等情况,可以人工编写整份 .d.ts 文件。

对于 异步情况等 引发的动态值转换,而无法自动识别类型的,尝试 #[wasm_bindgen(skip_typescript)] 跳过自动生成类型后,使用 #[wasm_bindgen(typescript_custom_section)] 人工插入少量类型声明来解决,节省编写时间。

构建

由于 wasm 无需依赖本机环境,根据情况可选在云构建或本地构建均可,主要包含 web 用途与 nodejs 用途的 wasm 产物构建。

web 用途

web 用途的 wasm 产物主要用于网页应用,playground 等,构建命令参考:

bash 复制代码
  # web 用途
  cd crates/binding_wasm && wasm-pack build --verbose --out-dir ./output/wasm_web --out-name index --release

web 用途的构建产物包含 ESM 格式胶水代码 ,可直接将产物整体用在 webpack 项目导入,对于 webpack 5 可直接开启 async webassembly 特性直接适配项目:

ts 复制代码
// webpack.config.js

  experiments: {
    asyncWebAssembly: true,
  },
ts 复制代码
import * as wasm from '/path/to/wasm-output'
// 或使用异步 await import() 延时、按需加载

nodejs 用途

nodejs 用途主要用于非主流平台兜底,如 边缘函数、serverless 等环境,构建命令参考:

bash 复制代码
  # nodejs 用途
  cd crates/binding_wasm && wasm-pack build --target nodejs --verbose --out-dir ./output/wasm --out-name index --release

和 web 用途构建命令区别在于特定了 --target nodejs ,这会得到 CJS 格式产物代码,可直接用于 nodejs 。

使用与安装时机

对于两种 wasm 包,通常命名为 @scope/wasm ( nodejs 用途) 、@scope/wasm-web ( web 用途),在对应平台下,可直接安装该包来使用。

同时,对非主流架构环境,我们一般在主包的 postinstall 时进行脚本检测,并在需要时自动安装 @scope/wasm 包来兜底,具体逻辑较冗长且模板化,可参考 swc 等项目取用即可。

如何安装指定平台包

最新版本的 pnpm v8 支持配置 pnpm.supportedArchitectures 来安装想要的平台包,即使你不在某个平台上,这通常用于 wasm 安装校验:

bash 复制代码
# .npmrc
# 关闭 postinstall 缓存
side-effects-cache=false
# 打印 postinstall 日志
reporter=append-only
ts 复制代码
// package.json

  // ↓ 该配置将匹配不到任何 `.node` 的平台包,于是自动 fallback 到 wasm 兜底包
  "pnpm": {
    "supportedArchitectures": {
      "os": ["unknown"],
      "cpu": ["x64"]
    }
  }
wasm 产物优化

通常 .node 二进制产物由 Rust 生产构建后自动优化,加上 strip 优化体积已足够。

对于 wasm 一般采用 wasm-opt 优化体积,默认 wasm-pack 生产构建最终阶段会自动下载相关工具并执行优化,如遇网络问题,可关闭自动优化,转为手动下载后执行优化:

bash 复制代码
  # 提前下载好执行工具,防止网络问题
  cargo install wasm-bindgen-cli
bash 复制代码
# binding_wasm/Cargo.toml

# 关闭 wasm-opt 自动优化,之后手动优化
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
bash 复制代码
  # 下载 wasm-opt 工具并解压
  # 最新版本见:https://github.com/WebAssembly/binaryen/releases
  curl -L https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-macos.tar.gz -o ./binaryen.tar.gz
  mkdir ./.cache
  tar -xvf ./binaryen.tar.gz -C ./.cache
  # 优化 wasm 产物
  ./.cache/binaryen-version_116/bin/wasm-opt -Oz -o ./output/wasm/index_bg.wasm ./output/wasm/index_bg.wasm

总结

在全平台构建时,编写 十分大量 的人力脚本与文件操作在所难免,如有可能,可将其统一抽象化,方便下次取用。

由于流程更偏向于固定模板化,在实践时,请自行参考相关项目自取所需即可。

相关推荐
xiao-xiang7 分钟前
jenkins-通过api获取所有job及最新build信息
前端·servlet·jenkins
C语言魔术师24 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳2 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?2 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二8 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
九酒10 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔10 小时前
HTML5 新表单属性详解
前端·html·html5