Rust 优化 js 工具函数实践

189 阅读6分钟

背景

在项目开始构建前,需要根据流水线中的构建参数对代码中的变量值进行统一替换

  1. 替换多路复用环境的值
  2. 替换 API_HOST,H5_HOST 等域名信息
  3. ......

现有方案:

  • ts文件:使用正则表达式对 key - value 进行匹配,替换原本 value 值

正则表达式如下:regexr-cn.com/2rrn9

const regex = new RegExp(`(${variableName}\\s*=\\s*)([^;]+)(;)`);
  • json文件:json -> obj,通过 key 获取 value 进行替换

不足

  1. 当面对复杂的变量结构时,难以直接通过正则表达式匹配到变量名和变量值进行替换。正则表达式如下:
const regex = new RegExp(`(${variableName}\\s*=\\s*)([^;]+)(;)`);
  • 当变量连续声明时,中间使用,分隔而不是; 无法识别
  • 当value中存在;正则分组匹配错误,导致替换失败
  1. 当面对复杂的代码结构时,难以直接精确控制:
// 三元表达式
export const API_HOST = inH5 ? getPageBaseUrl() : 'https://jiankang.baidu.com';
export const MALL_API_HOST = inH5 ? getPageBaseUrl() : 'https://jiankang.baidu.com';

// 函数表达式
export const API_HOST = getPageBaseUrl();
  • API_HOST会根据不同的环境,选择不同的url,但是正则匹配 会将三元表达式直接替换成字符串,是不正确的
  • 不能精确控制 value 类型进行替换

总结: 由于正则表达式不能正确理解 JavaScript 代码的复杂语法,可能会导致错误的匹配和替换。

目标: 在不改变原工作流的方式下,通过 Rust 调用 swc/core 进行 AST语法树分析替换变量,并编译成 wasm 供 js 调用

Js 如何使用 Rust 工具函数

使用 WASM 的方式,基于wasm-pack,将 Rust 代码编译成 WASM,以供 Node 调用

WASM 定义

WebAssembly(缩写为 wasm)是一种使用非 JavaScript
代码,并使其在浏览器中运行的方法。这些代码可以是 C、C++ 或 Rust
等。它们会被编译进你的浏览器,在你的 CPU
上以接近原生的速度运行。这些代码的形式是二进制文件,你可以直接在 JavaScript
中将它们当作模块来用。

上图的左侧是用 C++实现的求递归的函数。中间是十六进制 的 Binary
Code。右侧是指令文本。其实,中间的十六进制的 Binary Code 就是 WebAssembly。

总结:

1. WASM 是一种**编译目标**,不是用来给各位用手一行一行撸的代码,C、C++ 或 Rust 等都可以编译 成 WASM
2. WASM 模块可以被导入的到一个 Web / Node 中,并且暴露出供 JavaScript 使用的 WebAssembly 函数。

Rust 如何编译成 WASM

wasm-pack 是一个构建、测试和发布 WASM 的 Rust CLI 工具,通过 wasm-pack 相关的命令可以管理构建过程,包括自动调用 wasm-bindgen 来生成胶水代码。

wasm-pack build --target nodejs

wasm-pack 会在项目中创建一个 pkg 目录,并包含以下内容

- js 文件
- wasm 文件
- ts 声明文件
- package.json 文件,所以打包出来的包是可以直接发 布 n pm 的
- README 文件,是从你 的 Ru st 项 目中的 RE ADME 文件复制来的

JS 如何调用 WASM

通过wasm-pack 将 Rust 打包成可发布的 npm 包,js 通过调用 npm package 来调用编译好的 wasm 模块

数据交互

主要从 JS 如何调用 Rust 函数、数据交互两方面介绍

Rust如何暴露函数

wasm-bindgen

wasm-bindgen可以让WASM模块和Javascript模块进行更好的交互。通过使用宏(如 #[wasm_bindgen])来注解Rust代码,这些宏会指导 wasm-bindgen 工具生成必要的JS胶水代码(glue code),以便Rust函数可以作为JavaScript函数来调用,或允许Rust代码调用JavaScript函数。

use wasm_bindgen::prelude::*;

// 从Web导入 `window.alert` 函数
#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
}

// 从Rust导出一个`greet`函数到Javascript,该函数会`alert`一条欢迎信息
#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

数据交互

js调用Rust函数进行函数传参时,我们需要通过json来处理,就需要用到serdeserde_json两个包

Serde

Serde 是一个用于高效、通用地对 Rust 数据结构进行序列化反序列化的框架。 该包提供了SerializeDeserialize 两个trait。它本身不做转换的实际工作,实际工作是其它包实现的。我们可以结合serde,通过serde_jsonrmp-serde将一种数据格式转换成 JSONMessagePack格式。

Serde_json
serde_json 是 serde 的一个插件,它提供了对 JSON 格式的支持。你可以使用
serde_json 将 JSON 数据反序列化为 Rust 数据结构,或者将 Rust 数据结构序列化为
JSON 格式。
serde_json = {version = "1", features = ["preserve_order"]}
serde = "1"

在 serde_json 中开启【preserve_order】这个 features,是因为 serde_json 在处理 json 数据时,Map 默认使用的是 BtreeMap,它对插入顺序不保证

// source是一个读取后json文件的信息

#[wasm_bindgen]
pub fn replace_json(source: &str, config: &str) -> String {
    let mut json_value: Map<String, Value> = serde_json::from_str(source).unwrap();
    let replacer = ConstReplacer::new(config);
    // 更新Json
    json_value[replacer.replaced_name] = replacer.replaced_value;

      // 将更新后的 json 结构体重新转换回 json 字符串
    let updated_json = serde_json::to_string_pretty(&json_value).unwrap();
    updated_json
}
如果想要让修改后JSON中的key顺序保持不变,那可以开启{' '}

preserve_order

特性

#[cfg(not(feature = "preserve_order"))]
type MapImpl<K, V> = BTreeMap<K, V>;
#[cfg(feature = "preserve_order")]
type MapImpl<K, V> = IndexMap<K, V>;

具体实现:

尝试搭建一个 Rust 项目,通过 serde_json 处理数据交互,swc/core 进行 AST 语法分析,wasm-pack 进行 Rust 项目的打包,具体步骤如下:

a. 创建项目

cargo new --lib const_replace

b. 因为我们要编译成wasm供js调用,需要将crate-type改成cdylib

[lib]
crate-type = ["cdylib"]

c. 引入swc_core,通过遍历ast,进行变量的修改

cargo add swc_core --features common,ecma_ast,ecma_parser,ecma_visit,ecma_codegen

swc 中核心库及作用如下:swc_core - Rustswc 中核心库及作用如下:swc_core - Rust

d. 因为 js 需要通过 json 传参给 wasm,在 Rust 中需要引入 serde,serde_json 来解析 json

serde_json = {version = "1", features = ["preserve_order"]}
serde = "1"

e. 我们需要让 wasm 和 js 进行交互,需要引入wasm-bindgen

wasm-bindgen = "0.2.83"

f. 将需要导出的函数添加#[wasm_bindgen]过程宏,wasm-bindgen在编译时会生成对应的 glue code,从而在JS和webassembly之间调用函数

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn const_replace(source: &str, config: &str) -> Result<String, JsError> {
}

至此,我们项目所有的依赖如下:

[package]
name = "const-replace"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
swc_core = { version = "0.97.5", features = ["common", "ecma_ast", "ecma_parser", "ecma_visit", "ecma_codegen"] }
serde_json = {version = "1", features = ["preserve_order"]}
serde = "1"
wasm-bindgen = "0.2.83"

g. 通过wasm-pack 将 Rust 打包成可发布的 npm 包,js 通过调用 npm package 来调用编译好的 wasm 模块

yarn global add wasm-pack

// 因为我们是通过node执行js文件,所以将target设置成nodejs,也可设置成为web
wasm-pack build --target nodejs

// js调用
const pkg = require('const-replace');
const content = pkg.const_replace(source, config)