6

Babel是一个多功能的javascript编译器,你能使用babel创建多种有用的工具。比如:typescript,各种跨平台编译。如果你也想要造一个自己的轮子那是时候了解一下babel了。
babel是javascript编译器,也叫"转换编译器",意思就是你编写javascript代码,babel更改这些代码输出新的代码。当然babel提供插件机制,通常我们要做自己的代码处理,那么你需要的就是自己写一个babel插件。本文重点介绍如何编写一个babel插件。
编写插件之前首先要了解什么是抽象语法树,也叫AST。(想深入了解的也可以买本龙书看看,“编译原理”)。我们对代码做更改其实就是在操作抽象语法树。一个基本流程,输入源代码-->babel编译源代码为抽象语法树-->在babel插件中修改ast语法树-->输出编译更改之后的代码

介绍AST

先看看AST语法树是什么样子,再来叙述如何操作它.一段简单的代码看看,它的语法树是什么样子.
如:

js: `let a = 2`
ast: {
     "type": "VariableDeclaration",
    "start": 0,
    "end": 9,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 1,
        "column": 9
      }
    },
    "declarations": [
      {
        "type": "VariableDeclarator",
        "start": 4,
        "end": 9,
        "loc": {
          "start": {
            "line": 1,
            "column": 4
          },
          "end": {
            "line": 1,
            "column": 9
          }
        },
        "id": {
          "type": "Identifier",
          "start": 4,
          "end": 5,
          "loc": {
            "start": {
              "line": 1,
              "column": 4
            },
            "end": {
              "line": 1,
              "column": 5
            },
            "identifierName": "a"
          },
          "name": "a"
        },
        "init": {
          "type": "NumericLiteral",
          "start": 8,
          "end": 9,
          "loc": {
            "start": {
              "line": 1,
              "column": 8
            },
            "end": {
              "line": 1,
              "column": 9
            }
          },
          "extra": {
            "rawValue": 2,
            "raw": "2"
          },
          "value": 2
        }
      }
    ],
    "kind": "let"
}

AST没有完全拷贝只是拷贝了变量声明部分,如果想去看完整的抽象语法树可以点击这里astexplorer。可以看到语法树上面有完整的代码信息,包括类型,代码位置,变量声明方式等等。聪明的你应该已经意思到只要你修改了ast对象的属性,再用babel将ast转化为代码这就是你要的结果,没错基本思想就是这个样子。babel也给我们提供了很多工具来处理语法树,不会让你一行一行的编写一个语法树。下面我们就来挨着介绍。按照babel的处理步骤:解析,转换,生成。解析:babel转换代码为ast语法树,转换:根据插件对ast做处理(你可能比较关心这部分,因为编写插件这就是你要处理的地方),生成:将ast转换为js代码。

解析 babylon(单个介绍还没有进入插件正题)

解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)。(如果真想了解更加深入自行查阅编译原理相关书籍,我们直接进入使用阶段) 介绍babylon,也就是我们的babel解析器,也就是将javascript代码解析为AST语法树,使用它时你需要先安装它
$ npm install --save babylon
使用:
`import * as babylon from "babylon";

const code = `function square(n) {
return n * n;
}`;
/** 这就是转换之后的语法树
const ast = babylon.parse(code)
sourceType 可以是 "module" 或者 "script",它表示 Babylon 应该用哪种模式来解析。 "module" 将会在严格模式下解析并且允许模块定义,"script" 则不会。
注意: sourceType 的默认值是 "script" 并且在发现 import 或 export 时产生错误。 使用 scourceType: "module" 来避免这些错误。
由于 Babylon 使用了基于插件的架构,因此有一个 plugins 选项可以开关内置的插件。 注意 Babylon 尚未对外部插件开放此 API 接口,不排除未来会开放此API。(摘抄的文档下面会给文档出处),也就是这个地方自定义插件就不行了
*/
babylon.parse(code, {
sourceType: "module", // default: "script"
plugins: ["jsx"] // default: []
});
`

遍历babel-traverse(单个介绍还没有进入插件正题)

AST既然是一颗树,那我们需要对AST做操作的时候就需要我们遍历这颗树了,查找对应节点然后做操作。babel-traverse就是来干这个事情的,名字就是它的功能。
如果你要单独使用它依然得先安装:
$ npm install --save babel-traverse
既然是遍历语法树那肯定要先生成语法树了,所以配合babylon使用:

import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `function square(n) {
  return n * n;
};

const ast = babylon.parse(code);

// 仅仅是个例子后面正式编写一个插件给出其中的遍历方式
traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
       /** 这个地方就是吧上面函数里面变量名为n的改成了变量名为x
            输出之后是这样的了:
            function square(x) {
              return x * x;
            }
         */
      path.node.name = "x";
    }
  }
});

生成器 babel-generator(单个介绍还没有进入插件正题)

更改了语法树接下来就是将更改的语法树输入babel的生成器中生成更改之后的js代码
单独使用依然要安装:
$ npm install --save babel-generator
使用如下:

import * as babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";

const code = function square(n) {
  return n * n;
};

const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
       /** 这个地方就是吧上面函数里面变量名为n的改成了变量名为x
            输出之后是这样的了:
            function square(x) {
              return x * x;
            }
         */
      path.node.name = "x";
    }
  }
});

// 输出的代码就是你更改的
const { code } = generate(ast, {}, code);

在正式开始编写一个插件之前我们先来介绍一些编写插件需要用到的工具。比如修改节点名称容易,如果你要增加一个节点呢比如你要增加一行如下的代码 var a =+ 1;难道要自己编写一个ast node然后插入语法树的某个地方吗。babel提供了工具处理这些事情

babel-types

这是一个babel的工具库其中包含,构造、验证AST节点的方法,比如你要声明一个变量a,声明方式为var。或者说你要判定这个节点是不是一个变量定义类型节点,babel-types是为了帮你干这些事情的。
单独使用它:

$ npm install --save babel-types

使用方式:

`
import traverse from "babel-traverse";
import * as t from "babel-types";

traverse(ast, {
  enter(path) {
    // (path是什么编写插件的时候再介绍)判定节点是否是一个定义类型节点,并且变量定义名称为n 
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});
`

babel-template

虽然我们有了babel-types帮我们生成AST节点,但是babel-types处理的都是单个节点比如你要生成一个对象var a = { b: 1 },使用babel-types你需要定义一个变量类型节点a,需要生成一个空对象节点,还需要额外生成对象的属性节点,好麻烦。babel-template解释为了解决这个麻烦事情,对于这种比较大型的ast节点使用babel-template就比较好了。
先安装:
$ npm install --save babel-template
使用:
`
import template from "babel-template";
import generate from "babel-generator";
import * as t from "babel-types";

const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`);

const ast = buildRequire({
IMPORT_NAME: t.identifier("myModule"),
SOURCE: t.stringLiteral("my-module")
});

console.log(generate(ast).code);
// 这就是编译出来的代码
var myModule = require("my-module");
`
可以看见template函数里面是传递的字符串,也可以穿大写字符占位为一个变量如果用template 输出一个var a = { b:1 }就很好办了。这样子干:
`
import template from "babel-template";
import generate from "babel-generator";
import * as t from "babel-types";
// 直接输入字符串代码模板,不需要babel-types去那么多定义
const buildRequire = template(`
var a = { b: 1 };
`);

const ast = buildRequire();

console.log(generate(ast).code);
// 这就是编译出来的代码
var a = { b: 1 }
`
所以看出template的作用了吧,看名字见意思,对于生成比较大型且有模板的AST语法树tempalte就很有用了。

不开玩笑正式编写一个插件

简单的都介绍了下面正式编写一个插件:
单独创建一个js文件然后暴露一个函数接口就是你的插件入口:

// 这个types就是babel-types,在注册babel插件时解构就有它
    export default function({ types: t }) {
      // 插件内容
    }

接下来在函数返回一个带有visitor属性的对象,这是你插件的主要访问者,你对AST的处理也将放在里面,visitor对象存放函数,key值是你要遍历的节点类型, 函数接收两个参数path, state。下面我们重点介绍一下path, 因为处理ast path比较重要,state参数在你注册使用插件的时候可以传递参数,那么state.opts就可以获取传递的参数;

export default function({ types: t }) {
      // 插件内容
      return {
        visitor: {
            // 接收两个参数path, state
            Identifier(path, state) {
                // 删除了节点
                path.remove();
            }
        }
      }
   }

path

path存储着AST节点信息以及一些节点操作方法,因为AST是一颗树,那么树的查找,和树节点之间的关系就需要工具去处理,就像DOM树一样:

path存储的ast属性有:

  • node节点(当前遍历到的node节点)
  • parent 节点(父级AST节点)
  • parentPath 父级path
  • scope 作用域

    path存储的方法:

  • get获取子节点
  • findParent向父节点搜寻节点
  • getSibling 获取兄弟路径
  • getFunctionParent 获取包含该节点最近的父函数节点(查找的是function)
  • getStatementParent 向上获取最近的statement类型节点
  • relaceWith:用AST节点替换该节点
  • relaceWithMultiple 用多个AST节点替换节点
  • replaceWidthSourceString 用源码解析后的AST节点替换节点
  • insertBefore在之前插入兄弟节点
  • insertAfter 在之后插入兄弟节点
  • remove 删除节点
  • pushContainer 将ASTpush到节点属性里面
  • stop 停止遍历
  • skip 跳过此次遍历
    想了解具体的货 点这里深入了解babel
    现在我们写一个简单的编译目的是将let a = 5; 编译为 var a = 5;
import template from "babel-template";

    const temp = template("var b = 1")

    export default function({ types: t }) {
          // 插件内容
          return {
            visitor: {
                // 接收两个参数path, state
                VariableDeclaration(path, state) {
                    // 找到AST节点
                    const node = path.node;
                    // 保险一点还可以判断一下节点类型,强行用一下babel-types做个示例
                    // 判断节点类型 是否是变量节点, 申明方式是let
                     if (t.isVariableDeclaration(node, { kind: "let" })) {          
                        // 将let 声明编译为var
                        node.kind = "var";
                        // var b = 1 的AST节点
                        const insertNode = temp();
                        // 插入一行代码var b = 1
                        path.insertBefore(insertNode);
                     }
                }
            }
          }
       }

接下来使用编写的插件:

 // 导入自己写的插件
    const myPlugin = require('xxxx')
    const babel = require('@babel/core');
    const content = 'let a = 5';
    // 通过你编写的插件输出的代码
    const { code } = babel.transform(content, {
       plugins: [
           myPlugin
       ]
    });

一个简单的插件编写就完成了,编写插件需要注意几点,如果你需要遍历AST树,能够自己遍历 尽量不要使用path遍历,因为插件遍历遍历到底部会返回,可能会造成一个节点多次访问。最近自己在编写跨平台应用所以写一点使用心得,如果有不对的地方欢迎指正,如果想要了解多一点的看看这里深入了解babel 编写插件很好的辅助工具AST explorer 文档查阅看看babel文档


H_H_code
51 声望3 粉丝