@babel/core入门

1,797 阅读5分钟

如果我们需要在js中对js代码进行修改的时候,@babel/core是个全能的选项。

@babel/core中,已经集成了

  • ast树构建(parse)
  • ast树遍历和改造(traverse)
  • ast节点类型和创建(types)
  • ast输出代码(transfromFromAst)

等等几个常用功能,并以插件模式来实现业务逻辑解耦。

除此之外,@babel/generator是个单独的ast代码生成工具。它与transfromFromAst不同的是:

generatortransfrom
可以基于任何类型ast节点输出代码只能基于完整ast
代码压缩、引号选择等简单配置完整代码配置,支持插件

除了@babel/core,还有个轻量级的选项是acorn,而且babel的AST解析部分也是用的acorn

体积更小的acorn,压缩后在80kb左右,可以在web中使用;但功能也相对单一,仅能实现ast解析。对ast树的遍历需要单独实现,修改则是直接操作ast树。

理解AST语法树

首先来看一下ast树的样子:

const { parse, traverse, types, transformFromAstSync } = require('@babel/core')

const ast = parse('const a = 1')
console.log(JSON.stringify(ast, null, '    '))

为了方便观看,我删掉了代码行列数、起止位置等信息。

{
    "type": "File",
    "errors": [],
    "program": {
        "type": "Program",
        "sourceType": "module",
        "interpreter": null,
        "body": [
            {
                "type": "VariableDeclaration",
                "declarations": [
                    {
                        "type": "VariableDeclarator",
                        "id": {
                            "type": "Identifier",
                                "identifierName": "a"
                            },
                            "name": "a"
                        },
                        "init": {
                            "type": "NumericLiteral",
                            "extra": {
                                "rawValue": 1,
                                "raw": "1"
                            },
                            "value": 1
                        }
                    }
                ],
                "kind": "const"
            }
        ],
        "directives": []
    },
    "comments": []
}

所有节点都具备一个type属性,不同的type有不同的节点结构。

其中,File -> Program,是完整ast文档的固有信息,我们可以先暂时不管;从Program.body开始,是代码正文,下面用伪代码描述一下:

  • VariableDeclaration 开始声明变量,类型(kind)为const,包含以下几个变量
    • VariableDeclarator
      • id 变量名为a(id.name)
      • init 有初始值(init不是null),值类型为number(NumericLiteral)
        • NumericLiteral
          • value 值为 1
    • VariableDeclarator
    • VariableDeclarator
    • (本次声明的其他变量 ... )

从AST转为代码

因为拿到的是完整的ast文档,所以我们可以直接用transfrom来进行代码生成:

const ast = parse('const a = 1')
const { code } = transformFromAstSync(ast)
console.log(code)

输出

% node babel.js
const a = 1;

给加了个分号。

AST遍历

当我们需要收集代码信息,比如分析依赖做tree-shaking,或者要更改代码,比如const/let改为var,都要遍历AST,查找和操作节点。

const ast = parse('const a = 1')
traverse(ast, {
    enter(nodePath) {
        console.log(nodePath.type)
    }
})

输出

% node babel.js
Program
VariableDeclaration
VariableDeclarator
Identifier
NumericLiteral

节点类型方法

代码中,traverse的第二个参数,是各种节点操作方法的map:按照节点类型命名一个方法,对应类型的节点就会进入这个方法。

比较特殊的是enter方法,所有节点都会走进这个方法,然后再执行自身类型对应的方法。

traverse(ast, {
    enter(nodePath) {
        console.log(1, nodePath.type)
    },
    // 节点类型命名方法
    Identifier(nodePath) {
        console.log(2, nodePath.type)
    }
})

输出

% node babel.js
1 Program
1 VariableDeclaration
1 VariableDeclarator
1 Identifier
# 节点类型方法在enter方法之后执行
2 Identifier
1 NumericLiteral

NodePath

NodePath是用来描述节点关系的结构,通过traverse中的节点方法暴露出来。

常用的属性/方法包括:

  • NodePath.node 节点本身
  • NodePath.parentPath 父级节点
  • NodePath.scope 语法上下文,能得到全局变量等信息
  • NodePath.traverse() 自遍历
  • NodePath.replaceWith() 节点替换
  • NodePath.remove() 节点删除

AST修改

基于traverse和暴露出的NodePath,我们可以对AST进行修改操作。

直接修改Node

Node节点作为NodePath中的一个引用值属性,直接操作Node就会影响最终的代码生成。比如我们把变量a更名为A

const ast = parse('const a = 1')
traverse(ast, {
    Identifier(nodePath) {
        const { node } = nodePath
        node.name = 'A'
    }
})

运行

% node babel.js
const A = 1;

甚至删掉节点

traverse(ast, {
    Identifier(nodePath) {
        const { node } = nodePath
        node.name = 'A'
    },
    VariableDeclarator({ node }) {
        delete node.init // node.init = null 也可以
    },
    VariableDeclaration({ node }) {
        node.kind = 'var'
    }
})
// 输出 'var A;'

根据实际经验,直接操作节点,尤其是复杂节点的时候,会对遍历过程造成影响,引发一些不可预期的问题,所以建议在操作复杂节点时

使用NodePath修改

我们使用NodePath提供的方法,实现上面的效果:

traverse(ast, {
    Identifier(nodePath) {
        const { node } = nodePath
        if(node.name === 'a') {
            const newId = types.identifier('A')
            nodePath.replaceWith(newId)
        }
    },
    VariableDeclaration(nodePath) {
        const { node } = nodePath
        const { declarations, kind } = node
        if(kind === 'const') {
            const newNode = types.variableDeclaration('var', declarations)
            nodePath.replaceWith(newNode)
        }
    },
    NumericLiteral(nodePath) {
        if(nodePath.parentPath && nodePath.parentPath.type === 'VariableDeclarator') {
            nodePath.remove()
        }
    }
})

看起来更严谨、更优雅。

使用types创建AST节点

types后面.,第一个字母小写,对应节点类型的创建方法;第一个字母大写,对应相应节点类型定义。@babel/core对类型、属性提示做得非常友好,会自动补全。

// types.Identifier
types.identifier('A')
// types.VariableDeclaration
types.variableDeclaration('var', declarations)

必要的条件判断

注意这里每个节点方法中,都增加了修改条件判断。

因为每次新增、替换的节点,也都会进入traverse过程,不加条件判断会陷入死循环。

Maximum call stack size exceeded

插件

写一个Babel插件

插件是一个函数,这个函数需要返回一个结构:

function plugin() {
    return {
        visitor: {
            // ...
        }
    }
}

这个visitor,就是traverse的第二个参数。

const plugin = () => {
    return {
        visitor: {
            Identifier(nodePath) {
                const { node } = nodePath
                if(node.name === 'a') {
                    const newId = types.identifier('A')
                    nodePath.replaceWith(newId)
                }
            },
            VariableDeclaration(nodePath) {
                const { node } = nodePath
                const { declarations, kind } = node
                if(kind === 'const') {
                    const newNode = types.variableDeclaration('var', declarations)
                    nodePath.replaceWith(newNode)
                }
            },
            NumericLiteral(nodePath) {
                if(
                    nodePath.parentPath
                    && nodePath.parentPath.type === 'VariableDeclarator'
                ) {
                    nodePath.remove()
                }
            }
        }
    }
}

插件调用

const ast = parse('const a = 1')
const { code } = transformFromAstSync(
    ast,
    null,
    { plugins: [ plugin ] }  // 这里
)
console.log(code)

运行

% node babel.js
var A;

Babel插件技能get。

官方插件

就常用的代码修改,官方已发布很多插件,除了特定的场景,一般对脚手架之类应用,基本够用:

babeljs.io/docs/en/plu…

实践

@babel/plugin-transform-async-to-generator

安装

# https://babeljs.io/docs/en/babel-plugin-transform-async-to-generator
npm i -D @babel/plugin-transform-async-to-generator

代码

const ast = parse('const fn = async () => {  }')
const { code } = transformFromAstSync(
    ast,
    null,
    { plugins: ['@babel/plugin-transform-async-to-generator'] }  // 这里,字符串调用。
)
console.log(code)

输出

% node babel.js
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

const fn = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator(function* () {});

  return function fn() {
    return _ref.apply(this, arguments);
  };
}();

以上