背景
在项目开始构建前,需要根据流水线中的构建参数对代码中的变量值进行统一替换
- 替换多路复用环境的值
- 替换 API_HOST,H5_HOST 等域名信息
- ......
现有方案:
- ts文件:使用正则表达式对 key - value 进行匹配,替换原本 value 值
正则表达式如下:regexr-cn.com/2rrn9
const regex = new RegExp(`(${variableName}\\s*=\\s*)([^;]+)(;)`);
- json文件:json -> obj,通过 key 获取 value 进行替换
不足
- 当面对复杂的变量结构时,难以直接通过正则表达式匹配到变量名和变量值进行替换。正则表达式如下:
const regex = new RegExp(`(${variableName}\\s*=\\s*)([^;]+)(;)`);
- 当变量连续声明时,中间使用
,
分隔而不是;
无法识别 - 当value中存在
;
正则分组匹配错误,导致替换失败
- 当面对复杂的代码结构时,难以直接精确控制:
// 三元表达式
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来处理,就需要用到serde
和serde_json
两个包
Serde
Serde 是一个用于高效、通用地对 Rust 数据结构进行序列化反序列化的框架。
该包提供了Serialize
和Deserialize
两个trait
。它本身不做转换的实际工作,实际工作是其它包实现的。我们可以结合serde,通过serde_json
和rmp-serde
将一种数据格式转换成 JSON
和MessagePack
格式。
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)