babel原理及插件开发
摘要
如今的前端界已经离不开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后,可以得到以下的树形结构

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原理及插件开发
转载自:https://juejin.cn/post/6844903603983892487