前言

对于前端开发而言,babel肯定是再熟悉不过了,工作中肯定会用到。除了用作转换es6和jsx的工具之外,个人感觉babel基于抽象语法树的插件机制,给我们提供了更多的可能。关于babel相关概念和插件文档,网上是有很多的,讲的挺不错的。详细的解析推荐官方的babel插件手册。在开发插件之前,有些内容还是要了解一下的,已经熟悉的大佬们可以直接跳过。

抽象语法树(AST)

Babel 使用一个基于 ESTree 并修改过的 AST,它的内核说明文档可以在[这里](https://github. com/babel/babel/blob/master/doc/ast/spec. md)找到。

直接看实例应该更清晰:

function square(n) {
return n * n;
}

对应的AST对象(babel提供的对象格式)

{
//代码块类别,函数声明
type: "FunctionDeclaration",
//变量标识
id: {
type: "Identifier",
//变量名称
name: "square"
},
//参数
params: [{
type: "Identifier",
name: "n"
}],
//函数体
body: {
//块语句
type: "BlockStatement",
body: [{
//return 语句
type: "ReturnStatement",
argument: {
//二元表达式
type: "BinaryExpression",
//操作符
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}

大概就是上面这个层级关系,每一层都被称为节点(Node),一个完整AST对应的js对象可能会有很多节点,视具体情况而定。babel将每个节点都作为一个接口返回。其中包括的属性就如上面代码所示,例如type,start,end,loc等通用属性和具体type对应的私有属性。我们后面插件的处理也是根据不同的type来处理的。

看到这个庞大的js对象,不要感到头疼,如果说让我们每次都自己去分析AST和按照babel的定义去记住不同类型,显然不现实。这种事情应该交给电脑来执行,我们可以利用AST Explorer来将目标代码转成语法树对象,结合AST node types来查看具体属性。

Babel 的处理步骤

Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate),具体过程就不想详细描述了,直接看官方手册就好。

需要注意的是,babel插件就是在转换过程中起作用的,即将解析完成的语法树对象按照自己的目的进行处理,然后再进行代码生成步骤。所以要深入了解转换相关的内容。

代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps),以便于调试。

代码生成的原理:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。转换的时候是是进行递归的树形遍历。

转换

Visitor

转换的时候,是插件开始起作用的时候,但是如何进入到这个过程呢,babel给我们提供了一个Visitor的规范。我们可以通过Visitor来定义我们的访问逻辑。大概就是下面这个样子

const MyVisitor = {
//这里对应上面node的type,所有type为Identifier的节点都会进入该方法中
Identifier() {
console.log("Called!");
}
};
//以该方法为例
function square(n) {
return n * n;
}
//会调用四次,因为
//函数名square
//形参 n
//函数体中的两个n,都是Identifier
path.traverse(MyVisitor);
// 所以输出四个
Called!
Called!
Called!
Called!

因为深度优先的遍历算法,到一个叶子节点之后,发现没有子孙节点,需要向上溯源才能回到上一级继续遍历下个子节点,所以每个节点都会被访问两次。

如果不指定的话,调用都发生在进入节点时,当然也可以在退出时调用访问者方法。

const MyVisitor = {
Identifier: {
enter() {
console.log("Entered!");
},
exit() {
console.log("Exited!");
}
}
};

此外还有一些小技巧:

可以在方法名用|来匹配多种不同的type,使用相同的处理函数。

const MyVisitor = {
"ExportNamedDeclaration|Flow"(path) {}
};

此外可以在访问者中使用别名(如babel-types定义)

例如Function是FunctionDeclaration,FunctionExpression,ArrowFunctionExpression,ObjectMethod和ObjectMethod的别名,可以用它来匹配上述所有类型的type

const MyVisitor = {
Function(path) {}
};

Paths

AST 通常会有许多节点,那么节点直接如何相互关联呢? 我们可以使用一个可操作和访问的巨大可变对象表示节点之间的关联关系,或者也可以用Paths(路径)来简化这件事情。Path 是表示两个节点之间连接的对象。直接看例子比较清晰一点。


{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
...
}

将子节点 Identifier 表示为一个路径(Path)的话,看起来是这样的:

{
"parent": {
"type": "FunctionDeclaration",
"id": {...},
....
},
"node": {
"type": "Identifier",
"name": "square"
}
}

当你通过一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。 通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。

编写插件

前面都是些必备知识点,本文只是将一些相对重要一点的知识点提了一下。详细的还是要去看开发手册的。

个人而言开发插件的话应该有下面三个步骤:

  1. 分析源文件抽象语法树AST
  2. 分析目标文件抽象语法树
  3. 构建Visitor

    3.1 确定访问条件

    3.2 确定转换逻辑

插件主要的就是3步骤,但是前两步是十分重要的。3.1和3.2分别依赖于1和2的结果。只有清晰了解AST结构之后,才能有的放矢,事半功倍。

举个例子,如下代码:

var func = ()=>{
console.log(this.b)
};

目的是将箭头函数转换成普通函数声明(这里仅仅是具体这种格式的转化,其他部分就先不涉及)。如下:

var _this = this;
var func = function () {
console.log(_this.b);
};

源文件语法树

这里分析下这个简单的函数声明,按照上面定义分析,不过这里还是推荐AST Explorer可以清晰的看到我们的语法树。这里只截取有用信息:

        "init": {
"type": "ArrowFunctionExpression",
/*...其他信息....*/
"id": null,
//形参
"params": [],
"body": {
//函数体,this部分
"arguments": [
{
"type": "MemberExpression",
"object": {
//this 表达式
"type": "ThisExpression",
},
"property": {
//b属性
"type": "Identifier",
"name": "b"
}
}
]
}
}

我们要转换的只是ArrowFunctionExpression即箭头函数和this表达式ThisExpression部分,其他暂时不动。

那么我们的visitor里的函数名称就包括ArrowFunctionExpression和ThisExpression了。

//visitor里面方法的key就对应我们要处理的node  type
const visitor = {
//处理this表达式
ThisExpression(path){
//将this转换成_this的形式
},
//处理箭头函数。
ArrowFunctionExpression(path){
//转换成普通的FunctionExpression
}
}

目标文件语法树

同样的方法,语法树对象如下:

语法树太长,我们就看一下变化的地方好了

    //转换之后的body由两个元素的数组,两个变量声明是统计关系
"body": [
//var _this = this;结构
{
"type": "VariableDeclaration",
"kind": "var",
"declarations": [
{
"type": "VariableDeclarator",
//left为_this的标识
"id": {
"type": "Identifier",
"name": "_this"
},
//right为this表达式
"init": {
"type": "ThisExpression"
/***其他**/
}
},
// var func = function (b) {
// console.log(_this.b);
// };结构 只看关键的
{
"type": "VariableDeclaration",
"kind": "var",
"declarations": [
{
/*****省略*******/
"arguments": [
{
"type": "MemberExpression",
//转换之后的_this.b
"object": {
"type": "Identifier",
"name": "_this"
},
"property": {
"type": "Identifier",
"name": "b"
}
]
}
}
]

经过对比,确定我们的操作应该是将ArrowFunctionExpression替换为FunctionExpression,遇到有this表达式的,绑定一下this,并将其转换。

进行替换增加等操作时就要用到path提供的api了:

  • replaceWith(targetObj) 替换
  • findParent() 查找满足条件的父节点
  • insertBefore 插入兄弟节点

    更多请查询文档,这里只列出我们用到的方法。

构造节点

这里将这个操作单独拿出来,toFunctionExpression这个api的说明我始终没找到。。。。可能是我没找对地方FunctionExpression,没办法我去babel源码里找了一遍:

//@src  /babel/packages/babel-types/src/definitions/core.js
defineType("FunctionExpression", {
inherits: "FunctionDeclaration",
//....
}
//又找到 FunctionDeclaration
defineType("FunctionDeclaration", {
//这里才看到参数: id,params,body..
builder: ["id", "params", "body", "generator", "async"],
visitor: ["id", "params", "body", "returnType", "typeParameters"]
//....
}

这样的话才知道入参,如果有清晰的文档,请大家不吝赐教。下面就简单了。

后来又专门找了一下,终于找到对应文档了传送门

完善Visitor

const Visitor = {
//this表达式
ThisExpression(path){
//构建var _this = this
let node = t.VariableDeclaration(
'var',
[
t.VariableDeclarator(
t.Identifier('_this'),
t.Identifier('this')
)
]
),
//构建 _this标识符
str = t.Identifier('_this'),
//查找变量声明的父节点
//这里只是针对例子的,真正转换需要考虑的情况很多
parentPath = path.findParent((path) => path.isVariableDeclaration())
//满足条件
if(parentPath){
//插入
parentPath.insertBefore(node)
path.replaceWith(
str
)
}else{
return
}
},
//处理箭头函数。
ArrowFunctionExpression(path){
var node = path.node
//构造一个t.FunctionExpression节点,将原有path替换掉即可
path.replaceWith(t.FunctionExpression(
node.id,
node.params,
node.body
))
}
}

主体visitor至此算结束了,当然如果是插件的话

//babel调用插件时会将babel-types作为参数传入
export default function({ types: t }) {
return {
visitor:Visitor
}

在本地调试的话,可以分别引入babel-core和babel-types

var babel = require('babel-core');
var t = require('babel-types');
var code = `var func = ()=>{
console.log(this.b)
};`
const result = babel.transform(code, {
plugins: [{
//前面的Visitor
visitor: Visitor
}]
});
//输出转换之后的code
/**
* var _this = this;
* var func = function () {
* console.log(_this.b);
* };
*/
console.log(result.code);

结束语

参考文章

Babel 插件手册

Babel for ES6? And Beyond!

纸上得来终觉浅,原本也认为已经理解了babel的原理和插件机制,没想到连写个小demo都这么费劲。主要还是对相关api不熟悉,不知道如何去构建节点,熟练之后应该会好很多。此文是插件手册的一个简单总结,把自己实现的思路汇总了一下。抛砖引玉,共同进步,另外希望对有需要的同学略有帮助。详见我的博客

开发一个简单的babel插件的更多相关文章

  1. 开发一个简单的chrome插件-解析本地markdown文件

    准备软件环境 1. 软件环境 首先,需要使用到的软件和工具环境如下: 一个最新的chrome浏览器 编辑器vscode 2. 使用的js库 代码高亮库:prismjs https://prismjs. ...

  2. 【UI插件】开发一个简单日历插件(上)

    前言 最近开始整理我们的单页应用框架了,虽然可能比不上MVVM模式的开发效率,也可能没有Backbone框架模块清晰,但是好歹也是自己开发出来 而且也用于了这么多频道的东西,如果没有总结,没有整理,没 ...

  3. 如何开发一个简单的HTML5 Canvas 小游戏

    原文:How to make a simple HTML5 Canvas game 想要快速上手HTML5 Canvas小游戏开发?下面通过一个例子来进行手把手教学.(如果你怀疑我的资历, A Wiz ...

  4. 重新想象 Windows 8 Store Apps (64) - 后台任务: 开发一个简单的后台任务

    [源码下载] 重新想象 Windows 8 Store Apps (64) - 后台任务: 开发一个简单的后台任务 作者:webabcd 介绍重新想象 Windows 8 Store Apps 之 后 ...

  5. 编写一个简单的Jquery插件

    1.实现内容 定义一个简单的jquery插件,alert传递进来的参数 2.插件js文件(jquery.showplugin.js) (function ($) { //定义插件中的方法 var me ...

  6. Cocos2d-x-Lua 开发一个简单的游戏(记数字步进白色块状)

    Cocos2d-x-Lua 开发一个简单的游戏(记数字步进白色块状) 本篇博客来给大家介绍怎样使用Lua这门语言来开发一个简单的小游戏-记数字踩白块. 游戏的流程是这种:在界面上生成5个数1~5字并显 ...

  7. 手把手制作一个简单的IDEA插件(环境搭建Demo篇)

    新建IDEA插件File --> new --> Project--> Intellij PlatForm Plugin-->Next-->填好项目名OK 编写插件新建工 ...

  8. Python开发一个简单的BBS论坛

    项目:开发一个简单的BBS论坛 需求: 整体参考“抽屉新热榜” + “虎嗅网” 实现不同论坛版块 帖子列表展示 帖子评论数.点赞数展示 在线用户展示 允许登录用户发贴.评论.点赞 允许上传文件 帖子可 ...

  9. 实现一个简单的Vue插件

    我们先看官方文档对插件的描述 插件通常会为 Vue 添加全局功能.插件的范围没有限制--一般有下面几种: 1.添加全局方法或者属性,如: vue-custom-element 2.添加全局资源:指令/ ...

随机推荐

  1. 前端常用功能记录(三)—datatables表格初始化

    其实上篇说的也算是jQuery Datatables的初始化,但主要是对某些字段意义的理解.下面记录的是datatables常用的功能的初始化. 数据源 我经常使用的有两种,一种是JavaScript ...

  2. MySQL的replace方法

    mysql中replace函数直接替换mysql数据库中某字段中的特定字符串,不再需要自己写函数去替换,用起来非常的方便,mysql 替换函数replace()Update `table_name` ...

  3. Css设置img属性让图片水平居中/居左/居右的写法

    图片的居中显示css有很多方法,但在很多情况下有的方法无效,无意发现这个系统的官方处理图片居中,居左,居右的css写法,喜欢的朋友可以收藏下哦 图片的居中显示css有很多方法,但在很多情况下有的方法无 ...

  4. Python 模块之shutil模块

    #拷贝文件,可指定长度,fsrc和fdst都是一个文件对象 def copyfileobj(fsrc, fdst, length=16*1024) shutil.copyfileobj(open(&q ...

  5. elasticsearch-dump 迁移es数据 (elasticdump)

    elasticsearch 部分查询语句 # 获取集群的节点列表: curl 'localhost:9200/_cat/nodes?v' # 列出所有索引: curl 'localhost:9200/ ...

  6. Myeclipse/STS 首次在本地部署配置一个Spring MVC 项目 (十二)

    1. 在本地新创建一个文件夹 ,做为项目工作空间; 2. 用 Myeclipse 或 STS 进入该文件夹,该文件夹就成为项目的工作空间: 3. 就要进 窗口-首选项,配置: 环境默认编码: 1> ...

  7. 洛谷 P2089 烤鸡

    看了前面大佬的代码,发现这道题的解题思路都大同小异. 首先肯定要定义一个变量累加方案数量,因为方案数量要最先输出,所以所有方案要先储存下来.个人不喜欢太多数组,就只定义一个字符串. 然后我们发现只有1 ...

  8. Python内置模块与标准库

    Python内置模块就是标准库(模块)吗?或者说Python的自带string模块是内置模块吗? 答案是:string不是内置模块,它是标准库.也就是说Python内置模块和标准库并不是同一种东西. ...

  9. perl6 中将 字符串 转成十六进制

    say Blob.new('abcde'.encode('utf8')).unpack("H*"); say '0x'~'abcde'.encode('utf8').unpack( ...

  10. centos7更改网卡名

    虚拟机中安装centos7,分配两张网卡,安装完成后,使用ip addr 命令查看网卡,发现网卡名称为ens33 和 ens34,不符合平时的使用习惯,想把网卡名改为eth0和eth1,具体操作步骤如 ...