DIY 一个 Babel 插件

Babel,是一个 JavaScript 的编译工具,它能够将 es6+语法的代码,转换为浏览器兼容的低版本的代码。它简直就是一个神兵利器,前端工程师拥有了它,就能够在项目中使用一些较新的 es 语法。笔者决定弄懂它,并实现一个本身的 Babel 插件。javascript

Babel 的工做原理,能够用以下公式表述。它实际上就是接受输入的源代码,而后对它作一些处理和转换,最后输出为目标版本的代码。前端

const babel = sourceCode => distCode
复制代码

在将输入的源码作处理或者转换时,这就须要用到了它的插件系统。一个插件只负责处理一件事,好比@babel/plugin-transform-arrow-functions ,就是负责将箭头函数转换为普通函数的插件。Babel 提供了很是多的插件,这样就足以保证能够将新的 es 语法转换为旧版本的代码形式。想了解详细的 Babel 插件系统,能够查看Babel#Pluginsjava

若是不配置任何的插件,Babel 将不会对源码作任何的处理,它只会照原样输出。下面举个例子,node

const babel = require("@babel/core")

const code = ` const a = () => { console.log(1); } `

// 没有配置任何plugin,那么转换以后的code将没有任何变化
babel.transform(code, undefined, (err, result) => {
  if (err) {
    throw err
  }
  console.log(result.code)
})
复制代码

咱们将箭头函数使用 Babel 来转换,可是没有配置任何的插件,最后转换以后的结果将和输入的代码一摸同样。git

➜  babel node scripts/index.ts
const a = () => {
  console.log(1);
};
复制代码

若是配置了@babel/plugin-transform-arrow-functions,Babel 就能正常将咱们的箭头函数转换为普通函数的形式了。以下,es6

// 配置@babel/plugin-transform-arrow-functions
babel.transform(
  code,
  { plugins: ["@babel/plugin-transform-arrow-functions"] },
  (err, result) => {
    if (err) {
      throw err
    }
    console.log(result.code)
  }
)
复制代码

转换以后的代码以下,github

➜  babel node scripts/index.ts
const a = function () {
  console.log(1);
};
复制代码

对于其余的语法形式的转换,能够添加其余的插件。若是仅仅这样,对于一个实际项目代码的转换,将要配置很是多的插件。为了简化这种形式,Babel 又提供了 Presets,简单的说,就是将不少个插件集合从新命名为一个新名称。这样,只须要配置了这个 Presets,那么就相对于配置它所包含的全部的插件。Babel 定义了经常使用的 Presets,详细能够查看Babel#Presetsexpress

经过加入插件处理的方式,Babel 将会有很是好的可扩展性和可插拔性,好比 esNext 中又添加了一个新的语法糖,那么 Babel 只须要单独提供这个新语法处理的插件,并将它配置进去就能够了。对于前端工程师们,也能够根据实际业务需求,写本身的插件,将输入的源码处理成本身想要的输出。api

为了能写出本身的 Babel 插件,咱们就须要知道 Babel 将输入的代码转换成什么样子,插件接受的参数又是什么样子,最后须要返回的值是什么样子。数组

AST

Babel 会将输入的源代码先转换成 AST(Abstract Syntax Tree),而后将 AST 做为参数传给插件,插件将在 AST 上作处理,能够添加,删除或者改变节点。例如,咱们上面例子中箭头函数 a,生成的 AST 大体结构以下,

"program": {
    "type": "Program",
    "body": [
      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {
              "type": "Identifier",
              "name": "a"
            },
            "init": {
              "type": "ArrowFunctionExpression",
              "params": [],
              "body": {
                "type": "BlockStatement",
                "body": [
                  { ↔ }
                ],
              }
            }
          }
        ],
        "kind": "const"
      }
    ],
  },
复制代码

AST 能够当作一棵树,它包含了不少的节点,每一个节点都会包含一个 type 字段,这个 type 字段就是用来代表当前节点的类型,好比上面的Identifier代表是标识符,ArrowFunctionExpression代表是箭头函数表达式。想详细了解 AST 结构,能够查看astexplorer

要处理 AST 树,就得遍历这颗树,找到咱们要处理的节点位置。对于一棵树的遍历,有 DFS(深度优先搜索)和 BFS(广度优先搜索)两种方式。对于 AST 的遍历,使用的是 DFS 方式。Babel 提供了@babel/traverse来遍历它,能够很方便的找到须要处理的节点位置。例如上面的例子,咱们能够像下面这样找到console.log(1)1这个节点位置,

const babel = require("@babel/core")
const traverse = require("@babel/traverse")

const code = ` const a = () => { console.log(1); } `

babel.parse(code, null, (err, ast) => {
  if (err) {
    throw err
  }
  traverse(ast, {
    NumericLiteral(path) {
      console.log(JSON.stringify(path.node, null, 4))
    },
  })
})
复制代码

因为console.log(1)接受的参数是一个数字字面量,因此它对应的type就是NumericLiteral。最后找到这个节点的信息以下,

➜  babel node scripts/index.ts
{
    "type": "NumericLiteral",
    "start": 37,
    "end": 38,
    "loc": {
        "start": {
            "line": 3,
            "column": 16
        },
        "end": {
            "line": 3,
            "column": 17
        }
    },
    "extra": {
        "rawValue": 1,
        "raw": "1"
    },
    "value": 1
}
复制代码

能够看到,节点包含了 type,loc 信息,以及 value 等信息。更多关于 traverse 的使用,能够查看这里Babel#traverse

Plugin

根据上面的思路,咱们能够得出以下结论,

Babel 中插件接受 AST 做为参数,而后能够在 AST 上作一些自定义的处理,最后返回处理以后的 AST。

为了验证这个结论正确性,咱们来看看官方的@babel/plugin-transform-arrow-functions的源码,源码只有 28 行代码,我贴出来,并作一些本身的注释,一块儿看看。

import { declare } from "@babel/helper-plugin-utils";
import type NodePath from "@babel/traverse";

export default function declare((api, options) {
  // 判断当前Babel版本是不是v7.x
  api.assertVersion(7);

  // 接受咱们传入的参数
  const { spec } = options;

  // 返回一个对象
  return {
    name: "transform-arrow-functions",

    visitor: {
      ArrowFunctionExpression(
        path: NodePath<BabelNodeArrowFunctionExpression>,
      ) {
        // 先判断是否是箭头函数表达式,不是就直接返回
        if (!path.isArrowFunctionExpression()) return;

        // 将箭头函数转为函数表达式
        path.arrowFunctionToExpression({
          allowInsertArrow: false,
          specCompliant: !!spec,
        });
      },
    },
  };
});
复制代码

从源码能够看出,它返回一个declare函数。这个函数接受两个参数,一个api,一个是options。函数处理步骤以下,

  1. 判断是否 Babel v7 的版本
  2. 返回一个对象,包括namevisitor;其中,visitor又是一个对象,它才真正包含对肩头函数表达式的处理。

实际上,path.arrowFunctionToExpression 就是使用@babel/typesarrowfunctionexpression,详细能够查看babel-types#arrowfunctionexpression

跟咱们猜测的 Babel 插件样子有点出入,可是它包含了咱们猜测的内容。最后,咱们能够总结出写一个 Babel 插件的样子应该是这样的,

export default function declare(api, options) {
  // api能够作一些版本兼容性判断,或者缓存相关的。
  // options就是咱们配置插件时,传入的参数,这里插件内部就能够使用了

  return {
    name: "my-custorm-plugin",
    visitor: {
      // 遍历AST作处理
    },
  }
}
复制代码

DIY

清楚了 Babel 插件的模版形式,就能够按照这个模版写咱们自定义的功能插件。假设,咱们要写的一个 Babel 插件,就是去掉全部的console.log相关调试信息的代码。

// 源代码
const a = () => {
  console.log(1)
}
复制代码

例如上面的代码通过咱们的 Babel 插件处理以后,输出的代码应该是一个空的箭头函数 a,

// 转换以后
const a = () => {}
复制代码

根据 Babel 插件模版代码,咱们能够这样实现以下,

// plugins/remove-console-log.js
const types = require("@babel/types")

module.exports = function declare(api, options) {
  api.assertVersion(7)

  return {
    name: "remove-console-log",
    visitor: {
      ExpressionStatement(path) {
        const expression = path.node.expression
        if (types.isCallExpression(expression)) {
          const callee = expression.callee
          if (types.isMemberExpression(callee)) {
            const objName = callee.object.name
            const methodName = callee.property.name
            if (objName === "console" && methodName === "log") {
              path.remove()
            }
          }
        }
      },
    },
  }
}
复制代码

而后在 babel.config.js 中配置以下,

module.exports = {
  plugins: ["./plugins/remove-console-log.js"],
}
复制代码

最后,咱们经过 Babel 转换以后就能够获得咱们指望的结果了。

小结

经过本身实现一个 Babel 插件,而后贯穿整个过程把 Babel 原理弄清楚。上面其实还有一个小知识点,就是 Babel 怎么将源码转换成 AST 的。其实,它的过程也不难理解,只是在转换为 AST 以前,须要先进行词法分析,把源码字符串转换成 Token 数组;而后根据词法分析获得的结果,转换成 AST。完整的 Babel 原理过程能够简单的表述为以下,

let tokens = tokenizer(input) // 词法分析
let ast = parser(tokens) // 转换为AST
let newAst = transformer(ast) // 调用插件,进行转换
let output = codeGenerator(newAst) // 最后,生成新的目标代码
复制代码

若是想更加详细研究 Babel 的过程,能够看看这个简易的编译器the-super-tiny-compiler,它实现了完整的流程过程,代码也很是简单易懂。

参考

相关文章
相关标签/搜索