实现一个真正的babel插件(不仅仅是替换字符)及 ast操作原理

24 篇文章 0 订阅

babel作为当前源码编译的重要工具,有着很重要的地位。babel编译的核心流程是,先把代码解析为AST语法树,再遍历AST语法树并执行操作,最后根据规则生成代码。流程不复杂,复杂的是如何操作AST语法树,以及如何编写babel的插件。

网上有很多帖子在讲如何编写babel插件,但是讲的都比较浅显,看过之后并不能真正意义上去编写babel插件。在实际的项目中,我们需要的插件不仅仅是替换字符串或者打印出什么那么简单,接下来本文会实现一个含有表达式生成,节点类型分析,逻辑判断的babel插件。

业务需求:

在代码require('test')之后加上.default,实现module模块和es6中export的兼容。

说明:这个需求场景是我在升级项目的时候遇到的,在升级babel后,项目中require('test')之类的会出现报错,经查得到是模块规范未统一,需要加.default后不报错。由于项目中有太多的地方使用该场景,所以考虑采用增加babel插件的方法解决该bug。

编写插件之前:

先说下关于ast语法树的定义和操作相关

ast语法树是由许多节点(node)组成的,node之前的关系使用path表示,path是一个可操作的大的对象,有很多方法集成在上面。node有许多属性,比如type,start,end等。node可以通过defineType(args)生成,具体的可以参考babel的官方文档,这里不再详细介绍。babel-ast文档

ast遍历时采用的是树的深度优先遍历(深度优先遍历参见我的另一篇文章,树结构)。

babel中常用的库和工具类:

  • @babel/parser 将源代码解析成 AST。
  • @babel/generator 将AST解码生 js代码。
  • @babel/core 包括了整个babel工作流,也就是说在@babel/core里面我们会使用到@babel/parser、transformer[s]、以及@babel/generator。
  • @babel/code-frame 用于生成错误信息并且打印出错误原因和错误行数。(其实就是个console工具类)
  • @babel/helpers 也是工具类,提供了一些内置的函数实现,主要用于babel插件的开发。
  • @babel/runtime 也是工具类,但是是为了babel编译时提供一些基础工具库。作用于transformer[s]阶段,当然这是一个工具库,如果要使用这个工具库,还需要引入@babel/plugin-transform-runtime,它才是transformer[s]阶段里面的主角。
  • @babel/template 也是工具类,主要用途是为parser提供模板引擎,更加快速的转化成AST
  • @babel/traverse 也是工具类,主要用途是来便利AST树,也就是在@babel/generator过程中生效。
  • @babel/types 也是工具类,主要用途是在创建AST的过程中判断各种语法的类型。
@babel/code-frame 为全局错误捕获工具类

@babel/core
├── 输入字符串
├── @babel/parser
│   └── @babel/template
│       └── @babel/types
├── AST
├── transformer[s]
│   └── @babel/helpers
├── AST
├── @babel/generator
│   └── @babel/traverse

访问AST时的visitor对象

编写插件主要是编写visitor对象,即告诉遍历ast时要访问哪些类型的代码,以及对这些代码要做的操作。

比如:在访问对应的节点时,接收两个对象path,state(全局状态, 局部状态)

const MyVisitor = {
  Identifier(path,state) {
    console.log("找到你要访问的字符标识,要进行什么操作");
  }
};

在访问者中,会有进入和离开的属性,写法是这样的:

const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!");
    }
  }
};

path对象

这里不做详细讲解,只讨论下path.traverse()方法,以及给visitor传参数

实例:

const updateParamNameVisitor = {
  Identifier(path) {
    if (path.node.name === this.paramName) {//this.paramName局部state
      path.node.name = "x";
    }
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0];
    const paramName = param.name;
    param.name = "x";

    path.traverse(updateParamNameVisitor, { paramName });//给visitor传参数
  }
};

path.traverse(MyVisitor);

从上面的例子还可以看出visitor是可以嵌套的。

实际编写中一般使用的是@babel/traverse

操作代码@babel/types

@babel/types内集成了很多方法,具体参见:@babel/types这里介绍下总体概念,不详细介绍.

比如:const t = reauire(‘@babel/types')

t.isIndentifier和t.indentifier,前者是判断isIndentifier,后者是生成一个indentifier。

生成代码@babel/generator

const result = generate(Ast, {
  retainLines: false,
  compact: "auto",
  concise: false,
  quotes: "double",
},code)

生成的是一个对象 { code, map,... }

开始编写插件:

1,初始化

创建文件夹babel-plugin-require-to-default-from,进入执行npm init

插件包命名为@babel/plugin-require-to-default-from

2,环境搭建,安装包并引入

const babylon = require('@babel/parser')
const traverse = require("@babel/traverse").default
const generate = require("@babel/generator").default
const t = require('@babel/types')

3,创建AST

const code = `
  const test = require('test')
`

const Ast = babylon.parse(code,{
  sourceType:"module",
  //plugins:["exportDefaultFrom"]//这里是要用到的插件,文中插件未用到
})

4,遍历并操作AST

traverse(Ast,{
    enter(path){
      if(path.node.type === 'CallExpression' && path.node.callee.name == "require"){
        //判断require是否本身包含default
        if(!(path.parentPath.node.type === 'MemberExpression' && path.parentPath.node.property.name === 'default')){
          // t.memberExpression(object, property, computed, optional)
          const node_new = t.memberExpression(
            t.callExpression(
              t.identifier('request'),
              [t.identifier(`'${path.node.arguments[0].value}'`)]
            ),
            t.identifier('default')
          )
          path.replaceWith(node_new)
        }
      }
    }
})

这里使用的是@babel遍历ast。

在该环节踩了很多坑,主要原因是ast节点类型有很多,并没有文档详细的介绍什么节点是什么类型,最后采用代码调试解决。

调试如图:

同样的方法可以知道require是CallExpression类型

在调试过程中,还可以看到类型的属性。

上面的代码兼容了require().default自身带有default的功能

5,生成代码

const result = generate(Ast, {
  retainLines: false,
  compact: "auto",
  concise: false,
  quotes: "double",
},code)
console.log(result)

generate的api参见官方文档

6,按照插件api封装

export default function({types:t}){
  return {
    visitor:{
       enter(path){
           ...//放入上面的代码
       }
    }
  }
}

总结:

到此一个babel插件的雏形就编写完成了,babel插件编写用到的知识比较琐碎的,需要对各个api详细掌握才能有所用,有所思。

在编写该插件的过程中,遇到了很多坑,到一个一个解决,收获了很多。在接触一样新知识,还是要仔细研究官方文档,再加上扎实的基础,就能有所收获。

参考:https://www.jianshu.com/p/9aaa99762a52

https://babeljs.io/

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值