任务拆解:我要做什么
背景:在我们的项目页面中,存储单位显示的都是不准确的(老的formatSize中使用的单位不对导致的),所以在新的版本中需要去推进所有的项目进行一个统一的替换更改
目的:由于组内项目众多,使用老foramatSize方法地方也很多,处于方便业务方接入的目的,所以需要提供一个命令,让接入方进行一键替换
方案现状:有哪些可选方案
小小的调研了一下之前组内迁移方案都是【正则匹配,整体替换】:通过正则匹配到固定的字符串之后进行一个整体替换
方案评估
正则匹配
所以,一开始迁移到bit formatSize也是想通过正则匹配进行一个整体替换,具体代码可见 👇
ini
#!/usr/bin/env node
const formatSizeTransfer = file => {
//获取字符串格式的文件
const fileStr = fs.readFileSync(file, { encoding: "utf-8" });
try {
//进行正则匹配和替换
const newFileStr1 = fileStr.replace(
/formatSize([\s\S]+?)/g,
matchStr => {
const first = matchStr.indexOf("(");
const last = matchStr.lastIndexOf(")");
// 获取入参数,对入参进行顺序/格式等更改
const arr = splitStr.split(",");
const param1 = arr[0].replace(/^\s*|\s*$/g, "").replace(",", "");
const param2 = arr[1].replace(/^\s*|\s*$/g, "").replace(",", "");
const param3 = arr[2];
const param4 = arr[3];
let newParam3 = {};
if (param3) {
newParam3["showUnit"] = param3;
}
if (param4) {
newParam3["toFixedNum"] = param4;
}
let returnStr = "";
// 进行替换
if (param3 || param4) {
returnStr = `UnitUtils.formatSize(${param2}, ${param1}, ${JSON.stringify(
newParam3
)})`;
} else {
returnStr = `UnitUtils.formatSize(${param2}, ${param1})`;
}
return returnStr;
}
);
...
}
};
但是很快就遇到了不可解决的问题:
- 新的formatSize函数入参顺序和结构和老的不一样,这个用正则进行替换过于复杂
- 方法写法一旦有一点变化 ( e.g 换行了,入参不是单纯的string/number ) ,正则就无法进行正确匹配
- 场景难以区分: 使用的case:
formatSize("B", 1)
和 定义的case:const formatSize = ()=>{xxx}
正则不好区分
等等各种各样的问题... 以至于后来发现解决正则替换带来的问题和难度,比使用别的方法比如AST带来的难度困难更多更大,so.... 果断放弃!
正则替换的优劣势:
-
优势:上手简单,正则匹配十分精准,并且不会破坏原有的代码结构风格,可谓是"指哪儿打哪儿",只要匹配准确,替换更新准确率就是100%
-
劣势:不够灵活,只能从字符串层面去进行匹配和替换,而不能从代码/函数/组件结构进行分析
Babel
既然从代码字符层面进行正则匹配统一替换很困难,那就从代码结构考虑,改用AST的方式去实现吧!
作为前端同学都知道我们平时写的es6的js代码是经过 babel 编译成为es5格式后的js 才可以在浏览器中运行的(虽然现在有些浏览器也支持直接运行es6语法的js了)Babel在线工具
babel转换es6 js代码的原理就是:
- 使用 babel/parser 解析源代码,生成AST语法树
- 利用 babel-traverse 遍历AST树进行替换操作,生成新的AST树(babel plugin)
- 利用 babel-generator 将 AST 树输出为转码后的代码字符串
废话不多说,上代码:
ini
// #!/usr/bin/env node
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const formatSizeTransfer = fileStr => {
try {
// babel/parser把代码转为ast
const ast = parser.parse(fileStr, {
sourceType: "module",
plugins: ["jsx", "typescript"]
});
// babel-traverse遍历ast
traverse(ast, {
Identifier(path) {
if (path.node.name === "formatSize") {
if (path.container.arguments && path.container.arguments.length) {
const param1 = path.container.arguments[0];
const param2 = path.container.arguments[1];
const param3 = path.container.arguments[2];
const param4 = path.container.arguments[3];
path.container.arguments[0] = param2;
path.container.arguments[1] = param1;
let showUnit;
let toFixedNum;
if (!!param3) {
showUnit = t.objectProperty(
t.identifier("showUnit"),
t.stringLiteral(param3.value)
);
}
if (!!param4) {
toFixedNum = t.objectProperty(
t.identifier("toFixedNum"),
t.numericLiteral(param4.value)
);
}
if (!!param3 || !!param4) {
const newParam3 = t.objectExpression([showUnit, toFixedNum]);
path.container.arguments[2] = null;
path.container.arguments[3] = null;
path.container.arguments[2] = newParam3;
}
}
}
}
});
// babel/generator 把ast重新"组装"成js代码返回
const transformedCode = generate(ast).code;
return transformedCode;
} catch (err) {}
};
在操作ast的帮助下,旧formatSize可以正确的被替换成新的方法的格式了🎉~
但是这个时候新的问题又又又来了,替换是替换成功了,但原代码的格式也全部丢失了 👇:
原因:在原代码经过babel-parser解析为ast的过程中丢失了原本的format,导致反写ast的时候无法恢复到原始格式
php
// parse后使代码失去原有format
const ast = parser.parse(fileStr, {
sourceType: "module",
plugins: ["jsx", "typescript"]
});
babel
的目的是对代码向下兼容的,会进行代码转换,即使不做任何修改,输出的代码和原本的也有区别,比如空格,空行,注释位置会变化
Jscodeshift
在快要放弃的时候,无意中看到了facebook曾提供了一个叫jscodeshift
的工具,它是一个重构代码的工具集,对recast(一个通过分析AST做代码修改的库)做了封装,通过jscodeshift
编写codemod
, 然后对指定文件运行就可以批量重构代码,大大减少了体力劳动,并可复用
jscodeshift
的官方文档中写道:
jscodeshift is a toolkit for running codemods over multiple JavaScript or TypeScript files. It provides:
- A runner, which executes the provided transform for each file passed to it. It also outputs a summary of how many files have (not) been transformed.
- A wrapper around recast, providing a different API. Recast is an AST-to-AST transform tool and also tries to preserve the style of original code as much as possible .
最重点的在第二句的最后部分:jscodeshift在转换的同时会尽量保持原有代码的格式! 果然是山重水复疑无路 柳暗花明又一村~ 并且jscodeshift默认支持Typescript的解析,不可谓不好用~!
对于jscodeshift,recast
这个工具则是核心,其提供了对源码进行 AST 分析以及修改的能力,同时可以最大程度地保留原代码的格式,对于 AST 中没有修改的部分,他会原封不动地进行输出,这也是jscodeshift能够不改变原代码风格的原因,其也支持对 AST 进行格式化的代码输出
3.4 方案总结/选择
经过对比,最后决定使用 jscodeshift结合正则替换去实现:
- jscodeshit 对旧方法formatSize的全局替换/转换
- 正则匹配 对一些固定代码(import的部分)进行替换
优点 | 缺点 | |
---|---|---|
正则 | 1. 上手简单 2. 匹配十分精准 3. 不会破坏原有的代码结构风格 | 不够灵活,不能满足复杂case |
Babel | 1. 灵活,从代码结构层面进行全局替换操作 2. 现有文档丰富完整 | 1. 需要手动将源代码转换为ast,还涉及到了jsx,ts的转换 2. 破坏原有代码格式 |
Jscodeshift | 1. 同Babel一样灵活 2. 不破坏原有代码风格 | 文档太少,有更多更深的需求需要去看源码 |
最后效果
所以在经历了正则,babel,jscodeshift之后,终于实现了一键无痛替换formatSize,代码已上传到bnpm,有兴趣的同学可以down下来看,核心部分语法和babel差不多:bnpm.bytedance.net/package/@by...
jscodeshift转换后的效果👇,精确替换只需要替换的部分而不会更改原文件的结构
5. # 其他
-
ast-explorer :超强的 AST 可视化工具
- 源代码粘贴在左侧,即可即时在右侧看到解析代码获得的语法树并查看其中各个节点的属性
- 打开 "Transform" 开关,你还可以直接在浏览器里书写 codemod 脚本,并即时看到 codemod 转换后的效果
-
Babel 插件手册 想了解一下babel的童鞋阔以看看
TODO
- 整理一份jscodeshift食用文档,方便大家后续使用参考
- 社区是否有现成通用的不影响代码风格格式的转换插件/工具使用
后续也有迁移功能要做的童鞋,可以一起讨论,或者共建迁移通用文档,或者思考一个通用的迁移方案等等