前言#
前面我们也简单的接触过WebAssembly:rust基础学习--搭配webAssembly - 知乎 (zhihu.com),也曾将我们之前写的扫雷小游戏编译成wasm部署到web上:rust基础学习--基于Bevy实现扫雷小游戏day8 - 知乎 (zhihu.com)。
但是我们实际上都只是在学如何套壳子,没有任何涉及逻辑的,所以想了下准备来写一个babel插件。
虽然有些本末倒置,毕竟要写感觉可以直接基于swc去写。
但是别忘了,旧项目是属于能不动基本就不动的,所以要把旧项目的babel替换成swc,需要技术审核比如插件支持度什么的,所以对于旧项目来说,成本最低最稳定的还是使用babel。
另外说下这里不分析性能、不追求代码优雅,只是一个方案,github链接:
https://github.com/1714080902120/wasm_babbel_plugin_checking_exported_function环境#
因为这个插件的目的是在项目编译阶段,所以自然是node环境,到时打包wasm的target也得是nodejs。
需求#
其实一两年前公司的一个项目重构,在重构过程中因为大量的无注释代码搞得头疼,就有了强制开发写函数注释的想法。
那么我们需要整理下:
- 要知道这人写在哪个文件以及是什么文件,目标仅限于
.js文件 - 只针对于
export出去的function(包括箭头函数) - 找到没有注释的导出函数
实现#
第一点知道是什么文件这点简单,有的是办法,但是第二点和第三点就比较麻烦了,我们需要知道代码长什么样。
这种时候自然是我们的babel上场,有了它,我们可以轻松知道哪些是export的:exportdeclaration
然后我们在这个基础上去分析第三点即可。
js版本#
于是我基于这个想法实现了一个babel插件,代码比较简单,这里直接放出:
module.exports = function () {
return {
name: 'test',
visitor: {
ExportDeclaration(path) {
const fileName = path?.hub?.file?.opts?.filename;
if (fileName.includes('\\utils\\' || '\\api\\' || '\\wxs\\') && !fileName.includes('node_modules') && fileName.includes('.js')) {
const node = path?.node || {};
const nodeDeclaration = node?.declaration || {};
if (node.leadingComments == undefined) {
if (nodeDeclaration.type === 'FunctionDeclaration') {
const item = nodeDeclaration.id;
const { line, column } = item?.loc?.start;
console.log(`公共文件${"\033"}[40;33m${fileName}${"\033"}[0m里的方法${"\033"}[40;32m${item.name}${"\033"}[0m没有写注释,麻烦遵循规范,谢谢!位置:第${line}行${column}列`)
} else if (nodeDeclaration.type === 'VariableDeclaration') {
node?.declaration?.declarations?.map(e => {
const { line, column } = node?.loc?.start || {};
if (['ArrowFunctionExpression', 'FunctionExpression'].includes(e?.init?.type)) {
console.log(`公共文件${"\033"}[40;33m${fileName}${"\033"}[0m里的方法${"\033"}[40;32m${e.id.name}${"\033"}[0m没有写注释,麻烦遵循规范,谢谢!位置:第${line}行${column}列`)
}
return e
})
}
}
}
}
}
}
}
ExportDeclaration:使用了export关键字的leadingComments:放在函数头部的就是leadingCommentsFunctionDeclaration:函数声明,比如export function xx () {}VariableDeclaration:变量声明,比如export const xx = xxx,因为箭头函数和函数也可以这么声明,所以我们还需要在里面判断,ArrowFunctionExpression和FunctionExpression这两种。
其它不多说了,相信大家对babel或多或少有所了解,如果有哪里不懂可以评论区里说下。
后面我们rust就不解释了~
项目初始化#
我们这里直接创建lib的:
cargo new project_name --lib然后配置下Cargo.toml:
[package]
name = "project_name"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"] # 表示这是一个动态链接库
[dependencies]
js-sys = "0.3.69" # 它提供了js类型和方法
regex = "1.10.4" # 这个其实无所谓
wasm-bindgen = "0.2" # 不多说
web-sys = { version = "0.3.69", features = ["console"] } # 基于js-sys封装的,我们需要它的log等
[profile.release]
opt-level = "s"
噢,差点忘了,在这之前你还需要安装wasm-pack,至于如何安装,可以去看搭配WebAssembly那篇文章,这里不多说。
这就行了
rust实现#
这里直接贴代码:
use std::sync::Mutex;
use js_sys::{global, Array, Function, JsString, Number, Object, Promise, Reflect};
use regex::Regex;
use wasm_bindgen::{closure, prelude::*};
use web_sys::console;
pub fn get_nested_property(
node: &JsValue,
keys: &Vec<&str>,
depth: usize,
) -> Result<JsValue, JsValue> {
if depth >= keys.len() {
return Ok(node.clone());
}
match node.is_object() {
true => {
let next = Reflect::get(&node, &JsValue::from(keys[depth]))?;
get_nested_property(&next, keys, depth + 1)
}
_ => Err(JsValue::from(format!(
"current depth is not object, key: {}",
keys[0.min(depth - 1)],
))),
}
}
#[wasm_bindgen]
pub fn func(path: &Object) {
let file_name: JsString =
get_nested_property(&path, &vec!["hub", "file", "opts", "filename"], 0)
.unwrap_or_else(|_e| {
console::warn_1(&_e);
JsString::from("").into()
})
.into();
let file_name = file_name.as_string().unwrap_or_else(|| String::new());
let reg =
Regex::new(r#"^(?:[^\\]*\\){2,}(utils|api|wxs)[^\\]*\\.*\.js$"#).expect("get regexp fail");
if !file_name.contains("node_modules") && reg.is_match(&file_name) {
let node = get_nested_property(&path, &vec!["node"], 0).unwrap_or_else(|_e| {
console::warn_1(&_e);
JsValue::undefined()
});
let node_declaration =
get_nested_property(&node, &vec!["declaration"], 0).unwrap_or_else(|_e| {
console::warn_1(&_e);
JsValue::undefined()
});
let node_leading_comments = get_nested_property(&node, &vec!["leadingComments"], 0)
.unwrap_or_else(|_e| {
console::warn_1(&_e);
JsValue::undefined()
});
let line: Number = get_nested_property(&node, &vec!["loc", "start", "line"], 0)
.unwrap_or_else(|_| Number::from(-1).into())
.into();
let column: Number = get_nested_property(&node, &vec!["loc", "start", "column"], 0)
.unwrap_or_else(|_| Number::from(-1).into())
.into();
match (
get_nested_property(&node_declaration, &vec!["type"], 0)
.unwrap_or_else(|_e| {
console::warn_1(&_e);
JsString::from("").into()
})
.as_string()
.unwrap_or_else(|| "".to_string())
.as_str(),
node_leading_comments.is_undefined(),
) {
("FunctionDeclaration", true) => {
let item =
get_nested_property(&node_declaration, &vec!["id"], 0).unwrap_or_else(|_e| {
console::warn_1(&_e);
JsValue::undefined()
});
let item_name = get_nested_property(&item, &vec!["name"], 0)
.unwrap_or_else(|_e| {
console::warn_1(&_e);
JsString::from("").into()
})
.as_string()
.unwrap_or_else(|| String::new());
console::warn_1(&JsString::from(format!(
"\x1B[32mGreen 公共文件“{}”里的方法“{}”没有写注释,麻烦遵循规范,谢谢!位置:第“{}”行“{}”列\x1B[0m",
file_name, item_name, &line, &column
)));
}
("VariableDeclaration", true) => {
let declarations: Array =
get_nested_property(&node_declaration, &vec!["declarations"], 0)
.unwrap_or_else(|_e| {
console::warn_1(&_e);
Array::new().into()
})
.into();
let mut map_clo = |e, _i, _arr| {
let init_type = get_nested_property(&e, &vec!["init", "type"], 0)
.unwrap_or_else(|_e| {
console::warn_1(&_e);
JsString::from("").into()
})
.as_string()
.unwrap_or_else(|| String::new());
match init_type.as_str() {
"ArrowFunctionExpression" | "FunctionExpression" => {
let item_name = get_nested_property(&e, &vec!["id", "name"], 0)
.unwrap_or_else(|_e| {
console::warn_1(&_e);
JsString::from("").into()
})
.as_string()
.unwrap_or_else(|| String::new());
console::warn_1(&JsString::from(format!(
"\x1B[32mGreen 公共文件“{}”里的方法“{}”没有写注释,麻烦遵循规范,谢谢!位置:第“{}”行“{}”列\x1B[0m",
file_name, item_name, &line, &column
)));
}
_ => {}
}
e
};
let res = declarations.map(&mut map_clo);
}
_ => {}
}
}
}
// static MY_PLUGIN_OBJ: Lazy<Mutex<Object>> = Lazy::new(|| Mutex::new(Object::new()));
#[wasm_bindgen]
pub fn init() -> Result<Object, JsValue> {
let mut obj = Object::new();
Reflect::set(&obj, &JsString::from("name"), &JsString::from("my_plugin"))?;
let mut visitor = Object::new();
let func =
wasm_bindgen::closure::Closure::wrap(Box::new(func) as Box<dyn FnMut(&Object) + 'static>);
Reflect::set(
&visitor,
&JsString::from("ExportDeclaration"),
&func.into_js_value(),
)?;
Reflect::set(&obj, &JsString::from("visitor"), &visitor)?;
Ok(obj)
}
#[wasm_bindgen],如果你的函数需要暴露到js中,那么这个是必须的。wasm_bindgen::closure::Closure::wrap:将一个闭包转换成js函数,其实js-sys提供了Function,并且可以基于它创建js函数,但是这就本末倒置了,我们希望的是逻辑是rust实现的。这里也不一定要用函数,也可以是一个闭包。Reflect::set和Reflect::get不用多说,给一个对象赋值和读对象的键值,这里我是通过递归去寻找深层的字段,如果你有更好的方案请务必在评论区里说下,不胜感激~declarations.map:也就是Array::map,注意不要用iter().map(),这样是会报错的,打了半天log才发现。
代码写的有些乱,因为遇到一些类型转换之类的问题,它会很暴力的直接panic,并且错误信息不可读,所以这里无奈写了一堆错误log和兜底赋值,不然一个?就解决了。
其它也没啥好说的了,逻辑也就这样。
然后我们来打包。
打包#
这里我们直接通过npm来运行指令,基于npm的原因是后面如果我们想打包成一个npm包,那么npm是必要的,所以直接省事:
npm init --yes然后配置下script:
"build": "wasm-pack build --release -t nodejs --out-dir dist --out-name index",
"build-debug": "wasm-pack build --release -t nodejs --out-dir dist --out-name index -- --features debug"out-dir:表示打包的文件夹名字--out-name:入口文件名-t:自然就是target,这里我们选择nodejs
然后打包完成,在根目录会出现一个dist文件夹:
.gitignore
index_bg.wasm
index_bg.wasm.d.ts
index.d.ts
index.js
package.json那么到这就完成了
使用#
因为我们没有发布npm包,所以只能手动将这个文件夹里的内容复制到项目中,inedx.js中有对wasm的加载逻辑,所以我们不必写load wasm的逻辑,直接导入即可:
if (process.env.NODE_ENV !== 'production') {
const wasm = require('./scripts/plugins/babel/check-comment-wasm/index');
plugins.push(wasm.init);
}测试#
就不测试了,主要是得放图,很麻烦~
总结#
内容很少,但是耗时很长,因为还在学习中没有好的调试方案。。。如果你有好的调试方案请务必和我说下,谢谢!
