探究babel和babel插件是怎样事情的

你有能够会听到过这个词 webpack工程师 ,这个看似像是一个专业很强的职位实在许多时刻是一些前端对如今前端事变体式格局对一些吐槽,关于一个之前没有打仗过webpacknodejs,babel 之类的东西的人来讲,看到大批的设置文件后许多人都邑看懵

《探究babel和babel插件是怎样事情的》

许多人就痛快不论这些东西,直接上手写营业代码,把这些构建东西就相当于黑科技,我们把一切的文件都经由这些东西终究天生一个或许几个打包后的文件,个中关于优化和代码转换题目实在一大部份都是在这些设置内里的。如果我们不去相识个中的一部份道理,背面碰到许多题目(如打包后文件体积过大)时刻都是一筹莫展,而且万一哪天构建东西涌现题目时刻能够连事变都展开不下去了。

既然我们一样平常都要用到,最好的体式格局就是去研究一下这些东西的道理的作用,让这些东西成为我们手中的利器,而不是事变上的绊脚石,而且这些东西的设计者都是顶级的工程师,当你敲开壁垒探讨内部隐秘时刻,我置信你会感受到个中的编程之美。

这里我们去探究一下babel的道理

babel 是什么?

Babel · The compiler for writing next generation JavaScript

6to5

你在npm上能够看到如许一个包名字是6to5, 光看名字能够会让人感觉到很惊讶,名字看起来能够有点新鲜,实在babel 在最先的时刻名字就是这个。简朴粗犷es6 -> es5,一会儿就看懂了babel 是用来干啥的,然则很明显这不是一个好名字,这个名字会让人感觉到es6提高以后这个库就没用了,为了坚持生机这个库能够要不断的修正名字。下面是babel作者一次分享中假定如果按这个定名轨则能够涌现的称号

《探究babel和babel插件是怎样事情的》

很明显发作这类状况是很不合理的,团队内部经由大批议论后,终究挑选了babel,这与影戏银河系遨游指南中的Babel fish响应,也有关联到圣经中的一个故事Tower of Babel(ps.优异的人老是也很有情怀。)

babel is the new jQuery

redux 的作者曾说过如许一句话,能够换一种理解为

babel : AST :: jQuery : DOM

babel 关于 AST 就相当于 jQuery 关于 DOM, 就是说babel赋予了我们便利查询和修正 AST 的才能。(AST -> Abstract Syntax Tree) 笼统语法树 背面会讲到。

为何要用babel转换代码

我们之前做一些兼容都邑都邑打仗一些 Polyfill 的观点,比方如果某个版本的阅读器不支撑 Array.prototype.find 要领,然则我们的代码中有效到Arrayfind 函数,为了支撑这些代码,我们会工资的加一些兼容代码

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

关于这类状况做兼容也很好完成,引入一个 Polyfill 文件就能够了,然则有一些状况我们应用到了一些新语法,或许一些其他写法

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

这类状况靠 Polyfill, 由于一些阅读器基础就不辨认这些代码,这时刻就须要把这些代码转换成阅读器辨认的代码。babel就是做这个事变的。

babel做了哪些事变

《探究babel和babel插件是怎样事情的》

为了转换我们的代码,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就相似如许

《探究babel和babel插件是怎样事情的》

能够看到在节点上用各个的属性去示意种种信息以及顺序之间的关联,那这些节点每个叫什么名字,都用哪些属性名呢?我们能够在申明文档上找到这些申明。

关于接口

看这个文档时刻我们能够看到申明大多是相似这类

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
}

《探究babel和babel插件是怎样事情的》

节点遍历

经由一番勤奋我们终究相识了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,根据自身的主意写一些插件或许去孝敬一些代码,置信在这个历程当中你收成的相对比你设想中的要更多!

本文首发与
个人博客

    原文作者:x!!!
    原文地址: https://segmentfault.com/a/1190000013261724
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞