babel

739 阅读11分钟

前言

babel 怎么使用其实很简单,但是牵扯的概念,比如 AST, compiler 却很吸引人去深究。本文打算以分析现有 plugin 和制作自己的 plugin 作为切入点去理解这些概念。

什么是 babel

Babel is a JavaScript compiler
Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments.

就是个编译器,compiler。

那什么是 compiler 呢?wikipedia 给出的定义:

In computing, a compiler is a computer program that translates computer code written in one programming language (the source language) into another language (the target language). The name "compiler" is primarily used for programs that translate source code from a high-level programming language to a lower level language (e.g., assembly language, object code, or machine code) to create an executable program.

There are many different types of compilers...A program that translates between high-level languages is usually called a source-to-source compiler or transcompiler...

babel 就是个 compiler,可以把浏览器不支持的语法转化成浏览器可以识别和执行的语法。它帮你把脏活累活都干了,你只要写优雅的、最新的代码就好了。

简而言之:source code => output code 

babel 的用法

不详述,看官网,就是配置一下,要什么新语法就配什么内容。preset 和 plugin 搭配使用,preset 就是 plugin 的组合。配置的时候注意下 preset 和 plugin 的配置顺序就行了。

概念

Plugin

什么是 plugin 呢?就是把 souce code 转成 output code 过程中处理转化的那部分代码。

Babel is a compiler (source code => output code). Like many other compilers it runs in 3 stages: parsing, transforming, and printing.
Now, out of the box Babel doesn't do anything. It basically acts like const babel = code => code; by parsing the code and then generating the same code back out again. You will need to add plugins for Babel to do anything.

plugin 就是转换工作的核心内容。

These plugins apply transformations to your code.

多说一句 @babel/plugin-syntax-xxx@babel/plugin-proposal-xxx 的区别。

syntax plugin是用来让 parser 可以认识语法的,而真正转化的工作是 plugin 做的,但是如果引入 plugin-proposal 的话会自动激活对应的 plugin-syntax。见 syntax-plugins

These plugins only allow Babel to parse specific types of syntax (not transform).

Transform plugins will enable the corresponding syntax plugin so you don't have to specify both.

以 optional-chaining 语法为例,通过 npm install --save-dev @babel/plugin-proposal-optional-chaining 的方式安装 @babel/plugin-proposal-optional-chaining 你会发现 node_modules 里面还会出现一个叫 @babel/plugin-syntax-optional-chaining 的包。

plugin 可以分为很多类型,transform plugin 的分类可以在 babeljs.io/docs/en/plu… 看到,有 ES3 / ES5 / ES2015...

plugin 和 preset 的顺序配置说明等都可以在上面的链接中找到。

那么如何写 plugin 呢?文章后面的内容会给出答案

@babel/parser, @babel/core, @babel/traverse....

非重点,简述作用,看官网,给链接。

@babel/types

文档,里面写了如何定义、构建和校验 type。所有的 type 都罗列在了官网上,babeljs.io/docs/en/bab…

通常的写法 OptionalCallExpression

特殊的写法"OptionalCallExpression|OptionalMemberExpression"

生成一个 type,t.conditionalExpression(test, consequent, alternate)

types 的其他用法 t.cloneNode(), t.isMemberExpression(node)

如何写 plugin 呢?

官方给出了手册,plugin-handbook,内容非常详细。而且介绍了很多 compiler 和 AST 相关的概念和内容。同时还通过一些简单的例子介绍了常见的 plugin 编写中使用到的 api 和概念。实际上这个手册的内容可以把很多相关的概念串联起来。

Babel is a JavaScript compiler, specifically a source-to-source compiler, often called a "transpiler".

又提到了 transpiler,其实就是 compiler 的一种类型。

正式写 plugin 之前先介绍一些概念。

AST

abstract syntax tree,还是先看一下 wikipedia 的定义:

In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code.

DOM tree 其实就是一种类型 AST,那 js 也可以表示成 tree 的形式,也是一种 AST。具体如何被解析,本文不做详细的解释和探讨,这个内容太大,可以另开一篇文章。我们先看下 js 被解析成 AST 是什么样子的。通过 ast-explorer 这个在线工具可以看 js 被解析成 AST 的形式,很直观。

可以观察到,每个节点都有相似的结构,都是用一个 object 对这个节点进行描述,并且都有一个 type 属性,类似这样:

interface Node {
  type: string;
}

The type field is a string representing the type of Node the object is (e.g. "FunctionDeclaration", "Identifier", or "BinaryExpression"). Each type of Node defines an additional set of properties that describe that particular node type.

这里的 type 就对应了 @babel/types 中的 type,这个 type 在写 plugin 的时候很重要,不论是判断节点类型,还是生成新的节点,都会用到 type 概念。我们后面会详述判断节点类型和生成节点时对 type 的应用。

还有一点就是有些 type 可以组合或者说合并成一个更抽象的 type。比如

You can also use aliases as visitor nodes (as defined in babel-types).
For example,

Function is an alias for FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod and ClassMethod.

const MyVisitor = {
  Function(path) {}
};

当然除了 type 属性之外还有这个节点的很多详细信息,比如位置等信息。AST 很有用,除了做代码转化,可以做高亮、书写样式美化等。

关于 babel ast 的标准,参考这篇文档

解析过程

babel 的解析过程和通常的 compiler 是类似的,都是 Parse => Transform => Generate。其中 parse 分为 Lexical Analysis 和 Syntactic Analysis 两个过程。因为这个解析过程是比较复杂的,可以单独作为一个主题来写,所以这里不详述。具体的看手册中官方的链接就好了(简单口述)。

关于这部分的内容可以详细 the super tiny compilertiny-compiler 这两部分的内容。

[占位:还有一些不错的资源想不起来了,想起来了再补充在这里]。

遍历

生成 ast 之后,要想对 source code 进行改造,那么就要遍历这个 tree,然后分别对想要改造的节点做改造,生成新的 tree,最后再用新的 tree 生成新的代码。

遍历各个节点,就是要挨个访问各个节点,这里引入一个 visitor 的概念。具体来说,visitor 是一个对象,这个对象有一个或者多个属性,属性名是上面提到的 type,属性的值又是一个对象,对象有两个属性 enter 和 exit。enter 和 exit 对应的值是一个函数,这个函数中的内容就是真正进行代码转换时的逻辑内容。

When we talk about "going" to a node, we actually mean we are visiting them. The reason we use that term is because there is this concept of a visitor.
Visitors are a pattern used in AST traversal across languages. Simply put they are an object with methods defined for accepting particular node types in a tree. That's a bit abstract so let's look at an example.

举个例子,一个 visitor 大概是这样一个结构:

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

如果只用到 enter 方法,上面的 visitor 可以简写为:

const Visitor = {
  Identifier() {}
  ...
};

通常而言,大部分的 plugin 都是上面这种写法,其实就是只在 enter 的时候进行逻辑处理。

如果多个 type 的处理逻辑是相同的话,可以进行合并:

If necessary, you can also apply the same function for multiple visitor nodes by separating them with a | in the method name as a string like Identifier|MemberExpression.
Example usage in the flow-comments plugin

const MyVisitor = {
  "ExportNamedDeclaration|Flow"(path) {}
};

还是以手册中的例子为例,来看下整个遍历过程,理解一下 enter 和 exit 都分别在什么时候,以及遍历的方式,深度优先遍历。

  • Enter FunctionDeclaration
    • Enter Identifier (id)
      • Hit dead end
    • Exit Identifier (id)
    • Enter Identifier (params[0])
      • Hit dead end
    • Exit Identifier (params[0])
    • Enter BlockStatement (body)
      • Enter ReturnStatement (body)
        • Enter BinaryExpression (argument)
          • Enter Identifier (left)
            • Hit dead end
          • Exit Identifier (left)
          • Enter Identifier (right)
            • Hit dead end
          • Exit Identifier (right)
        • Exit BinaryExpression (argument)
      • Exit ReturnStatement (body)
    • Exit BlockStatement (body)
  • Exit FunctionDeclaration

path

path 是什么

上面写 visitor 的时候看到每个方法都会传入一个 path 参数,那么 path 是什么呢?

An AST generally has many Nodes, but how do Nodes relate to one another? We could have one giant mutable object that you manipulate and have full access to, or we can simplify this with Paths.
A Path is an object representation of the link between two nodes.

还是看官方的示例。可以看到这个 path 有 node 和 parent 两个属性,和一些很多的其他 metadata,说直白一点就是相关信息。同时这个 path 还挂载了很多方法,可以对 node 进行操作。path 的常用方法和 api 会在后面说。

看官方的说法:

As well as tons and tons of methods related to adding, updating, moving, and removing nodes...

In a sense, paths are a reactive representation of a node's position in the tree and all sorts of information about the node. Whenever you call a method that modifies the tree, this information is updated. Babel manages all of this for you to make working with nodes easy and as stateless as possible.

When you have a visitor that has a Identifier() method, you're actually visiting the path instead of the node. This way you are mostly working with the reactive representation of a node instead of the node itself.

这里面有个关键词 reactive,我们后面分析 optional-chaining plugin 的时候就会对这个 reactive 有更加深刻的理解。就是说 path 更新 node 之后,path 也是实时发生变化的。

你通过 visitor 访问 node 的时候,实际上访问的是这个 node 对应的 path 而不是 node 本身。大部分情况下操作的也是 path 而不是 node 本身,操作 path 就会更新 node。path 相当于是 node 的 reactive representation。

To access an AST node's property you normally access the node and then the property. path.node.property

// the BinaryExpression AST node has properties: `left`, `right`, `operator`
BinaryExpression(path) {
  path.node.left;
  path.node.right;
  path.node.operator;
}

If you need to access the path of that property instead, use the get method of a path, passing in the string to the property.

BinaryExpression(path) {
  path.get('left'); 
}
Program(path) {
  path.get('body.0');
}

这里要看出他们的区别,一个是获取 node ,一个是获取 node 对应的 path。

这个 path 在写 plugin 的时候非常有用,我们后面分析几个现有插件的时候可以看到。还是强调一点,这个 path 和 ast 的 node 不是一个东西,但是是有关联的。path 是一个很大的描述这个节点以及这个节点和其他节点之间关系的对象,并且还拥有很多方法,可以对节点进行操作。

path 的常见 api

常见的 api 有:

path.get('object|callee|alternate') // 获取当前 path 的子节点 path

path.replaceWith() // 替换当前语法树节点

path.isOptionalMemberExpression() // 判断当前节点类型

还有很多,常见的 api 都在文档中有罗列,过一下。

path.findParent(), path.find(), path.getSibling(index), path.replaceWithMultiple([node1, node2...]), path.insertBefore(node) 等,文档中的例子很清楚,因为还没有用到所以就不详述了。

但是有些 api 在文档中没有,在 plugin 内部却用到了,而且也比较常见,例如在 optional-chaining 中就看到了:

path.scope.maybeGenerateMemoised(node) // 根据 node 生成一个变量

这些不常见的可以在 babel-core-apidoc 中找到,我们一会儿分析 optional-chaining 插件的时候再说说这个方法。

在 optional-chaining 中还看到一个方法,这里顺便说下。

scope.buildUndefinedNode() // void 0 和 undefined 之间的区别

state

关于 state 的部分也很有用,但是因为在实际的 plugin 和 项目中还没有关于这部分的使用和操作,所以理解并不是很深刻,感觉主要是用在函数内部的时候,防止修改范围越界。在遇到问题的时候可以来详细看看。现在也还是看看官方给出的示例和内容。

scope

scope 也是一个很重要的概念和部分。

A scope can be represented as:

{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...]
}

分析 optional-chaining 和 nullish-coalescing-operator 插件

直接看源码。这部分内容打算单独形成一篇文章,这里写占个位,后续把链接粘贴过来。

但是其实 optional-chaining 没那么简单,juejin.cn/post/684490… ,背后有很多语言方面的考虑和容错、兼容等问题的考虑。

github.com/tc39/propos… 可以好好看看这个规范里的内容去理解 optional-chaining 背后的思考。

还可以再看下 function-bind 这个组件,babeljs.io/docs/en/nex…

自己编写一个组件

接下来,我们自己编写一个简单的插件。本来需要在本地简单搭建一下环境,但是现在有一个线上的环境可以直接用,对于一些简单插件的开发,使用起来更加方便。在线插件预览工具,不过需要注意的一点是,这个在线的工具,可能用的 babel 版本比较老,比如生成 memberExpression 的时候就无法传入 optional 参数。而且也无法引入 babel 的语法插件等,所以只能用来写一点简单的插件。

// obj['prop'] => obj.prop
module.exports = function(babel) {
    const t = babel.types;
    return {
        visitor: {
            MemberExpression(path){
              const { node } = path;
              if(node.computed) {
              	path.replaceWith(
                  t.memberExpression(
                    node.object,
                    t.identifier(node.property.value),
                    false
                  )
                )
              }
            }
        }
    }
}

a?.b?.c  => a && a.b && a.b.c

module.exports = function(babel) {  return {
    visitor: {
      "OptionalCallExpression"(path) {
        const {
          scope
        } = path;
        const optionals = [];
        let optionalPath = path;

        while (optionalPath.isOptionalMemberExpression()) {
          const {
            node
          } = optionalPath;

          if (node.optional) {
            optionals.push(node);
          }

          if (optionalPath.isOptionalMemberExpression()) {
            optionalPath.node.type = "MemberExpression";
            optionalPath = optionalPath.get("object");
          }
        }

        let replacementPath = path;

        for (let i = 0; i <= optionals.length - 1; i++) {
          const node = optionals[i];
          replacementPath.replaceWith(
            t.logicalExpression(
              "&&",
              node.object,
              node
            )
          );
          replacementPath = replacementPath.get("left");
        }
      }
    }
  }
}

module.exports = optionalChaining;

babel 插件开发环境生成工具

github.com/babel/gener…

测试用例都写好了,直接用就行了。通常的组件都不复杂,有这个就够用了。

获取插件参数

写插件的时候还可以获取给插件传入的参数。简单,看文档

Questions

Q1:.babelrc 、 babel.config.json、.babelrc.json好像都是babel的配置文件,有什么区别,更推荐那个?

官方说法:

For compatibility reasons, .babelrc is an alias for .babelrc.json.

.babelrc 和 .babelrc.json 是一样的话,那就是说只有两种配置文件了, babel.config.json、.babelrc.json。这两种的区别文档里也说得很清楚。其实就是 babel.config.json 算是全局配置,而 .babelrc.json 是局部配置。

Q2:与webpack集成的babel-loader,好像也可以配置babel,这时候与上述单独文件配置的优先级是什么样?

babel-loader 

Q3:如何让nodejs支持import写法,服务端渲染如何识别

通过 babel 命令行的形式?

其他

babel 官网给出的 ES2015 学习资源:babeljs.io/docs/en/lea…

dev.to/mfco/build-…

github.com/oakland/bab… 可以来自己写一个插件并且测试

...