天天看点

探索babel和babel插件是怎么工作的

你有可能会听到过这个词 webpack工程师 ,这个看似像是一个专业很强的职位其实很多时候是一些前端对现在前端工作方式对一些吐槽,对于一个之前没有接触过<code>webpack</code>,<code>nodejs</code>,<code>babel</code> 之类的工具的人来说,看到大量的配置文件后很多人都会看懵

探索babel和babel插件是怎么工作的

很多人就干脆不管这些东西,直接上手写业务代码,把这些构建工具就相当于<code>黑科技</code>,我们把所有的文件都经过这些工具最终生成一个或者几个打包后的文件,其中关于优化和代码转换问题其实一大部分都是在这些配置里面的。如果我们不去了解其中的一部分原理,后面遇到很多问题(<code>如打包后文件体积过大</code>)时候都是束手无策,而且万一哪天构建工具出现问题时候可能连工作都开展不下去了。

既然我们日常都要用到,最好的方式就是去研究一下这些工具的原理的作用,让这些工具成为我们手中的利器,而不是工作上的绊脚石,而且这些工具的设计者都是顶级的工程师,当你敲开壁垒探究内部秘密时候,我相信你会感受到其中的编程之美。

这里我们去探索一下<code>babel</code>的原理

Babel · The compiler for writing next generation JavaScript

探索babel和babel插件是怎么工作的

<code>redux</code> 的作者曾说过这样一句话,可以换一种理解为

<code>babel</code> 对于 <code>AST</code> 就相当于 <code>jQuery</code> 对于 <code>DOM</code>, 就是说<code>babel</code>给予了我们便捷查询和修改 <code>AST</code> 的能力。<code>(AST -&gt; Abstract Syntax Tree) 抽象语法树 后面会讲到。</code>

我们之前做一些兼容都会都会接触一些 <code>Polyfill</code> 的概念,比如如果某个版本的浏览器不支持 <code>Array.prototype.find</code> 方法,但是我们的代码中有用到<code>Array</code> 的<code>find</code> 函数,为了支持这些代码,我们会人为的加一些兼容代码

对于这种情况做兼容也很好实现,引入一个 <code>Polyfill</code> 文件就可以了,但是有一些情况我们使用到了一些新语法,或者一些其他写法

这种情况靠 <code>Polyfill</code>, 因为一些浏览器根本就不识别这些代码,这时候就需要把这些代码转换成浏览器识别的代码。<code>babel</code>就是做这个事情的。

探索babel和babel插件是怎么工作的

为了转换我们的代码,<code>babel</code>做了三件事

<code>Parser</code> 解析我们的代码转换为<code>AST</code>。

<code>Transformer</code> 利用我们配置好的<code>plugins/presets</code>把<code>Parser</code>生成的<code>AST</code>转变为新的<code>AST</code>。

<code>Generator</code> 把转换后的<code>AST</code>生成新的代码

从图上看 <code>Transformer</code> 占了很大一块比重,这个转换过程就是<code>babel</code>中最复杂的部分,我们平时配置的<code>plugins/presets</code>就是在这个模块起作用。

可以看到要想搞懂<code>babel</code>, 就是去了解上面三个步骤都是在干什么,我们先把比较容易看懂的地方开始了解一下。

解析步骤接收代码并输出 <code>AST</code>,这其中又包含两个阶段词法分析和语法分析。词法分析阶段把字符串形式的代码转换为 <code>令牌(tokens)</code> 流。语法分析阶段会把一个令牌流转换成 <code>AST</code> 的形式,方便后续操作。

代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

看起来<code>babel</code>的主要工作都集中在把解析生成的<code>AST</code>经过<code>plugins/presets</code>然后去生成<code>新的AST</code>这上面了。

我们一直在提到<code>AST</code>它究竟是什么呢,既然它的名字叫做<code>抽象语法树</code>,我们可以想象一下如果把我们的程序用树状表示会是什么样呢。

我们想象一下要表示上述代码应该是什么样子,首先必须有东西可以表示这些具体的<code>声明</code>,<code>变量</code>,<code>常量</code>的具体信息,比如<code>(这棵树上肯定有二个变量,变量名是a和b,肯定有两个运算语句,操作符是 + )</code>,有了这些信息还不够,我们必须建立起它们之间的关系,比如<code>一个声明语句,声明类型是 var, 左侧是变量, 右侧是表达式</code>。有了这些信息我们就可以还原这个程序,这也是把代码解析成<code>AST</code>时候所做的事情,对应上面我们说的<code>词法分析</code> 和 <code>语法分析</code>。

在<code>AST</code>中我们用<code>node</code>(节点)来表示各个代码片段,比如我们上面程序整体就是一个节点<code>Program</code>节点(所有的 AST 根节点都是 Program 节点),因为它下面有两条语句所以它的 <code>body</code>属性上就两个声明节点<code>VariableDeclaration</code>。所以上面程序的<code>AST</code>就类似这样

探索babel和babel插件是怎么工作的

看这个文档时候我们可以看到说明大多是类似这种

这里提到<code>interface</code>这个我们在其他语言中是比较常见的,比如<code>Node</code>规定了<code>type</code>和<code>loc</code>属性,如果其他节点继承自<code>Node</code>,那么它也会实现<code>type</code>和<code>loc</code>属性就是说继承自<code>Node</code>的节点也会有这些属性,基本所有节点都继承自<code>Node</code>,所以我们基本可以看到<code>loc</code>这个属性<code>loc</code>表示个一些位置信息。

有了上面这些概念我们已经可以大概了解<code>AST</code>的概念,以及各个模块代表的含义,假设我们有这样一个程序,我们用图形简易的分析下它的结构

探索babel和babel插件是怎么工作的

<code>babel</code>拿到抽象语法树后会使用<code>babel-traverse</code>进行递归的树状遍历,对于每一个节点都会向下遍历到尽头,然后向上遍历退出分支去寻找下一个分支。这样确保我们能找到任何一个节点,也就是能访问到我们代码的任何一个部分。可是我们要怎么去完成修改操作呢,<code>babel</code>给我们提供了下面这两个概念。

我们已经知道<code>babel</code>会遍历节点组成的抽象语法树,每一个节点都会有自己对应的<code>type</code>,比如变量节点<code>Identifier</code>等。我们需要给<code>babel</code>提供一个<code>visitor</code>对象,在这个对象上面我们以这些节点的<code>type</code>做为<code>key</code>,已一个函数作为值,类似如下,

这样在遍历进入到对应到节点时候,<code>babel</code>就会去执行对应的<code>enter</code>函数,向上遍历退出对应节点时候,<code>babel</code>就会去执行对应的<code>exit</code>函数,接着上面的代码我们可以做一个测试

我们执行对应代码可以看到上面<code>enter</code>和<code>exit</code>函数分别执行了四次

从上面简单的代码上也可以看到<code>a,b,c,d</code>四个变量,它们应该属于同一级别的节点树上,所以遍历时候会分别进入对应节点然后退出再去下一个节点。

我们通过<code>visitor</code>可以在遍历到对应节点执行对应的函数,可是要修改对应节点的信息,我们还需要拿到对应节点的信息以及节点和所在的位置<code>(即和其他节点间的关系)</code>, <code>visitor</code>在遍历到对应节点执行对应函数时候会给我们传入<code>path</code>参数,辅助我们完成上面这些操作。注意 <code>Path</code> 是表示两个节点之间连接的对象,而不是当前节点,我们上面访问到了<code>Identifier</code>节点,它传入的 <code>path</code>参数看起来是这样的

从上面我们可以看到 <code>path</code> 表示两个节点之间的连接,通过这个对象我们可以访问到节点、父节点以及进行一系列跟节点操作相关的方法。我们修改一下上面的 <code>visitor</code> 函数

在执行一下上面的代码就可以看到<code>name</code>打印出来的依次是<code>a</code>,<code>b</code>,<code>c</code>,<code>d</code>。这样我们就有可以修改操作我们需要改变的节点了。另外<code>path</code>对象上还包含添加、更新、移动和删除节点有关的其他很多方法,我们可以通过文档去了解。

<code>babel</code>为了方便我们开发,在每一个环节都有很多人性化的定义也提供了很多实用性的工具,比如之前我们在定义<code>visitor</code>时候分别定义了<code>enter</code>,<code>exit</code>函数,可很多时候我们其实只用到了一次在<code>enter</code>的时候做一些处理就行了。所以我们如果我们直接定义节点的<code>key</code>为函数,就相当于定义了<code>enter</code>函数

上面我们还提到了plugins是函数的情况,其实我们写的差距一般都是一个函数,这个入口函数上<code>babel</code>也会穿入一个<code>babel-types</code>,这是一个用于<code>AST</code> 节点的 <code>Lodash</code> 式工具库(类似<code>lodash</code>对于<code>js</code>的帮助), 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。

假如我们有如下代码

我们发现这里把<code>console.log</code>简写成了<code>log</code>,为了让这些代码可以执行,我们现在用<code>babel</code>装置去转换一下这些代码。

既然是<code>console.log</code>没有写全,我们就改变这个<code>log</code>函数调用的地方,把每一个<code>log</code>替换成<code>console.log</code>,我们看一下<code>log(*)</code>属于函数执行语句,相对应的节点就是<code>CallExpression</code>,我们看下它的结构

执行后我们可以看到结果

我们已经知道每一个模块都是一个对于的<code>AST</code>,而<code>AST</code>根节点是 <code>Program</code> 节点,下面的语句都是<code>body</code>上面的子节点,我们只要在<code>body</code>头声明一下<code>log</code>变量,把它定义为<code>console.log</code>,后面这样使用就也正常了。

这里简单的修改下visitor

执行后生成的代码为

到这里我们已经简单的分析代码,修改一些抽象语法树上的内容来达到我们的目的,但是还是有很多中情况还没考虑进去,而<code>babel</code>现阶段不仅仅代表着去转换<code>es6</code>代码之类的功能,实际上我们自己可以写出很多有意思的插件,欢迎来了解<code>babel</code>,按照自己的想法写一些插件或者去贡献一些代码,相信在这个过程中你收获的绝对比你想象中的要更多!

继续阅读