手把手带你走进 Babel 的编译世界

前言

谈及 Babel,必然离不开 AST。有关 AST 这个知识点其实是很重要的,但由于涉及到代码编译阶段,大多情况都是由各个框架内置相关处理,所以作为开发(使用)者本身,往往会忽视这个过程。希望通过这篇文章,带各位同学走进 AST,借助 AST 发挥更多的想象力。

AST 概述

想必大家总是听到 AST 这个概念,那么到底什么是 AST?

AST 全称是是 Abstract Syntax Tree,中文为抽象语法树,将我们所写的代码转换为机器能识别的一种树形结构。其本身是由一堆节点(Node)组成,每个节点都表示源代码中的一种结构。不同结构用类型(Type)来区分,常见的类型有:Identifier(标识符),Expression(表达式),VariableDeclaration(变量定义),FunctionDeclaration(函数定义)等

AST 结构

随着 JavaScript 的发展,为了统一ECMAScript标准的语法表达。社区中衍生出了ESTree Spec,是目前社区所遵循的一种语法表达标准。

ESTree 提供了例如Identifier、Literal等常见的节点类型。

节点类型

公共属性

AST 示例

有的同学可能会问了,这么多类型都需要记住么? 其实并不是,我们可以借助以下两个工具来查询 AST 结构。

AST Explorer (常用)

AST 可视化

结合一个示例,带大家快速了解一下 AST 结构。

function test(args) {

  const a = 1;

  console.log(args);

}

复制代码


上述代码,声明了一个函数,名为test,有一个形参args。

函数体中:

声明了一个const类型变量a,值为 1

执行了一个 console.log 语句

将上述代码粘贴至AST Explorer,结果如图所示:

接下来我们继续分析内部结构,以const a = 1为例:

变量声明在 AST 中对应的就是 type 为VariableDeclaration的节点。该节点包含kind和declarations两个必须属性,分别代表声明的变量类型和变量内容。

细心的同学可能发现了declarations是一个数组。这是为什么呢?因为变量声明本身支持const a=1,b=2的写法,需要支持多个VariableDeclarator,故此处为数组。

而 type 为VariableDeclarator的节点代表的就是a=1这种声明语句,其中包含id和init属性。

id即为Identifier,其中的name值对应的就是变量名称。

init即为初始值,包含type,value属性。分别表示初始值类型和初始值。此处 type 为NumberLiteral,表明初始值类型为 number 类型。

Babel 概述

Babel 是一个 JavaScript 编译器,在实际开发过程中通常借助 Babel 来完成相关 AST 的操作。

Babel 工作流程

Babel AST

Babel 解析代码后生成的 AST 是以ESTree作为基础,并略作修改。

官方原文如下:

The Babel parser generates AST according to Babel AST format. It is based on ESTree spec with the following deviations:

Literal token is replaced with StringLiteral, NumericLiteral, BigIntLiteral, BooleanLiteral, NullLiteral, RegExpLiteral

Property token is replaced with ObjectProperty and ObjectMethod

MethodDefinition is replaced with ClassMethod

Program and BlockStatement contain additional directives field with Directive and DirectiveLiteral

ClassMethod, ObjectProperty, and ObjectMethod value property's properties in FunctionExpression is coerced/brought into the main method node.

ChainExpression is replaced with OptionalMemberExpression and OptionalCallExpression

ImportExpression is replaced with a CallExpression whose callee is an Import node.

Babel 核心包

npm i @babel/parser @babel/traverse @babel/types @babel/generator @babel/template -D

Babel 插件

Babel 插件大致分为两种:语法插件和转换插件。语法插件作用于 @babel/parser,负责将代码解析为抽象语法树(AST)(官方的语法插件以 babel-plugin-syntax 开头);转换插件作用于 @babel/core,负责转换 AST 的形态。绝大多数情况下我们都是在编写转换插件。

Babel 工作依赖插件。插件相当于是指令,来告知 Babel 需要做什么事情。如果没有插件,Babel 将原封不动的输出代码。

Babel 插件本质上就是编写各种 visitor 去访问 AST 上的节点,并进行 traverse。当遇到对应类型的节点,visitor 就会做出相应的处理,从而将原本的代码 transform 成最终的代码。

export default function (babel) {

  // 即@babel/types,用于生成AST节点

  const { types: t } = babel;

  return {

    name: "ast-transform", // not required

    visitor: {

      Identifier(path) {

        path.node.name = path.node.name.split("").reverse().join("");

      },

    },

  };

}

复制代码


这是一段AST Explorer上的 transform 模板代码。上述代码的作用即为将输入代码的所有标识符(Identifier)类型的节点名称颠倒。

其实编写一个 Babel 插件很简单。我们要做的事情就是回传一个 visitor 对象,定义以Node Type为名称的函数。该函数接收path,state两个参数。

其中 path(路径)提供了访问/操作 AST 节点的方法。path 本身表示两个节点之间连接的对象。例如path.node可以访问当前节点,path.parent可以访问父节点等。path.remove()可以移除当前节点。具体 API 见下图。其他可见 handlebook。

Babel Types

Babel Types 模块是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。

类型判断

Babel Types 提供了节点类型判断的方法,每一种类型的节点都有相应的判断方法。更多见babel-types API。

import * as types from "@babel/types";

// 是否为标识符类型节点

if (types.isIdentifier(node)) {

  // ...

}

// 是否为数字字面量节点

if (types.isNumberLiteral(node)) {

  // ...

}

// 是否为表达式语句节点

if (types.isExpressionStatement(node)) {

  // ...

}

复制代码

创建节点

Babel Types 同样提供了各种类型节点的创建方法,详见下属示例。

注: Babel Types 生成的 AST 节点需使用@babel/generator转换后得到相应代码。


import * as types from "@babel/types";

import generator from "@babel/generator";

const log = (node: types.Node) => {

  console.log(generator(node).code);

};

log(types.stringLiteral("Hello World")); // output: Hello World

复制代码


基本数据类型


types.stringLiteral("Hello World"); // string

types.numericLiteral(100); // number

types.booleanLiteral(true); // boolean

types.nullLiteral(); // null

types.identifier(); // undefined

types.regExpLiteral("\\.js?$", "g"); // 正则

复制代码


"Hello World"

100

true

null

undefined

/\.js?$/g

复制代码


复杂数据类型

数组

types.arrayExpression([

  types.stringLiteral("Hello World"),

  types.numericLiteral(100),

  types.booleanLiteral(true),

  types.regExpLiteral("\.js?$", "g"),

]);

复制代码


["Hello World", 100, true, /.js?$/g];

复制代码


对象

types.objectExpression([

  types.objectProperty(

    types.identifier("key"),

    types.stringLiteral("HelloWorld")

  ),

  types.objectProperty(

    // 字符串类型 key

    types.stringLiteral("str"),

    types.arrayExpression([])

  ),

  types.objectProperty(

    types.memberExpression(

      types.identifier("obj"),

      types.identifier("propName")

    ),

    types.booleanLiteral(false),

    // 计算值 key

    true

  ),

]);

复制代码


{

  key: "HelloWorld",

  "str": [],

  [obj.propName]: false

}

复制代码


JSX 节点

创建 JSX AST 节点与创建数据类型节点略有不同,此处整理了一份关系图。

JSXElement


types.jsxElement(

  types.jsxOpeningElement(types.jsxIdentifier("Button"), []),

  types.jsxClosingElement(types.jsxIdentifier("Button")),

  [types.jsxExpressionContainer(types.identifier("props.name"))]

);

复制代码



<Button>{props.name}</Button>

复制代码


JSXFragment


types.jsxFragment(types.jsxOpeningFragment(), types.jsxClosingFragment(), [

  types.jsxElement(

    types.jsxOpeningElement(types.jsxIdentifier("Button"), []),

    types.jsxClosingElement(types.jsxIdentifier("Button")),

    [types.jsxExpressionContainer(types.identifier("props.name"))]

  ),

  types.jsxElement(

    types.jsxOpeningElement(types.jsxIdentifier("Button"), []),

    types.jsxClosingElement(types.jsxIdentifier("Button")),

    [types.jsxExpressionContainer(types.identifier("props.age"))]

  ),

]);

复制代码


<>

  <Button>{props.name}</Button>

  <Button>{props.age}</Button>

</>

复制代码


声明

变量声明 (variableDeclaration)


types.variableDeclaration("const", [

  types.variableDeclarator(types.identifier("a"), types.numericLiteral(1)),

]);

复制代码


const a = 1;

复制代码


函数声明 (functionDeclaration)


types.functionDeclaration(

  types.identifier("test"),

  [types.identifier("params")],

  types.blockStatement([

    types.variableDeclaration("const", [

      types.variableDeclarator(

        types.identifier("a"),

        types.numericLiteral(1)

      ),

    ]),

    types.expressionStatement(

      types.callExpression(types.identifier("console.log"), [

        types.identifier("params"),

      ])

    ),

  ])

);

复制代码


function test(params) {

  const a = 1;

  console.log(params);

}

复制代码


React 函数式组件

综合上述内容,小小实战一下~

我们需要通过 Babel Types 生成button.js代码。乍一看不知从何下手?

// button.js

import React from "react";

import { Button } from "antd";

export default (props) => {

  const handleClick = (ev) => {

    console.log(ev);

  };

  return <Button onClick={handleClick}>{props.name}</Button>;

};

复制代码


小技巧: 先借助AST Explorer网站,观察 AST 树结构。然后通过 Babel Types 逐层编写代码。事半功倍!

types.program([

  types.importDeclaration(

    [types.importDefaultSpecifier(types.identifier("React"))],

    types.stringLiteral("react")

  ),

  types.importDeclaration(

    [

      types.importSpecifier(

        types.identifier("Button"),

        types.identifier("Button")

      ),

    ],

    types.stringLiteral("antd")

  ),

  types.exportDefaultDeclaration(

    types.arrowFunctionExpression(

      [types.identifier("props")],

      types.blockStatement([

        types.variableDeclaration("const", [

          types.variableDeclarator(

            types.identifier("handleClick"),

            types.arrowFunctionExpression(

              [types.identifier("ev")],

              types.blockStatement([

                types.expressionStatement(

                  types.callExpression(types.identifier("console.log"), [

                    types.identifier("ev"),

                  ])

                ),

              ])

            )

          ),

        ]),

        types.returnStatement(

          types.jsxElement(

            types.jsxOpeningElement(types.jsxIdentifier("Button"), [

              types.jsxAttribute(

                types.jsxIdentifier("onClick"),

                types.jSXExpressionContainer(types.identifier("handleClick"))

              ),

            ]),

            types.jsxClosingElement(types.jsxIdentifier("Button")),

            [types.jsxExpressionContainer(types.identifier("props.name"))],

            false

          )

        ),

      ])

    )

  ),

]);

复制代码

应用场景

AST 本身应用非常广泛,例如:Babel 插件(ES6 转化 ES5)、构建时压缩代码 、css 预处理器编译、 webpack 插件等等,可以说是无处不在。


如图所示,不难发现,一旦涉及到编译,或者说代码本身的处理,都和 AST 息息相关。下面列举了一些常见应用,让我们看看是如何处理的。

代码转换

// ES6 => ES5 let 转 var

export default function (babel) {

  const { types: t } = babel;

  return {

    name: "let-to-var",

    visitor: {

      VariableDeclaration(path) {

        if (path.node.kind === "let") {

          path.node.kind = "var";

        }

      },

    },

  };

}

复制代码

babel-plugin-import

在 CommonJS 规范下,当我们需要按需引入antd的时候,通常会借助该插件。

该插件的作用如下:

// 通过es规范,具名引入Button组件

import { Button } from "antd";

ReactDOM.render(<Button>xxxx</Button>);

// babel编译阶段转化为require实现按需引入

var _button = require("antd/lib/button");

ReactDOM.render(<_button>xxxx</_button>);

复制代码

简单分析一下,核心处理: 将 import 语句替换为对应的 require 语句。

export default function (babel) {

  const { types: t } = babel;

  return {

    name: "import-to-require",

    visitor: {

      ImportDeclaration(path) {

        if (path.node.source.value === "antd") {

          // var _button = require("antd/lib/button");

          const _botton = t.variableDeclaration("var", [

            t.variableDeclarator(

              t.identifier("_button"),

              t.callExpression(t.identifier("require"), [

                t.stringLiteral("antd/lib/button"),

              ])

            ),

          ]);

          // 替换当前import语句

          path.replaceWith(_botton);

        }

      },

    },

  };

}

复制代码

TIPS: 目前 antd 包中已包含esm规范文件,可以依赖 webpack 原生 TreeShaking 实现按需引入。

LowCode 可视化编码

当下LowCode,依旧是前端一大热门领域。目前主流的做法大致下述两种。

Schema 驱动

目前主流做法,将表单或者表格的配置,描述为一份 Schema,可视化设计器基于 Schema 驱动,结合拖拽能力,快速搭建。

AST 驱动

通过CloudIDE,CodeSandbox等浏览器端在线编译,编码。外加可视化设计器,最终实现可视化编码。

大致流程如上图所示,既然涉及到代码修改,离不开AST的操作,那么又可以发挥 babel 的能力了。

假设设计器的初始代码如下:

import React from "react";

export default () => {

  return <Container></Container>;

};

复制代码


此时我们拖拽了一个Button至设计器中,根据上图的流程,核心的 AST 修改过程如下:

新增 import 声明语句 import { Button } from "antd";

将<Button></Button>插入至<Container></Container>

话不多说,直接上代码:

import traverse from "@babel/traverse";

import generator from "@babel/generator";

import * as parser from "@babel/parser";

import * as t from "@babel/types";

// 源代码

const code = `

  import React from "react";

  export default () => {

    return <Container></Container>;

  };

`;

const ast = parser.parse(code, {

  sourceType: "module",

  plugins: ["jsx"],

});

traverse(ast, {

  // 1. 程序顶层 新增import语句

  Program(path) {

    path.node.body.unshift(

      t.importDeclaration(

        // importSpecifier表示具名导入,相应的匿名导入为ImportDefaultSpecifier

        // 具名导入对应代码为 import { Button as Button } from 'antd'

        // 如果相同会自动合并为 import { Button } from 'antd'

        [t.importSpecifier(t.identifier("Button"), t.identifier("Button"))],

        t.stringLiteral("antd")

      )

    );

  },

  // 访问JSX节点,插入Button

  JSXElement(path) {

    if (path.node.openingElement.name.name === "Container") {

      path.node.children.push(

        t.jsxElement(

          t.jsxOpeningElement(t.jsxIdentifier("Button"), []),

          t.jsxClosingElement(t.jsxIdentifier("Button")),

          [t.jsxText("按钮")],

          false

        )

      );

    }

  },

});

const newCode = generator(ast).code;

console.log(newCode);

复制代码

结果如下:

import { Button } from "antd";

import React from "react";

export default () => {

  return (

    <Container>

      <Button>按钮</Button>

    </Container>

  );

};

复制代码

ESLint

自定义 eslint-rule,本质上也是访问 AST 节点,是不是跟 Babel 插件的写法很相似呢?

module.exports.rules = {

  "var-length": (context) => ({

    VariableDeclarator: (node) => {

      if (node.id.name.length <= 2) {

        context.report(node, "变量名长度需要大于2");

      }

    },

  }),

};

复制代码

Code2Code

以 Vue To React 为例,大致过程跟ES6 => ES5类似,通过vue-template-compiler编译得到 Vue AST => 转换为 React AST => 输出 React 代码。

有兴趣的同学可以参考vue-to-react

其他多端框架:一份代码 => 多端,大体思路一致。

总结

在实际开发中,遇到的情况往往更加复杂,建议大家多番文档,多观察,用心去感受 ~

最后

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点 star:http://github.crmeb.net/u/lsq不胜感激 !

PHP 学习手册:https://doc.crmeb.com

技术交流论坛:https://q.crmeb.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容