GoGoCode协助清理代码中的「垃圾」

1,881 阅读6分钟

本文来自 GoGoCode 用户涛哥投稿

什么是「垃圾」代码?

什么是垃圾代码?如果你想知道什么是垃圾代码的话,我现在就带你研究!

权限管理场景中,我们经常会根据权限码的不同来调用不同的接口,或者展现不同的UI界面,以实现功能的灰度测试,在新功能稳定运行之后,全量开放给更多的用户。

比如我们的业务代码中,用全局对象Tryout挂载不同的权限属性,比如某个功能权限码sid=123,那我们的全局对象在经过初始化之后,会以固定的格式Tryout.TRYOUT_SID_123来标识当前用户是否有当前权限。

if(Tryout.TRYOUT_SID_123) {
    doSomething()
}
else {
    doOtherthing()
}

或者:

let modelUrl = Tryout.TRYOUT_SID_123 ? 'url_A' : 'url_B';

或者:

let obj = {
    proA:  Tryout.TRYOUT_SID_123 ? 'aaa' : 'bbb'
}

在模板代码中,我们以mustache模板为例,我们的模板代码可能会有如下代码:

{{if Tryout.TRYOUT_SID_123}}
  <templateA>
{{else}}
  <templateB>
{{/if}}

以上场景的代码会因为一次功能的迭代,存在我们的业务代码的js文件和html文件中。

那么随着时间的推移,项目的不断迭代,业务逻辑不断增加,这些代码会变得越来越多,相互堆叠在一起,使得我们的代码看起来越来越复杂。

最后代码看起来可能是这样:

if(Tryout.TRYOUT_SID_123 && Tryout.TRYOUT_SID_234) {
    doSomething()
}
else if(Tryout.TRYOUT_SID_345 || Tryout.TRYOUT_SID_456){
    doOtherthing()
}
...
else {
...
}

当我们再次回到这些页面开发新的功能的时候,这些逻辑堆叠在一起,我们就需要去了解之前的代码到底做了什么,为什么会有这些逻辑,梳理完这些老的代码,我们才能去做新功能的开发,上面的示例看起来比较简单,但是实际上的业务比上述例子要复杂的多,这样在梳理这些逻辑的时候,会消耗掉我们很多的开发时间。

实际上,随着业务权限的放开,很多的功能其实在经过一段时间的灰度测试之后,已经开放给了所有的客户,也可能因为功能的使用率比较低,而做了下线处理,也就是说,很多的条件判断逻辑在代码中已经不需要了,那这些代码仍然存在在业务逻辑中,这些代码除了增加我们的开发成本,毫无其他用处。

如何清理“垃圾”代码

既然这些代码已经是过去时了,我们整理一下这些代码,不就好了吗,仍然以最开始的示例代码为例:

if(Tryout.TRYOUT_SID_123) {
    doSomething()
}
else {
    doOtherthing()
}

Tryout.TRYOUT_SID_123 === true 时,我们此时就不需要关注 else 里面的内容了,上面的代码就可以简化为:

doSomething()

Tryout.TRYOUT_SID_123 === false 时,就可以简化为:

doOtherthing()

嗯,看起来还是可以手动去做的。

但是我在我们的业务代码中,我们的权限码的数量已经快要达到四位数了,而每一个权限吗对应的代码逻辑模块可能达到几十上百处,然后再想想,还有欲望去手动整理吗,我想谁也不会想去浪费时间整理这些“垃圾”。

做完了,看不到任何效果,做错了,哪怕是多删了一个字符,也会让业务代码崩掉,这个风险和收益是完全不成正比的,就这么放着的话,下次功能迭代的时候,遇到功能特别复杂的页面,看到各种权限代码穿插,完全没有开发的欲望。

那怎么办呢?我们能否有一种自动化的方式去帮我们做这件事,只要告诉工具哪个权限码全量了或者下线了,就能自动帮我们清理掉这些逻辑呢?

场景整理

我们先从js文件的转换看起,经过梳理这些权限码所在的代码片段,我们发现 99% 的场景都是以下三种场景

变量对象赋值场景

var a = Tryout.TRYOUT_SID_sid
let b = Tryout.TRYOUT_SID_sid
const c = Tryout.TRYOUT_SID_sid

let d = {
    obj: Tryout.TRYOUT_SID_sid
}

三元运算场景

Tryout.TRYOUT_SID_ ? 'aaa' : 'bbb'
!Tryout.TRYOUT_SID_ ? 'aaa' : 'bbb'

条件判断场景 if...else

if(Tryout.TRYOUT_SID_123 && aaa || bbb) {
    doSomething()
}
else if(Tryout.TRYOUT_SID_234) {
    doOtherthing()
}
else {
    doElse()
}

现在我们基本整理出了上面三种需要处理的场景,因为我们的权限码是固定的格式:Tryout_TRYOUT_SID_xxx,看起来是复合正则匹配的场景的,那我们是否可以通过正则匹配的方式来处理代码呢,思考一下。

代码中的条件判断写法千变万化,运算的优先级,各种嵌套,一句话,代码的写法多种多样,没有固定的规则去适配这些规则。

看到这里,自然而然就想到,如果用 AST 来处理这些代码,是不是会变得很简单呢?

看过这篇文章:0成本上手AST,用GoGoCode解决Vue2迁移Vue3难题 后,感觉这是个操作AST的神器,我决定用它来试一试!

代码处理

我们总结一下上述三个需要处理的场景,假设我们要处理的权限码已经全量(=== true),比如针对赋值场景,我们的转换目标如下:

var a = Tryout.TRYOUT_SID_sid
let b = Tryout.TRYOUT_SID_sid
const c = Tryout.TRYOUT_SID_sid

let d = {
    obj: Tryout.TRYOUT_SID_sid
}

转换后 =>

var a = true
let b = true
const c = true

let d = {
    obj: true
}

GoGoCode 的转换代码:

// 变量赋值场景
result = AST.replace([
    `var $_$ = Tryout.TRYOUT_SID_${sid}`,
    `let $_$ = Tryout.TRYOUT_SID_${sid}`,
    `const $_$ = Tryout.TRYOUT_SID_${sid}`
], 'let $_$ = true;')

三元运算场景:

let test = Tryout.TRYOUT_SID_sid ? 'aaa' : 'bbb'
let test = !Tryout.TRYOUT_SID_sid ? 'aaa' : 'bbb'

转换后 =>

let test = 'aaa'
let test = 'bbb'

GoGoCode 的转换代码:

// 三元运算符为true场景
result = AST.replace(`Tryout.TRYOUT_SID_${sid} ? $_$1 : $_$2`, '$_$1')
// 三元运算符为false场景
result = AST.replace(`!Tryout.TRYOUT_SID_${sid} ? $_$1 : $_$2`, '$_$2')

条件判断if

嗯?这里的 GoGoCode 转换怎么写呢,因为 if 里面的场景实在是太多了, if 语句里面可以有表达式,有逻辑运算,看起来简单的 replace 方案已经无法处理这种场景了。

然后我们回过头去看看上面的赋值语句以及三元运算的转换,我们写的规则是不是太简单了,如果代码逻辑再稍微复杂一点,比如三元运算:

let test = Tryout.TRYOUT_SID_sid ? 'aaa' : (isA ? 'bbb' : 'ddd');

上述是最简单的代码转换,要覆盖更多场景,我们还要下更多的功夫。

逻辑运算处理

我们举个更复杂的例子,试用功能变量 Tryout.TRYOUT_SID_123 参与了复杂的逻辑运算

if(Tryout.TRYOUT_SID_123 || (aa && bb) && cc || (a == b)) {
    doSomething()
}
else {
    doOtherthing()
}

if 语句里面包含的内容,在AST结构里面叫做 test,其实我们知道 Tryout.TRYOUT_SID_123 === true ,那么true与任何值的||运算,最终结果都是true,我们根本不关心这个test的构成。

反过来,如果 Tryout.TRYOUT_SID_123 === falsefalse与其他值的||操作,那就要忽略这个false,继续执行后面的判断,两种不同的场景,转换后的代码也是完全不同的,我们希望得到的转换结果如下:

Tryout.TRYOUT_SID_123 === true

doSomething()

Tryout.TRYOUT_SID_123 === false

if((aa && bb) && cc || (a == b)) {
    doSomething()
}
else {
    doOtherthing()
}

总结一下,在这个条件判断的运算中,这里我们只需要去计算 If 语句的值,就可以得到我们希望转换的代码结果:

如果值为true,我们就把 if 语句里面的内容拿出来;

如果值为false,我们就把 else 语句里面的内容拿出来;

如果test值不确定,那就清理掉确定的部分,比如上面的下线场景,我们删除确认为falseTryout.TRYOUT_SID_123 就可以了,其他不变。

再回头看看三元运算和条件赋值,其实都是逻辑运算。

逻辑运算简化

看到这里,我们的核心问题就变成了逻辑运算的简化问题,这里用一张简单的图给大家分享一下处理的过程,当然这里也要借助 GoGoCode 的强大的查找功能:

截屏2021-04-19 上午10.24.47.png

我们把逻辑运算的处理封装成了一个工具函数excuteIF,这个工具函数接受一个条件判断的语句,接受一个对象作为参数,这个参数用来标识已知的条件

excuteIF(
    '(Tryout.TRYOUT_SID_123 || (aa && bb) && cc || (a == b)',
    {
        Tryout.TRYOUT_SID_123: true
    }
)

这个函数的返回简化后的结果,比如上面 Tryout.TRYOUT_SID_123===false 的例子,那将返回

(aa && bb) && cc || (a == b)

我们来尝试实现一下这个函数:

const $ = require('gogocode')

excuteIF = function (caseStr, options) {
    // 生成gogocodeAST
    let g_ast = $(caseStr)
    // 循环处理入参中传入的值  
    for (let o in options) {
      // 查找目标代码
      let node = g_ast.find(o)
      // 标识查找的目标值是true还是false,以执行不同的操作
      let excuteType = options[o]
      let parent = node.parent()
      let nodePath = node[0].nodePath
      let parentNode = parent[0].nodePath.node

      // 如果父级为逻辑运算
      if (parentNode.type === 'LogicalExpression') {
        // true场景
        if(excuteType) {
          // 与true「或」操作
          if (parentNode.operator === '||') {
              parent.replaceBy($('true'))
          }
          // 与true「与操作」
          else if (parentNode.operator === '&&') {
              // 替换掉目标代码
              if (nodePath.name === 'left') {
                parent.replaceBy(parentNode.right)
              } else if (nodePath.name === 'right') {
                parent.replaceBy(parentNode.left)
              }
          }
        }
        // false场景
        else {
            // 与false「与」操作
          if (parentNode.operator === '&&') {
              parent.replaceBy($('false'))
          }
          // 与false「或」操作」
          else if (parentNode.operator === '||') {
              // 替换掉目标代码
              if (nodePath.name === 'left') {
                parent.replaceBy(parentNode.right)
              } else if (nodePath.name === 'right') {
                parent.replaceBy(parentNode.left)
              }
          }
        }
      }
    }
    // 重新生成代码
    return g_ast.generate()
}

let result = excuteIF('Tryout.TRYOUT_SID_123 || aa && bb', {
  'Tryout.TRYOUT_SID_123': false
})

// result这里被处理成了aa && bb
return result

以上代码的执行效果,大家可以到 playground 亲自试试,代码地址点这里

成果展示

有了上述理论基础,我们就可以尝试进行真实业务代码的处理了:

截屏2021-04-19 上午11.29.45.png

可以看到,代码转换之后,代码被大大的简化了,逻辑也变得足够清晰,在简洁的代码基础之上再去做迭代,是不是感觉有信心多了。

总结

到这里,再回过头来整理一下我们清理js文件的思路:

  1. 拿到已经全量或者下线的权限码
  2. 获取含有特定权限码标记的文件
  3. 找到包含权限码的代码块
  4. 处理逻辑运算的结果
  5. 根据逻辑运算的结果操作AST树,替换相应的代码,生成新的代码

然后我们再把上述步骤封装成一个命令行工具,这样我们现在敲入npm run sid -- 123,就可以完成单个的权限码123的清理工作了。

安利时间

大家还可能会问我,为什么是 GoGoCode 呢,为什么不是 Babel 或者 jscodeshift 呢,当然可以是,但是我只能告诉你,相比较其他两个工具,GoGoCode 能更快的让你上手,而且代码量非常少,API 相当丰富,playground 也已上线,如果你还不了解,那么现在就开始吧:

GoGoCode的Github仓库(新项目求star ^_^) github.com/thx/gogocod…

GoGoCode的官网 gogocode.io/zh

可以来 playground 快速体验一下 play.gogocode.io/