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

你有可能会听到过这个词 webpack工程师 ,这个看似像是一个专业很强的职位其实不少时候是一些前端对如今前端工做方式对一些吐槽,对于一个以前没有接触过webpacknodejs,babel 之类的工具的人来讲,看到大量的配置文件后不少人都会看懵javascript

config.js

不少人就干脆无论这些东西,直接上手写业务代码,把这些构建工具就至关于黑科技,咱们把全部的文件都通过这些工具最终生成一个或者几个打包后的文件,其中关于优化和代码转换问题其实一大部分都是在这些配置里面的。若是咱们不去了解其中的一部分原理,后面遇到不少问题(如打包后文件体积过大)时候都是一筹莫展,并且万一哪天构建工具出现问题时候可能连工做都开展不下去了。前端

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

这里咱们去探索一下babel的原理node

babel 是什么?

Babel · The compiler for writing next generation JavaScriptwebpack

6to5

你在npm上能够看到这样一个包名字是6to5, 光看名字可能会让人感受到很诧异,名字看起来可能有点奇怪,其实babel 在开始的时候名字就是这个。简单粗暴es6 -> es5,一会儿就看懂了babel 是用来干啥的,可是很明显这不是一个好名字,这个名字会让人感受到es6普及以后这个库就没用了,为了保持活力这个库可能要不停的修更名字。下面是babel做者一次分享中假设若是按这个命名法则可能出现的名称git

babel-history

很明显发生这种状况是很不合理的,团队内部通过大量讨论后,最终选择了babel,这与电影银河系漫游指南中的Babel fish相应,也有关系到圣经中的一个故事Tower of Babel(ps.优秀的人老是也颇有情怀。)es6

babel is the new jQuery

redux 的做者曾说过这样一句话,能够换一种理解为github

babel : AST :: jQuery : DOM

babel 对于 AST 就至关于 jQuery 对于 DOM, 就是说babel给予了咱们便捷查询和修改 AST 的能力。(AST -> Abstract Syntax Tree) 抽象语法树 后面会讲到。web

为何要用babel转换代码

咱们以前作一些兼容都会都会接触一些 Polyfill 的概念,好比若是某个版本的浏览器不支持 Array.prototype.find 方法,可是咱们的代码中有用到Arrayfind 函数,为了支持这些代码,咱们会人为的加一些兼容代码shell

if (!Array.prototype.find) {
  Object.defineProperty(Array.prototype, 'find', {
      // 实现代码
      ...
  });
}

对于这种状况作兼容也很好实现,引入一个 Polyfill 文件就能够了,可是有一些状况咱们使用到了一些新语法,或者一些其余写法

// 箭头函数
var a = () => {}
// jsx
var Component = () => <div />

这种状况靠 Polyfill, 由于一些浏览器根本就不识别这些代码,这时候就须要把这些代码转换成浏览器识别的代码。babel就是作这个事情的。

babel作了哪些事情

babel-works

为了转换咱们的代码,babel作了三件事

  • Parser 解析咱们的代码转换为AST
  • Transformer 利用咱们配置好的plugins/presetsParser生成的AST转变为新的AST
  • Generator 把转换后的AST生成新的代码

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

从简单的提及

能够看到要想搞懂babel, 就是去了解上面三个步骤都是在干什么,咱们先把比较容易看懂的地方开始了解一下。

Parser 解析

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

Generator 生成

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

babel的核心内容

看起来babel的主要工做都集中在把解析生成的AST通过plugins/presets而后去生成新的AST这上面了。

AST抽象语法树

咱们一直在提到AST它到底是什么呢,既然它的名字叫作抽象语法树,咱们能够想象一下若是把咱们的程序用树状表示会是什么样呢。

var a = 1 + 1
var b = 2 + 2

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

AST中咱们用node(节点)来表示各个代码片断,好比咱们上面程序总体就是一个节点Program节点(全部的 AST 根节点都是 Program 节点),由于它下面有两条语句因此它的 body属性上就两个声明节点VariableDeclaration。因此上面程序的AST就相似这样

ast

能够看到在节点上用各个的属性去表示各类信息以及程序之间的关系,那这些节点每个叫什么名字,都用哪些属性名呢?咱们能够在说明文档上找到这些说明。

关于接口

看这个文档时候咱们能够看到说明大可能是相似这种

interface Node {
  type: string;
  loc: SourceLocation | null;
}

这里提到interface这个咱们在其余语言中是比较常见的,好比Node规定了typeloc属性,若是其余节点继承自Node,那么它也会实现typeloc属性就是说继承自Node的节点也会有这些属性,基本全部节点都继承自Node,因此咱们基本能够看到loc这个属性loc表示个一些位置信息。

节点单位

咱们程序不少地方都会被拆分红一个个的节点,节点里面也会套着其余的节点,咱们在文档中能够看到AST结构的各个 Node 节点都很细微,好比咱们声明函数,函数就是一个节点FunctionDeclaration,函数名和形参那么参数都是一个变量节点Identifier。生成的节点每每都很复杂,咱们能够借助astexplorer来帮助咱们分析AST结构。

图像展现

有了上面这些概念咱们已经能够大概了解AST的概念,以及各个模块表明的含义,假设咱们有这样一个程序,咱们用图形简易的分析下它的结构

function square (n) {
    return n * n
}

ast-example

节点遍历

通过一番努力咱们终于了解了AST以及其中内容的含义,可是这一部分基本不须要咱们作什么,babel会借助Babylon帮咱们生成咱们须要的AST结构。咱们更多要去作的是去修改和改变Babylon生成的这个抽象语法树。

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

visitor

咱们已经知道babel会遍历节点组成的抽象语法树,每个节点都会有本身对应的type,好比变量节点Identifier等。咱们须要给babel提供一个visitor对象,在这个对象上面咱们以这些节点的type作为key,已一个函数做为值,相似以下,

const visitor = {
    Identifier: {
        enter() {
              console.log('traverse enter a Identifier node!')
        },
        exit() {
              console.log('traverse exit a Identifier node!')
        }
      }
}

这样在遍历进入到对应到节点时候,babel就会去执行对应的enter函数,向上遍历退出对应节点时候,babel就会去执行对应的exit函数,接着上面的代码咱们能够作一个测试

const babel = require('babel-core')

const code = `var a = b + c + d`

// 若是plugins是个函数则返回的对象要有visitor属性,若是是个对象则直接定义visitor属性
const MyVisitor = {
  visitor
}

babel.transform(code, {
  plugins: [MyVisitor]
})

咱们执行对应代码能够看到上面enterexit函数分别执行了四次

traverse enter a Identifier node! 
traverse exit a Identifier node!  
... x4

从上面简单的代码上也能够看到a,b,c,d四个变量,它们应该属于同一级别的节点树上,因此遍历时候会分别进入对应节点而后退出再去下一个节点。

Paths

咱们经过visitor能够在遍历到对应节点执行对应的函数,但是要修改对应节点的信息,咱们还须要拿到对应节点的信息以及节点和所在的位置(即和其余节点间的关系), visitor在遍历到对应节点执行对应函数时候会给咱们传入path参数,辅助咱们完成上面这些操做。注意 Path 是表示两个节点之间链接的对象,而不是当前节点,咱们上面访问到了Identifier节点,它传入的 path参数看起来是这样的

{
  "parent": {
    "type": "VariableDeclarator",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "..."
  }
}

从上面咱们能够看到 path 表示两个节点之间的链接,经过这个对象咱们能够访问到节点、父节点以及进行一系列跟节点操做相关的方法。咱们修改一下上面的 visitor 函数

const visitor = {
    Identifier: {
    enter(path) {
      console.log('traverse enter a Identifier node the name is ' + path.node.name)
    },
    exit(path) {
      console.log('traverse exit a Identifier node the name is ' + path.node.name)
    }
  }
}

在执行一下上面的代码就能够看到name打印出来的依次是a,b,c,d。这样咱们就有能够修改操做咱们须要改变的节点了。另外path对象上还包含添加、更新、移动和删除节点有关的其余不少方法,咱们能够经过文档去了解。

一些有用的工具

babel为了方便咱们开发,在每个环节都有不少人性化的定义也提供了不少实用性的工具,好比以前咱们在定义visitor时候分别定义了enter,exit函数,可不少时候咱们其实只用到了一次在enter的时候作一些处理就好了。因此咱们若是咱们直接定义节点的key为函数,就至关于定义了enter函数

const visitor = {
    Identifier(){
        // dosmting
    }
}

// 等同于 ↓ ↓ ↓ ↓ ↓ ↓

const visitor = {
    Identifier: {
        enter() {
            // dosmting
        }
    }
}

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

实际运用

假如咱们有以下代码

const a = 3 * 103.5 * 0.8
log(a)
const b = a + 105 - 12
log(b)

咱们发现这里把console.log简写成了log,为了让这些代码能够执行,咱们如今用babel装置去转换一下这些代码。

改变log函数调用自己

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

interface CallExpression <: Expression {
  type: "CallExpression";
  callee: Expression | Super | Import;
  arguments: [ Expression | SpreadElement ];
  optional: boolean | null;
}

callee是咱们函数执行的名称,arguments就是咱们穿入的参数,参数咱们不须要改变,只须要把函数名称改变就行了,以前的callee是一个变量,咱们如今要把它变成一个表达式(取对象属性值的表达式),咱们看一下手册能够看到是一个MemberExpression类型的值,这里也能够借助以前提到的网站astexplorer来帮助咱们分析。有了这些信息咱们就能够去实现咱们的目的了,咱们这里手动引入一下babel-types辅助咱们建立新的节点

const babel = require('babel-core')
const t = require('babel-types')

const code = `
    const a = 3 * 103.5 * 0.8
    log(a)
    const b = a + 105 - 12
    log(b)
`

const visitor = {
    CallExpression(path) {
        // 这里判断一下若是不是log的函数执行语句则不处理
        if (path.node.callee.name !== 'log') return
        // t.CallExpression 和 t.MemberExpression分别表明生成对于type的节点,path.replaceWith表示要去替换节点,这里咱们只改变CallExpression第一个参数的值,第二个参数则用它本身原来的内容,即原本有的参数
        path.replaceWith(t.CallExpression(
            t.MemberExpression(t.identifier('console'), t.identifier('log')),
            path.node.arguments
        ))
    }
}

const result = babel.transform(code, {
    plugins: [{
        visitor: visitor
    }]
})

console.log(result.code)

执行后咱们能够看到结果

const a = 3 * 103.5 * 0.8;
console.log(a);
const b = a + 105 - 12;
console.log(b);

直接在模块中声明log

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

这里简单的修改下visitor

const visitor = {
    Program(path) {
        path.node.body.unshift(
      t.VariableDeclaration(
        'var',
        [t.VariableDeclarator(
          t.Identifier('log'),
          t.MemberExpression(t.identifier('console'), t.identifier('log'))
        )]
      )
    )
    }
}

执行后生成的代码为

var log = console.log;

const a = 3 * 103.5 * 0.8;
log(a);
const b = a + 105 - 12;
log(b);

总结

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

本文首发与 我的博客
相关文章
相关标签/搜索