全局替换的思路历程

任务拆解:我要做什么

背景:在我们的项目页面中,存储单位显示的都是不准确的(老的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;
      }
    );
    ...
  } 
};

但是很快就遇到了不可解决的问题:

  1. 新的formatSize函数入参顺序和结构和老的不一样,这个用正则进行替换过于复杂
  2. 方法写法一旦有一点变化 ( e.g 换行了,入参不是单纯的string/number ) ,正则就无法进行正确匹配
  3. 场景难以区分: 使用的case:formatSize("B", 1) 和 定义的case:const formatSize = ()=>{xxx}正则不好区分

等等各种各样的问题... 以至于后来发现解决正则替换带来的问题和难度,比使用别的方法比如AST带来的难度困难更多更大,so.... 果断放弃!

正则替换的优劣势:

  • 优势:上手简单,正则匹配十分精准,并且不会破坏原有的代码结构风格,可谓是"指哪儿打哪儿",只要匹配准确,替换更新准确率就是100%

  • 劣势:不够灵活,只能从字符串层面去进行匹配和替换,而不能从代码/函数/组件结构进行分析

Babel

既然从代码字符层面进行正则匹配统一替换很困难,那就从代码结构考虑,改用AST的方式去实现吧!

作为前端同学都知道我们平时写的es6的js代码是经过 babel 编译成为es5格式后的js 才可以在浏览器中运行的(虽然现在有些浏览器也支持直接运行es6语法的js了)Babel在线工具

babel转换es6 js代码的原理就是:

  1. 使用 babel/parser 解析源代码,生成AST语法树
  2. 利用 babel-traverse 遍历AST树进行替换操作,生成新的AST树(babel plugin)
  3. 利用 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

👉 github.com/facebook/js...

在快要放弃的时候,无意中看到了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结合正则替换去实现:

  1. jscodeshit 对旧方法formatSize的全局替换/转换
  2. 正则匹配 对一些固定代码(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. # 其他

TODO

  • 整理一份jscodeshift食用文档,方便大家后续使用参考
  • 社区是否有现成通用的不影响代码风格格式的转换插件/工具使用

后续也有迁移功能要做的童鞋,可以一起讨论,或者共建迁移通用文档,或者思考一个通用的迁移方案等等

相关推荐
秋天的一阵风3 分钟前
Vue3探秘系列— 路由:vue-router的实现原理(十六-上)
前端·vue.js·面试
秋天的一阵风4 分钟前
Vue3探秘系列— 路由:vue-router的实现原理(十六-下)
前端·vue.js·面试
海底火旺24 分钟前
JavaScript中的Object方法完全指南:从基础到高级应用
前端·javascript·面试
海底火旺25 分钟前
JavaScript中的Symbol:解锁对象属性的新维度
前端·javascript·面试
天天扭码25 分钟前
一文吃透 ES6新特性——解构语法
前端·javascript·面试
Kagerou26 分钟前
组件测试
前端
JustHappy28 分钟前
啥是Hooks?为啥要用Hooks?Hooks该怎么用?像是Vue中的什么?React Hooks的使用姿势(上)
前端·vue.js·react.js
张可43 分钟前
历时两年半开发,Fread 项目现在决定开源,基于 Kotlin Multiplatform 和 Compose Multiplatform 实现
android·前端·kotlin
嘻嘻嘻嘻嘻嘻ys1 小时前
《Spring Boot 3 + GraalVM原生镜像实战:云原生时代的毫秒启动与性能调优》
前端·后端
嘻嘻嘻嘻嘻嘻ys1 小时前
《Spring Boot 3.0×GraalVM:云原生时代的毫秒级启动实战革命》
前端·后端