灏天阁

聊聊Babel: 让你的 JavaScript 代码跨浏览器兼容

· Yin灏

Babel 的执行过程

对于计算机来说,我们敲出来的代码都是「高级语言」,即人能读懂但计算机读不懂,因此想要执行就必须经过「编译」。

像「react」、「vue」等框架需要对其 「.vue」 和 「.jsx」 代码进行编译才能在页面上正常显示也是同理。

既然「Babel」需要将高版本 js 语法转换成低版本 js 语法,那么也势必需要进行编译,因此「Babel」的执行过程就是一个编译转换的过程。

「Babel 的执行过程如下图所示:」

由此可以发现,使用「Babel」进行代码转换的过程主要包含三个部分:

  • 编译(parse)
  • 转换(transform)
  • 生成(generator)
1. parse

在「Babel」执行过程中,编译器插件是@babel/parser,其「作用就是将源码转换为 AST」,使用前需要「npm install @babel/parser」, 使用方法如下:

const babelParser = require("@babel/parser");
const code = "const name= 'dossweet';";
const ast = babelParser.parse(code);
console.log(ast);

执行后的结果如下:

这就是「Babel」生成的「AST 结构」。可以发现这个「AST」和平常我们看到的「AST」结构好像不同,因为没有发现“name"、“=”等词法单元。

这是因为「Babel」的「parser」是根据「Babel」的「AST」结构生成的,基于「ESTree 规范」。

“name”、“=”等词法单元都在 body:[[Node]]中,执行「ast.program.body[0].declarations」就能看到编写的代码内容啦:

2. transform

「transform 是 Babel 中非常重要的一个环节,将高版本的 JS 转换为低版本的 JS 就是在这个阶段执行。」

「Babel」的执行可以看做是一个工具链。所谓工具链,就是需要依赖一些插件来完成将高版本 js 转换为低版本 js,因此「只有包含插件的 Babel 才能发挥出真正的作用」。

如果没有转换插件,「Babel」只会将源码生成「AST 抽象语法树」,然后再通过生成器生成和原来的源码一模一样的代码,这样的过程没有任何意义,「因此插件是 Babel 的核心,也是用户可以自定义执行过程的环节。」

「Babel」官网提供了一些官方的插件,例如 「@babel/core」 、「**@babel/types」** 、「**@babel/traverser」**  来供大家使用。

  • 「@babel/types」插件的主要作用是创建、修改、删除、查找 ast 节点。
  • 「@babel/traverser」插件的主要作用是对 ast 进行遍历 parse,在迭代的过程中可以定义回调函数,回调函数的参数提供了丰富的增、删、改、查以及类型断言的方法。
  • 「@babel/core」插件的主要作用是将底层的插件进行封装,并加入读取、分析配置文件等功能来是简化插件的执行。

3. generator

「generator 是 Babel 的生成器」,其使用也是以@babel/generator 的方式进行的,「主要功能就是将转换后的 AST 抽象语法树转换为代码,以便于浏览器可以运行」。

可以发现,「Babel」将高版本的 js 语法转换为低版本的 js 语法就是通过先将源代码转换为 AST 抽象语法树,然后借助一系列插件来遍历抽象语法树中的节点,修改 js 语法为低版本的写法,然后将转换后的抽象语法树生成低版本的源码,从而达到不同浏览器兼容的目的。

与此同时,「Babel」在「tranform 阶段」修改抽象语法树时,不仅可以修改 js 语法为低版本,还能做一些其他有意义的事情,比如「删去所有的 console.log」、「统计代码执行时间」、「修改变量名称」等,这些工作可以大幅度提高我们的开发效率,不再需要我们在业务代码中手动处理,体验非常 nice~

「接下来,我们就一起实现一个简易的 Babel 插件,来进一步体验 Babel 插件的魅力吧!」

实现一个 Babel 的插件

插件格式:

「Babel」给予了开发者自定义插件的功能,但是我们需要按照插件的规范来进行功能的开发。

一个「Babel 插件」的框架至少需要包含如下内容:

export default function ({ types: t }) {
  return {
    visitor: {
      // 访问者方法 1
      Identifier(path, state) {
        // 处理 Identifier 节点的代码
      },

      // 访问者方法 2
      BinaryExpression(path, state) {
        // 处理 BinaryExpression 节点的代码
      },

      // 访问者方法 n
      // ...
    },
  };
}

其中 「{type: t}」 是一个「Babel 的对象」,「visitor」是插件的访问者,用于访问抽象语法树上的节点,从而进行一些增删改查的操作。

「Identifier」和「BinaryExpression」等都是访问者方法,在遍历抽象语法树时会被调用。

「Visitor」 中的每个函数接收 2 个参数:「path」 和 「state」。其中 「path」 参数表示当前正在被遍历的节点,我们可以使用它来访问和修改节点,「state」用于在插件内部存储状态信息。

插件功能:
  • 删除所有的 console.log

插件源码:

const { types: t } = require("@babel/core");

module.exports = function () {
  return {
    visitor: {
      CallExpression(path) {
        if (
          t.isMemberExpression(path.node.callee) &&
          t.isIdentifier(path.node.callee.object, { name: "console" }) &&
          t.isIdentifier(path.node.callee.property, { name: "log" })
        ) {
          path.remove();
        }
      },
    },
  };
};

上述代码中,使用了 「@babel/types」 模块中的 「types 对象」,该对象提供了一些常见的 「AST」 节点类型定义和操作方法,例如 isMemberExpression、isIdentifier 等。通过这些方法,我们可以判断 「AST」 节点的类型,并对其进行处理。

在这个插件中,我们主要是判断函数调用是否为 「console.log」,如果是,则调用 「path.remove()」 方法将其从 「AST」 中移除,从而实现将 「console.log」 语句自动注释掉的功能。

「仓库地址:https://github.com/dossweet/BabelStudy」

总结

「Babel」的本质就是一个「编译器」,用法就是使用插件来修改「抽象语法树 AST」,从而实现高版本 js 语法转换为低版本 js 语法的目的,最终达到保证不同的浏览器环境都可以正常使用页面的效果。

在「前端工程化」中,「Babel」常作为「Webpack」的「loader」和「plugin」与「Webpack」配合使用。

考虑到「Babel」是通过操作「抽象语法树」来进行一些转换工作,因此我们也可以自定义一些插件来提高我们的开发效率~

参考文献:

[1] Babel 官网: https://www.babeljs.cn/

[2] 编写 Babel 插件: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

[3] 理解 Babel 的基本原理和使用方法: https://www.cnblogs.com/jyybeam/p/13375179.html

- Book Lists -