babel原理及插件开发

8,114 阅读6分钟

摘要

如今的前端界已经离不开ES6,然而老旧浏览器并不支持,项目中特别是国内公司又需要兼容低版本的老旧浏览器,多亏了babel这个神奇的工具,可以让我们的ES6代码运行在旧浏览器中。

大部分前端开发人员只是配置一下babel,根据需要装个插件之类,我想肯定少有人去研究babel转换ES6代码的原理及插件原理,于是在某个日子里由于项目的需要去研究了一下babel的原理。

需要说明的是,本文不涉及babel的用法,不论是看官网文档还是其他这类文章都太多,本文会结合自己曾经写的一个babel插件来分析babel的原理。

分析

babel实际上类似一般的的语言编译器,作用就是输入输入代码,实际上跟很多人理解的不太一样,babel并不是只能用于ES6编译成ES5,只要你愿意,你完全可以把ES5编译成ES6,或者使用自己创造的某种语法(例如JSX,以及本文结合的babel插件就属于这类),你需要做的只是编写对应的插件。

babel转换代码的过程主要为三步:

解析

使用babylon这个解析器,它会根据输入的javascript代码字符串根据ESTree规范生成AST(抽象语法树)。

转换

根据一定的规则转换、修改AST。

生成

使用babel-generator将修改后的AST转换成普通代码。

这就是babel工作的整个过程,就是纯粹的字符串输入输出而已,而babel插件或者预置的stage-0,1,2,3,jsx等,都是第二步转换的“规则”。

什么是AST?

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

如果要理解babel的原理,理解AST是必不可少的,尽管前端同学可能平时对于代码编译这类接触不多,但也应该了解AST。

我们都知道javascript代码是由一系列字符组成的,我们看一眼字符就知道它是干什么的,例如变量声明、赋值、括号、函数调用等等。但是计算机并没有眼睛可以看到,它需要某种机制去理解代码字符串,基于此考虑为了让人和计算机都能够理解代码,就有了AST这么个东西,它是源代码的一种映射,在某种规则中二者可以相互转化,语言引擎根据AST就能知道代码的作用是什么。

以下一句简单的变量声明

var a = 1;

当这句声明生成AST后,可以得到以下的树形结构

Imgur

body中就是主体代码的信息,可以看到VariableDeclaration,即变量声明,在declarations数组中就是声明的详情。

具体可以在astexplorer.net/中查看,你可以在左侧输入任何代码,右侧会对应显示生成的AST。

编写插件

简介

本文编写的插件为babel-plugin-webpack-async-module-name,用途是在webpack中为import()异步模块命名。

具体转换是转换以下方法调用:

importName('./a.js', 'name-a');

ES6是提供了一个import()方法用于动态导入模块,然而这个方法只有一个路径参数,没有能够为动态模块命名之类的参数,好在webpack社区提供了一种在webpack中的命名方式:

import( /*webpackChunkName: 'name-a'*/'./a.js');

在使用的时候加入一行注释,根据注释中的webpackChunkName的值,结合webpack配置的output的chunkName为模块命名,具体可以查看webpack文档。

然而这样必须在每次调用的时候手动添加注释及注释中的名字,强迫症是无法忍受的,于是想了想发现babel可以实现一个自定义方法接收模块名生成带注释的import()方法,这个插件作用就是生成这个带注释的import()的方法。

编写

babel-plugin-xxx.js中导出一个函数

module.exports = function(babel) {
  var t = babel.types
  return {
    visitor: {

    }
  }
}

babel的插件系统基于访问者模式设计,我们编写的这个函数就是为访问者模式提供一个接口。

babel.types包含里处理AST的一系列工具方法,具体可以查看文档,实际编写的时候,建议在astexplorer.net/中输入编译前后的代码,对比AST的区别,然后通过babel.types提供的方法修改AST即可。

编写babel插件首先需要知道要处理的哪种语法,具体到上面的这个插件中,需要处理的是函数调用,那么可以在visitor中添加CallExpression属性,代表处理的是函数调用,以下是具体代码。

visitor: {
  CallExpression: function (path) {
    const {node} = path
    if (t.isIdentifier(node.callee, {name: 'importName'})) {
      const [module, name] = node.arguments
      if (name) {
        module.leadingComments = [{
          type: "CommentBlock",
          value: `webpackChunkName: '${name.value}'`
        }]
      }
      path.replaceWith(
        t.CallExpression(
          t.identifier('import'),
          [module]
        )
      )
    }
  }
}

path是处理的AST节点的路径,接着需要判断具体调用的函数方法为importName,中间根据name加上注释,然后使用path.replaceWith替换为新的CallExpression即可。

这样经过babel插件处理后,代码中的importName('./a.js', 'name-a')的AST就会被转成正确的import()方法的AST,并加上注释。

生成

这一步就是根据babel配置中presets和plugin选项中定义的规则产生的新的AST去生成正常的代码。

总结

babel就是一个编译工具,根据输入字符串得到输出,解析 -> 转换 -> 生成就是大致原理,至于如何从正常代码解析成AST以及根据AST生成代码,我认为这是语言的编译原理相关,babel的解析器只是其中的一种实现,而babel的强大之处在于提供的丰富AST转换工具(好吧实际上是半路出家对于编译原理不甚了解就不好意思献丑了)。

最后再次提一下这个插件babel-plugin-webpack-async-module-name,目前一定规模的前端应用采用代码异步加载是必然趋势,强迫症如果不想写一堆require.ensure()...一堆东西或者想使用import()作为异步模块导入并简单地自定义模块名,可以尝试下这个插件😎。

本文来自babel原理及插件开发