前言
-
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 办法取得 AST
const 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 one unshiftContainer(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 节点类型为 ObjectProperty 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) })) } }
-
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').default const 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 利用流程
- parser 源码转换为 AST
- traverse 遍历 AST,联合 template/types 更新 AST,
- 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) })) } } }; }