前言

  • babel在前端畛域堪称是赫赫有名,这个工具能够实现源码到源码的转换,为什么须要源码到源码的转换?因为前端语言倒退迅速,同时因为不同浏览器对语言及各种标准的实现并不统一,导致很多先进的、优雅的语法不可能在日常开发中安心应用。然而通过babel 可能将源码中的新的高级语法转换为旧的兼容性更强的语法,就能够大大降低开发的老本,进步代码的兼容性。同时babel 也能够实现代码的动态查看,前端驰名开发框架 react 也依赖于 babel。
    babel 性能如此弱小,曾经被许多框架或者开发工具的集成,如react vue-cli等。层层的封装导致咱们平时开发业务过程中很少接触到babel 相干的配置及利用。然而作为前端开发人员咱们有必要理解其原理,及根底的开发。本文次要介绍 babel 相干的工具以及应用形式,意在让大家对babel原理有初步的认知。

    babel 相干工具简介

  • babel首先通过 parser 工具将源码转换为形象语法树(AST);再通过traverse工具对语法树进行遍历的同时,借助template与types工具实现形象语法树节点的增删改;最终通过generator将转换后的形象语法树转换为源码字符串输入。这里先大抵列出babel 相干的工具,babel正是通过这些工具实现的源码到源码的转换。接下来将简略介绍并举例这些工具如何利用于开发中。

    • @babel/parser: 前身 babylon,用于源码到AST的转换
    • @babel/generator: AST到源码的转换
    • @babel/template: 代码字符串生成 AST 模板
    • @babel/traverse: AST 遍历 && 操作
    • @babel/types: 字符串转换为 AST && AST 节点类型判断
    • @babel/core: 蕴含 parse transform type , 依赖babel 配置文件
    • @babel/runtime: helpers 工具库

    @babel/parser

    简介

    babel/parser通过词法解析、语法解析之后实现源码字符串到形象语法树(AST)的 转换操作,即该工具的输出为源码字符串,输入AST对象。

    具体利用

  • parse 办法导入
  • 获取待转换的源码字符串
  • parse 办法调用取得形象语法树(AST)

    • 图一为 ‘let a = 100’ 转换失去的语法树的局部构造。能够看到源码字符串曾经被转换为等价的AST 对象,对象通过不同的字段来区别不同的语法,变量,以及代码构造;如type 用于示意以后节点对应的源码字符串是函数申明、变量申明、还是属性表达式。
const fs = require('fs')const { join } = require('path')// parse 办法导入const parser = require("@babel/parser").parse;// 通过读文件获取源码字符串const code = fs.readFileSync(join(__dirname, './src.js')).toString()// code 为 ‘let a = 100’// 通过parse 办法取得ASTconst ast = parser(code)// 后果打印console.log(ast.program.body)


图1: AST 对象

Tips

  • AST: 是源代码语法结构的一种形象示意。它以树状的模式体现编程语言的语法结构,树上的每个节点都示意源代码中的一种构造
  • 词法解析:将字符序列转换为单词(Token)序列的过程。

    let a - 2// Uncaught SyntaxError: Unexpected token '-'// 意思就是词法解析失败,你的代码语法存在问题
  • 语法解析:语法分析的工作是在词法剖析的根底上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确。

    @babel/traverse

    简介

    提供节点遍历与增删改操作,用于实现AST的更新转换。该办法承受AST对象以及一个 visitors 对象。

    // traverse(ast, visitors)

    具体利用

  • 为了实现源码到源码的转换,@babel/parser 将代码字符串转换为JS 便于操作的对象,通过对该对象的转换间接的实现源码的转换。为了可能转换AST节点的构造,咱们要可能拜访该节点,babel提供了遍历AST的工具 @babel/traverse, 通过 traverse办法,开发者能够很不便的拜访语法树上的每个节点;同时traverse通过访问者模式为被拜访的节点增加操作方法,用于实现对AST节点的增删改。当然你也能够通过本人的形式对AST进行遍历,如大家所熟知的, 深度优先(栈),广度优先(队列) 都能够实现对一个树的遍历。traverse采纳深度优先策略。traverse为咱们提供了两次拜访节点的机会,即开始遍历以后节点时的拜访(enter),以及遍历完结退出拜访(exit)。
  • 通过traverse 在指定AST节点实现log 输入操作:

    const traverse = require('@babel/traverse').default// traverse(ast, visitors)// ast 语法树对象// visitors 用于拜访语法树对象上指定节点的 对象traverse(ast, {// ast 节点// FunctionDeclaration  函数申明节点,FunctionDeclaration(path, state){  const name = 'state.file.opts.filename'  console.log(name)},// 通过对象配置 开始拜访 与 退出拜访节点操作Identifier: {  enter(path){    console.log(322)    // path.toString 会调用 generator 办法将 节点转换为 代码字符串    console.log(path.toString())  },  exit(){    console.log(3224)  }},}
  • 然而通常咱们拜访AST节点的目标不是为了简略的实现log 操作,而是为了更新以后AST节点。为此traverse为parser 办法失去的节点增加了节点操作相干办法,能够通过节点上的这些操作方法实现节点的更新操作,只有将以上log 操作替换成节点的更新操作即可。次要的操作方法有以下几个:

    insertBefore(nodes_: t.Node | t.Node[]):Insert the provided nodes before the current one.insertAfter(nodes_: t.Node | t.Node[]):Insert the provided nodes after the current oneunshiftContainer(listKey: string, nodes: Nodes,):pushContainer(listKey: string, nodes: Nodes,):replaceWith(node: t.Node | NodePath): 替换以后节点remove(): 节点删除

    visitor 对象

    traverse办法承受一个visitor对象,在该对象中通过定义一个以节点的type命名的办法,来实现对该类型节点的拜访与更新操作。

    // 代码字符串模版const insert = template(`console.log(PATH + '=====>' + NAME)`)// visitor 对象{// 用于拜访 对象属性类型的节点 如 { a: 100 } 中的 a对应的 AST 节点类型为 ObjectPropertyObjectProperty(path, state){   const { value }  = path.node      // 判断属性的类型   if(!(t.isFunctionExpression(value) || t.isArrowFunctionExpression(value))) return;   const name = get(value, 'node.key.name') || ''   const filePath = get(state, 'file.opts.filename')// 节点插入操作   path.get('body').insertAfter(insert({     PATH: t.stringLiteral(filePath),     NAME: t.stringLiteral(name)   })) }}
  • visitor对象名字的由来是 访问者模式 (the visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying the structures. It is one way to follow the open/closed principle. ): 为对象提供一系列新的办法用于扭转原对象,然而不会扭转原有对象的构造。@babel/traverse相干源码如下所示:

    // traverse通过对AST节点进行了再一次的包装,增加节点操作相干性能// 相干源码如下Object.assign(NodePath.prototype,NodePath_ancestry,NodePath_inference,NodePath_replacement,  // 节点替换NodePath_evaluation,NodePath_conversion,NodePath_introspection,NodePath_context,NodePath_removal,    // 节点删除NodePath_modification,  //节点批改相干操作NodePath_family,NodePath_comments,); // 对节点进行二次封装// class NodePath<T extends t.Node = t.Node>create(node, obj, key, listKey?): NodePath {return NodePath.get({  parentPath: this.parentPath,  parent: node,  container: obj,  key: key,  listKey,});}

    @babel/template @babel/types

    简介

  • 这两个工具库用于生成新的 AST 节点
  • 其中 @babel/types 可生成简略较简略的节点,如 t.identifier("a"),可生成 { type: 'Identifier', name: 'a' }节点,可示意一个 a变量的申明。
  • @babel/types 还能够进行节点类型的判断如

    const t = require('@babel/types')t.isVariableDeclarator(path)  // 用于判断是否是变量申明t.isFunctionDeclaration(path) // 用于判断是否是函数申明
  • @babel/template 可用于生成简单的AST 模版办法,再通过配置对象将 AST 模版生成不同的 AST 节点。
  • When calling template as a function with a string argument, you can provide placeholders which will get substituted when the template is used. You can use two different kinds of placeholders: syntactic placeholders (e.g. %%name%%) or identifier placeholders (e.g. NAME).

    具体利用

    // 创立AST 节点const template = require('@babel/template').defaultconst generate = require("@babel/generator").default;// 生成节点模版const insert = template(`console.log(100)`)// 通过模板生成 新的AST 节点const node = insert()// 生成可配置的 节点模版const temp = template(`let NAME = 1 + 1`)// 通过配置对象生成一个 新的 AST节点const tempNode = temp({NAME: 'num'})console.log(generate(tempNode).code)// let num = 1 + 1;

@babel/generator

简介

@babel/generator 这个工具用于将 AST 转换为 代码字符串,即输出为AST对象,输入为源码字符串。

具体利用

通过generate工具将template工具生成的模版节点转换为代码字符串。

// 工具导入const generate = require("@babel/generator").default;// 生成节点模版const temp = template(`let NAME = 1 + 1`)// 通过配置生成新的 AST节点const tempNode = temp({  NAME: 'num'})// 通过generate 生成转换后的源码字符串const code = generate(tempNode).code

综合利用-babel插件

  • 大抵理解了 babel 的工作原理,能够将 babel 利用在理论代码打包过程中。
  • babel 利用流程

    1. parser 源码转换为 AST
    2. traverse 遍历AST,联合 template/types 更新AST,
    3. generator 将转换后的AST 转换为源码输入
  • babel 插件的开发简化了以上几个流程,因为是开发 babel 的插件,因而 parser 与 generator 流程是咱们不须要关怀的,插件的关注点在于操作指定的 AST节点上,即 visitor 对象的编写;可参考 babel 插件手册

    示例:为每个办法增加log

    vue 打包过程会对插件进行缓存,因而你批改完插件进行再进行打包可能不会失效,能够通过批改 babel-config.js中此插件的配置进行批改再进行打包
    具体实现
    以下为 babel 配置文件,示例插件间接通过本地导入即可。

    // babel.config.js// 导入本地编写的插件const test = require('./src/babel-plugin/test-console')module.exports = {presets: [  ['@vue/app', {    useBuiltIns: 'entry',  }],],// 插件配置plugins: [  [  // ...其余插件  // 自定义本地插件配置,如能够通过批改 name 来革除缓存  [test, {name: 'c'}]],};

    以下为插件示例代码

    const get = require('lodash.get')// 间接定义 visitor 对象即可,这里通过一个函数返回 该对象module.exports = function test(babel, ops) {const { types: t, template } = babel// 定义一个 节点 模版,用于插入到指定的AST节点中const insert = template(`console.log(PATH + '=====>' + NAME)`)// 返回一个 蕴含 visitors 的对象return {  name: 'my-plugin2',  visitor: {// 拜访对象办法属性     ObjectMethod(path, state){// 获取以后属性名      const name = get(path, 'node.key.name') || ''// 获取以后文件门路      const filePath = get(state, 'file.opts.filename')// 在节点开端插入由 template 生成的新的节点对象      path.get('body').insertAfter(insert({        // 留神这里不能间接配置为 `${filePath}`, 否则最终会被解析为变量名        /*           Module parse failed: Invalid regular expression flag (119:17)        */        PATH: t.stringLiteral(filePath),        NAME: t.stringLiteral(name)      }))     },// 拜访对象属性     ObjectProperty(path, state){      const { value }  = path.node         // 判断属性的类型      if(!(t.isFunctionExpression(value) || t.isArrowFunctionExpression(value))) return;      const name = get(value, 'node.key.name') || ''      const filePath = get(state, 'file.opts.filename')      path.get('body').insertAfter(insert({        PATH: t.stringLiteral(filePath),        NAME: t.stringLiteral(name)      }))    }  }};}