博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从零开始写一个 Babel 插件
阅读量:6271 次
发布时间:2019-06-22

本文共 3743 字,大约阅读时间需要 12 分钟。

相信目前常与 ES6 代码打交道的同学对 Babel 应该不会陌生,在 ES6 代码被编译转化为 ES5 代码的过程中,Babel 插件显得尤为重要,我们最后经由 Babel 生成的代码取决于插件在这一层中做了什么事,在探索这其中的过程之前,我们先来了解下一些所需的基础知识。

抽象语法树

Babel 的工作流可以用下面一张图来表示,代码首先经由 babylon 解析成(AST),后经一些遍历和分析转换(主要过程),最后根据转换后的 AST 生成新的常规代码。

在这其中,理解清楚 AST 十分重要,我们之所以需要将代码转换为 AST 也是为了让计算机能够更好地进行理解。我们可以来看看下面这段代码被解析成 AST 后对应的结构图:

function abs(number) {  if (number >= 0) {  // test    return number;  // consequent  } else {    return -number; // alternate  }}复制代码

所有的 AST 根节点都是 Program 节点,从上图中我们可以看到解析生成的 AST 的结构的各个 Node 节点都很细微, 有个文档对每个节点类型都做了详细的说明,你可以对照各个节点类型在这查找到所需要的信息。在这个例子中,我们主要关注函数声明里的内容, IfStatement 对应代码中的 if...else 区块的内容,我们先对条件(test)进行判断,这里是个简单的二进制表达式,我们的分支也会从这个条件继续进行下去,consequent 代表条件值为 true 的分支,alternate 代表条件值为 false 的分支,最后两条分支各自在 ReturnStatement 节点进行返回。

了解 AST 各个节点的类型是后续编写插件的关紧,AST 通常情况下都是比较复杂的,上述一段简单的函数定义也生成了比较大的 AST,对于一些复杂的程序,我们可以借助 来帮我们分析 AST 的结构。

遍历节点

在插件里进行节点遍历需要先了解 visitor 和 path 的概念,前者相当于从众多节点类型中选择开发者所需要的节点,后者相当于对节点之间的关系的访问。

visitor

Babel 使用 babel-traverse 进行树状的遍历,对于 AST 树上的每一个分支我们都会先向下遍历走到尽头,然后向上遍历退出遍历过的节点寻找下一个分支。Babel 提供我们一个 visitor 对象供我们获取 AST 里所需的具体节点来进行访问,比如我只想访问 if...else 生成的节点,我们可以在 visitor 里指定获取它所对应的节点:

const visitor = {  IfStatement() {    console.log('get if');  }};复制代码

继续上述所说的遍历,其实这种遍历会让每个节点都会被访问两次,一次是向下遍历代表进入(enter),一次是向上退出(exit)。因此实际上每个节点都会有 enter 和 exit 方法,在实际操作的时候需要注意这种遍历方式可能会引起的一些问题,上述例子是省略掉 enter 的简写。

const visitor = {  IfStatement: {    enter() {},    exit() {}  }}复制代码

path

visitor 模式中我们对节点的访问实际上是对节点路径的访问,在这个模式中我们一般把 path 当作参数传入节点选择器中。path 表示两个节点之间的连接,通过这个对象我们可以访问到节点、父节点以及进行一系列跟节点操作相关的方法(类似 DOM 的操作)。

var babel = require('babel-core');var t = require('babel-types');const code = `d = a + b + c`;const visitor = {	Identifier(path) {		console.log(path.node.name);  // d a b c	}};const result = babel.transform(code, {	plugins: [{		visitor: visitor	}]});复制代码

替换节点

具备了 AST 相关知识和了解 visitor、path 后,就可以编写一个简单的 Babel 插件了。我们要把上述的 abs 函数换成原生支持的 Math.abs 来进行调用 。

首先我们先解析下 abs(-8) 的 AST 结构,直接从表达式语句(ExpressionStatement)开始:

{  type: "ExpressionStatement",  expression: {    type: "CallExpression",    callee: {      type: "Identifier",      name: "abs"    },    arguments: [{      type: "UnaryExpression",      operator: "-",      prefix: true,      arguments: {        type: "NumericLiteral",        value: 8      }    }]  }}复制代码

上述AST结构可以在 查看。

我们可以看到表达式语句下面的 expression 主要是函数调用表达式(CallExpression),因此我们也需要创建一个函数调用表达式,此外,Math.abs 是一个二元操作表达式,属于 MemberExpression 类型。上述两个 AST 节点我们可以借助 里提供的一些方法帮我们快速创建。

// 创建函数调用表达式t.CallExpression(    // 创建对象属性引用	t.MemberExpression(t.identifier('Math'), t.identifier('abs')), 	// 原始节点函数调用参数	path.node.arguments )复制代码

最后我们需要对此次函数调用不符合的节点进行过滤,过滤掉名字不等于 abs 的函数调用,因为 Babel 在遍历的过程是递归的,如果不过滤做限制的话,程序将会一直运行最终报调用栈超过阈值的错误。

RangeError: unknown: Maximum call stack size exceeded

最终代码如下:

var babel = require('babel-core');var t = require('babel-types');const code = `abs(-8);`;const visitor = {	CallExpression(path) {		if (path.node.callee.name !== 'abs') return;        		path.replaceWith(t.CallExpression(			t.MemberExpression(t.identifier('Math'), t.identifier('abs')),			path.node.arguments		));	}};const result = babel.transform(code, {	plugins: [{		visitor: visitor	}]});// Math.abs(-8)console.log(result.code);复制代码

上述例子使用了 transform api 直接解析转换生成了新的代码,另外在单独编写 Babel 插件的时候,暴露的参数里一般都含有常用的 babel-types 对象供使用。

export default function({ types: t }) {    return {        visitor: {            CallExpression(path) {                if (path.node.callee.name !== 'abs') return;                path.replaceWith(t.CallExpression(                    t.MemberExpression(t.identifier('Math'),                    t.identifier('abs')),                    path.node.arguments                ));            }        }    };}复制代码

,可以在线直观的观察插件是否起作用!

参考文章:

转载于:https://juejin.im/post/5ce53caef265da1bd1462f20

你可能感兴趣的文章
rtf格式的一些说明,转载的
查看>>
REST Security with JWT using Java and Spring Security
查看>>
echarts学习总结(二):一个页面存在多个echarts图形,图形自适应窗口大小
查看>>
IIS7显示ASP的详细错误信息到浏览器
查看>>
使用fiddler对手机APP进行抓包
查看>>
exit和_exit的区别
查看>>
Javascript、Jquery获取浏览器和屏幕各种高度宽度(单位都为px)
查看>>
php不重新编译,安装未安装过的扩展,如curl扩展
查看>>
JavaScript编码encode和decode escape和unescape
查看>>
ppp点对点协议
查看>>
html5游戏开发-简单tiger机
查看>>
Codeforces 712C Memory and De-Evolution
查看>>
编写的windows程序,崩溃时产生crash dump文件的办法
查看>>
Ural2110 : Remove or Maximize
查看>>
Django REST framework 的TokenAuth认证及外键Serializer基本实现
查看>>
《ArcGIS Runtime SDK for Android开发笔记》——问题集:如何解决ArcGIS Runtime SDK for Android中文标注无法显示的问题(转载)...
查看>>
Spring Boot日志管理
查看>>
动态注册HttpModule管道,实现global.asax功能
查看>>
使用 ES2015 编写 Gulp 构建
查看>>
[转]Using NLog for ASP.NET Core to write custom information to the database
查看>>