本文会对babel文档文档从一个推导角度来阐述每个babel模块的作用,尝试理清其中脉络,方便快速理解。

本文不是官网的copyer或者中文翻译

核心

babel的核心功能在@babel/core包中,核心api为transform系列函数:

babel.transform(code, options, function(err, result) {
  result; // => { code, map, ast }
});

该函数可以将es6+代码转译成es5代码,所以被广泛集成在其他工具里面,完成代码的转译工作,如babel-loader内部就是调该api。

在babel中,还提供了@babel/cli@babel/register两个工具,前者提供命令行工具函数对文件进行转译;后者提供require钩子:对node的require函数改造,对后续require函数在执行时自动对模块进行源码转译后在导入。

babel的目标是对代码进行转译,这个过程可以拆解为:解析源码,遍历ast改造代码,重新生成代码这三个过程。为了提高使用范围,在v7+版本中,babel将功能拆解出来了多个工具,主要有:

  • 解析源码: @babel/parser;
  • 遍历ast改造代码: @babel/traverse@babel/plugin-*;
  • 重新生成代码:@babel/generator;

    解析源码

    将源码解析,生成ast(抽象语法树),会将:

    function square(n) {
    return n * n;
    }

    解析成:

    {
    type: "FunctionDeclaration",
    id: {
      type: "Identifier",
      name: "square"
    },
    params: [{
      type: "Identifier",
      name: "n"
    }],
    body: {
      type: "BlockStatement",
      body: [{
        type: "ReturnStatement",
        argument: {
          type: "BinaryExpression",
          operator: "*",
          left: {
            type: "Identifier",
            name: "n"
          },
          right: {
            type: "Identifier",
            name: "n"
          }
        }
      }]
    }
    }

    ast可以简单的理解为源码字符串进行语法分析后的结构化数据,方便后续进行检查或者改造。

ast中的节点一般还会包含坐标位置,如字符串下标,行数,列数等,更多详细内容请参考官方文档。

老版本babel中使用的是 acornacorn-jsx,在v7以上时,进行了fork改造为@babel/parser

另外@babel/core也集成了@babel/parser功能,可以直接从@babel/core中导出api直接使用:

babel.parse(code: string, options?: Object, callback: Function)

当前的解析器默认只支持最新的es6代码,如果需要兼容一些新语法(非语法糖之类的新特性,新表达式和新操作服,如对象解构,可选表达式,类型等),需要扩展babel语法插件

很多工具其实只需要解析代码即可,如代码检验,如语法高亮,源码中数据收集。

遍历ast改造代码

讲过解析器已经将源码解析成更好处理的结构化数据ast,如果需求是对代码进行调整,只需对ast数据进行调整,然后使用生成器生成新的代码即可。但整个babel需要解决的是将所有最新的es6+特性转译成向后兼容的浏览器可执行代码(es5),需要处理的情况众多,如果直接对ast进行改造,那么代码将非常臃肿。且es规范还在不停的迭代中,臃肿的代码的对后续维护迭代也带来巨大的挑战。针对这种困境,必须需要进行架构上的调整,使用插件化架构。

babel即是处于这样一个原因,采用了访问者模式。可以简单的理解为,在对ast进行一个遍历时,每次进入一个新的节点或者退出一个节点时,都会拜访每一个插件,咨询它们是否需要对当前的情况进行处理。这钟架构证了性能,也保证了扩展性。

另一种插件化架构,也就是流式架构,如gulp。也就是插件队列依次对上一个插件处理后的ast对象进行更深一层次的改造。但这种架构,需要多次循环ast, 在实际使用中,一般一个生产项目,文件内容巨大,文件数量居多,会导致性能崩溃。

所以babel核心框架中,只包含了访问节点和调用插件的逻辑。实际对ast的改造,全部转交给了插件。这也是为什么babel自带了那么多@babel/plugin-*插件。同样社区也拥有非常多的插件,从能能够支持flow, typescipt这些新语法。

插件化的架构,也允许使用者进行拔插式配置,根据当前使用场景进行高度定制。这也就是在配置文件中如babrlrc.js可以配置插件的原因。

为了支持高度动态配置化来适配复杂的场景,babel会将每个插件负责的功能划分足够小,一般每个插件只会负责一个特性。这会导致使用时,需要去了解每一个插件的作用,然后在配置文件中配置超长的插件列表,带来巨大的心智负担和维护难度。为了解决这个问题,babel提供了预设的机制。简单的理解就是一个babel配置可以继承另一个配置,那么我们只需要继承社区上或者官方专业人员配置的预设即可,如:

  • @babel/preset-env
  • @babel/preset-react
  • @babel/preset-typescript
  • @babel/preset-flow

为了方便插件中的复用,babel将遍历ast的工具也开放出来为一个单独的模块@babel/traverse。将节点类型的判断和创建节点的工具库,放在了@babel/types

另外对于一些babel中多个模块公用的一些工具,都封装成工具模块,也就是@babel/helper-*系列模块,如:

  • @babel/helper-compilation-targets
  • @babel/helper-module-imports

由于babel自带了那么多插件,所以很多helper其实是插件的辅助工具,如helper-module-imports就是辅助生成一些导入节点。

在es6+转成es5的过程中,很多语法糖语法(语法上的细微调整),如let, const等实现直接用插件调整代码即可解决。但对于其他的需要大端代码才能实现的特性,如Array#includes,生成器,迭代器,async/await, promise等,如果每次都通过代码展开,那么编译后的代码将会巨大。为了解决这个问题,会将includes的实现放在补丁(polyfill)中,然后直接使用补丁中的实现。如生成器,迭代器,async/await, promise等都是通过这种机制支持。

这些的补丁(polyfill)的导入方式也有两种,一种是全量导入,也就是导入@babel/polyfill模块。一种是按需导入,需要使用预设@babel/preset-env,根据实际使用情况,在使用的模块中按需导入@babel/runtime中的补丁(polyfill)。如:

var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");

var Circle = function Circle() {
  _classCallCheck(this, Circle);
};
@babel/polyfill@babel/runtime的底层实现都是core-js

实际情景下,还是存在插件无法解决的情况:一个无法用老代码补丁实现,也无法使用语法糖替换代码的特性,如Proxy对象,这种特性一般需要js引擎从底层提供。在使用这些特性时,需要注意浏览器兼容性。

生成代码

使用@babel/generator即可对一个ast树重新生成为代码。

配置

我们通常见到的babel配置就是就是用于指导babel行为的配置文件,可以简单的理解为@babel/coretransform函数的选项支持使用配置文件配置。

更多的配置详细使用等请看官网。

其他官方工具

babel还提供了一些其他工具,用于扩展babel生态链:

  • @babel/standalone: 支持浏览器上运行的babel版本,用于一些在线编辑网站,如JS Bin
  • @babel/code-frame: 代码窗口,用于输出类似这种:

    1 | class Foo {
    > 2 |   constructor()
      |                ^
    3 | }
  • @babel/template: babel插件开发工具,支持根据代码字符串创建ast节点。因为ast节点携带信息较多,且结构较深,在手动创建复杂的代码节点时十分不便。使用官方提供的这个工具,可以快速创建一整段代码节点,并且还支持占位符:

    const buildRequire = template(`
    var IMPORT_NAME = require(SOURCE);
    `);
    
    const ast = buildRequire({
    IMPORT_NAME: t.identifier("myModule"),
    SOURCE: t.stringLiteral("my-module"),
    });
    
    //  ||   ||   ||
    // \\// \\// \\//
    
    const myModule = require("my-module");

joyerli
158 声望5 粉丝

前端搬砖一枚,会分享一些对技术的个人理解和思考,还会分享一些自己解决实际碰到的业务需而设计的奇葩技术方案。


引用和评论

0 条评论