你不会还没用过这4个babel插件吧?

78 阅读2分钟

背景

最近研究如何将 JSON 转换成 .vue 文件,JSON 格式如下:

{
    "state": {  "text": { "type": "JSExpression",    "value": "\"outer\"",    "compiled": "var text: string = \"outer\";"  }},
    "lifeCycles": {  "onMounted": {    "type": "JSFunction",    "value": "()=>{console.log(\"did mount\", text);}",   "compiled": "()=>{console.log(\"did mount\", text);}"  },  "onBeforeMount": {    "type": "JSFunction",    "value": "()=>{console.log(\"will unmount\")}",    "compiled": "()=>{console.log(\"will unmount\")}"  }},
    "methods": {  "testFunc": {    "type": "JSFunction",    "value": "function testFunc() {\n  console.log('test func', text);\n  text = 'inner';\n}",    "source": "function testFunc() {\n  console.log('test func', text);\n  text = 'inner';\n}"  }},
    "children": 主要存储组件信息,用于生成template
}

生成要求如下:

1)state 中所有的变量均采用 const 变量名 = ref(xxx) 的方式定义

2)methods 中如果 value 是匿名函数,则使用 const 方法名 = 函数内容 的方式定义

3)以 composition api 的方式组织代码

根据这个 JSON 处理出 js 部分的内容不是难点,主要要解决变量定义的问题,例如 text 生成的代码:

const text = ref('outer')

我们可以看到 JSON 中 methods 或 lifeCycles 有使用该变量,但是其用法并不正确,在 JS 中使用 ref 定义的变量,正确用法应该是:变量名.value,那么我们应该怎么处理才能满足要求呢?

解决思路

要解决这个问题,很多人可能下意识都会想到处理 string ,通过正则匹配等方式做替换。这种方案理论上也可行,看起来似乎很简单,但是极可能无法考虑到所有的场景,容易出问题。

而借助 babel 插件去解决这个问题将会非常简单,主要会用到以下3个插件:

1)@babel/parser 用来将代码转成 ast

2)@babel/traverse 遍历 ast

3)@babel/generate 将处理后的 ast 转成代码

我们再来看看函数中使用到变量的场景,主要有2个:赋值、作为参数传递,而 babel 在转换的过程中,会将这两种场景分别标注为 AssignmentExpression 和 CallExpression 中的 argements

想要探索 ast 结构的可以使用这个网站:astexplorer.net/

接下来直接上代码

import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
function rewriteMethod(fnString, state) {  
    const fnAst = parser.parse(fnString, { 
       plugins: ['typescript'],  
    })  

    traverse(fnAst, {    
        AssignmentExpression(path: any) {    
          const left = path.node.left;    
              if (state[left.name] && left.type === 'Identifier') {    
                left.name += '.value';     
              }    
        },    
        CallExpression(path: any) { 
          const args = path.node.arguments;
          for (const arg of args) {  
              if (state[arg.name] && t.isIdentifier(arg)) {   
                   arg.name += '.value'        
              }     
          }  
        } 
    })  

    return generate(fnAst).code
}

通过 traverse 遍历 ast ,并更新 ast 的部分内容,最后再将修改后的 ast 生成代码。

拓展一下

在babel 插件中,@babel/types 也常用于处理代码,主要用于重新生成函数、变量等。例如,我想将一个 ts 定义的变量或函数参数,处理成普通的 js 代码,便可先 parser 得到 ast,然后 traverse ast ,并使用 @babel/types 重新定义这些参数,最后 generate 将 ast 转成代码。

具体场景代码就不贴了,有兴趣欢迎探讨👏🏻

写在最后

最近也研究了如何将 Monaco 中写的 JS 代码转换成 JSON,同样使用了这4个 babel 插件,对于要重新分析代码做各种事情的场景都非常好用。