前言
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) })) } } }; }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。