30分钟入门babel插件

什么是Babel?

Babel 是一个Javascript转译器,主要作用是将 ECMAScript 的高版本语法(例如 ES6)转换成低版本语法(例如 ES5),以便能够运行在当前和旧版本的浏览器或其他环境中。

Babel主要有以下几个方面的用途:

  • 语法转换(转译esnext、typescript、jsx等)
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js
  • 代码转换(代码插桩,例如自动化埋点、变量替换等)
  • 代码静态分析(各类linter工具、api文档生成、ts类型检查等)

Babel的编译流程简介

Babel编译是一个code to code的过程,整个流程分为三步:

Babel编译流程

  • 解析(parse):将源码解析成抽象语法树(AST)
  • 转换(transform):遍历AST,并使用babel api对AST节点进行增删改
  • 生成(generate):将转换后的 AST 转换成代码,同时可以创建Source Map映射。

Babel AST

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。 — 来源百度百科

由于Babel的整个编译流程都是围绕着AST进行的,所以有必要了解一下AST的常见节点类型,需要强调的是,节点类型不需要死记硬背,需要的时候可以通过AST可视化平台查看。

AST可视化平台

这里对AST平台做简单的介绍,将Transform的开关打开可以看到平台分成了四个区域,对应了babel编译流程几个阶段的结果。

  • 左上角区域:编写源代码
  • 右上角区域:源代码经过parse得到的AST
  • 左下角区域:编写babel插件,可以干预transform阶段,对AST节点增删改
  • 右下角区域:展示转换后的 AST 转换成的代码
    AST可视化平台
Identifier

Identifier表示代码中的标识符,如变量名、属性名、参数名,以及声明和引用

通过AST可视化平台可以方便的测试出代码中的哪些为Identifier类型。测试地址
在这里插入图片描述

Literals

Literals表示代码中的字面量类型,字面量包含数字、大整数、字符串、模版字符串、布尔值、null、正则表达式等

通过AST可视化平台测试代码中的哪些为Literals类型。测试地址
在这里插入图片描述

ps:JS中undefined也是一个字面量,但是在AST中并不是用Literals类型表示,因为它不像其他字面量那样有明确的值,而是一个标识符,它的值是未定义的,所以它的类型是Identifie。

Statements

Statements表示代码中的各类语句,如流程控制语句(while语句、for语句等)、条件语句(if语句、switch语句)、表达式语句、异常处理语句(try…catch…)。break、continue、return这类可以独立执行的代码都属于语句。

通过AST可视化平台测试代码中的哪些为Statements类型。测试地址
在这里插入图片描述
ps:声明语句不属于Statements,因为声明语句不会执行任何操作,而只是将变量、函数或类的定义放在程序中,以便被其他部分引用。

Declarations

Declarations表示代码中的声明语句,如变量声明、函数声明、import、export等

通过AST可视化平台测试代码中的哪些为Declarations类型。测试地址
在这里插入图片描述

Expressions

Expressions表示代码中的表达式,指那些执行完有结果的语句(区别于Statements),如表达式,函数调用,数组,对象等

通过AST可视化平台测试代码中的哪些为Expressions类型。测试地址
在这里插入图片描述

想了解全部的Babel AST可以在babel仓库的AST文档里查看

Babel提供的APi

Babel在编译流程的三步中,每一步都暴露了一些api,所有api可在babel api文档里查看。

  • parse阶段提供了@babel/parse,包含源码转换成AST的方法
  • transform阶段提供了@babel/traverse,包含遍历AST和对AST节点增删改的方法
  • generate阶段提供了@babel/generator,包含将AST转换成代码并生成source map的方法

实现babel插件主要关注transform阶段,所以接下来主要针对这个阶段介绍相关的api

AST的遍历过程

babel traverse提供了traverse方法function traverse(ast, options),ast指定要遍历的节点,options里制定visitor函数。visitor函数以AST的类型命名,babel在遍历AST的过程中会在每个节点调用对应类型的visitor函数。一个visitor函数也可以同时用于多种AST节点的处理,只要将AST节点类型用|连接。
在这里插入图片描述
babel在遍历AST的时候会经过每个AST节点两次,分别为进入(enter)和离开(exit)两个阶段,可以对一个节点类型的两个阶段分别指定回调函数,如果只指定了一个函数那就是在enter阶段调用。
在这里插入图片描述

visitor函数path参数

AST是颗树状结构,path用于记录一个节点路径。它可以用来构建从根节点到当前节点的路径。
在这里插入图片描述

如图,将一颗4层的AST局部放大,可以看到4个AST节点通过三个path关联起来,node记录了AST节点信息。
其中,

  • path1关联了node0和node1,并可以通过get方法关联到path2和path3
  • path2关联了node1和node2,并可以通过parentPath关联到path1、通过getNextSibling方法关联到path3
  • path3关联了node1和node3,并可以通过parentPath关联到path1、通过getPrevSibling方法关联到path2

通过这样的关联关系就可以方便的串起整颗AST,遍历AST。
在这里插入图片描述
从上图可以看出,path 对象上提供了很多属性和方法,常用的可以分为以下几种类型:
获取节点信息、父子、兄弟节点path相关

  • path.node 指向 AST 节点
  • path.parent指向父级 AST 节点
  • path.getPrevSibling获取上一个兄弟节点对应的path
  • path.getNextSibling获取下一个兄弟节点对应的path
  • path.find从当前节点向祖先节点查找节点对应的path
  • path.get获取属性的path,可获取到子节点对应的path
  • path.set设置属性的path
  • path.scope获取当前节点的作用域信息

增删改AST相关

  • path.insertBefore在节点之前插入节点
  • path.insertAfter在节点之后插入节点
  • path.replaceWith将节点替换另一个节点
  • path.replaceWithMultiple将当前节点替换多个节点
  • replaceWithSourceString以源代码的形式替换节点
  • path.remove删除节点

判断AST类型相关

  • path.isXXX 判断节点是不是XXX类型
  • path.assertXXX 判断节点是不是XXX类型,不是则抛出异常

遍历节点相关

  • path.traverse 从当前节点开始遍历
  • path.skip 跳过当前节点的子节点的遍历
  • path.stop 结束后续节点遍历
visitor函数state参数

state参数用于不同节点间传递数据,同时包含了插件的options和file信息

  • state.set记录数据
  • state.get获取数据
  • state.opts插件入参的options参数
  • state.cwd当前插件运行所处当前工作目录
  • state.file指向当前运行的文件
其他AST相关API

Babel在transform阶段为了化简AST的创建逻辑还提供了@babel/types@babel/template

  • @babel/types提供判断AST类型和创建AST节点的方法
  • @babel/template提供批量创建AST的方法

Babel插件实战:实现一个简单的DefinePlugin

DefinePlugin是一个常见的Webpack插件,他允许在编译时将你代码中的变量替换为其他值或表达式。我们可以通过babel插件实现相同的能力。
根据webpack官网的描述,传递给 DefinePlugin 的每个键都是一个标识符或多个以.连接的标识符。

  • 如果该值为字符串,它将被作为代码片段来使用。
  • 如果该值不是字符串,则将被转成字符串。
  • 如果值是一个对象,则它所有的键将使用相同方法定义。
  • 如果键添加 typeof 作为前缀,它会被定义为 typeof 调用。

DefinePlugin对值做了比较多的处理,而这篇文章目的是学习babel插件写法,所以仅对值做简单的toString处理。

const DEFINE = {
  	PRODUCTION: JSON.stringify(true),
    VERSION: JSON.stringify('5fa3b9'),
    BROWSER_SUPPORTS_HTML5: true,
    TWO: '1+1',
    'typeof window': JSON.stringify('object'),
    'process.env.NODE_ENV': JSON.stringify("pre"),
}
区分目标AST类型

根据DefinePlugin的描述,键主要分为三种类型,我们需要找出每种类型对应的AST节点类型,不需要查文档,在AST可视化平台简单测试一下即可得到结果:
【普通标识符】为Identifier类型
【多个以.连接的标识符】为MemberExpression类型
【以typeof操作符为前缀】为UnaryExpression类型

匹配目标AST

匹配普通标识符
在这里插入图片描述

对于普通标识符,观察其AST可以发现Identifier类型节点上有个name字段记录了标识符字符串,所以可以通过这个值于DefinePlugin入参的键做匹配,vistor函数如下

{
  Identifier(path) {
      if (DEFINE[path.node.name]) {
        // todo
      }
  }
}

匹配多个以.连接的标识符
在这里插入图片描述

可以看到多个以.连接的标识符的AST相对比较复杂,一种思路是通过对MemberExpression类型往下递归匹配,这种方式实现和理解成本相对较高。我们还有一种更简单的匹配方法,可以将MemberExpression类型AST generate成字符串code,这样就可以通过字符串与键做匹配,vistor函数如下

{
  MemberExpression(path) {
    if (DEFINE[path.toString()) {
    	// todo
    }
  }
}

ps:path的toString方法就相当于将AST generate成字符串

匹配以typeof操作符为前缀的标识符
在这里插入图片描述
可以看到UnaryExpression类型 AST节点上有个operator属性可以判断操作符类型,当operator属性值为’typeof’,就可以将AST 转成字符串code与键匹配,vistor函数如下

{
	UnaryExpression(path) {
    if (path.node.operator === 'typeof' && DEFINE[path.toString()) {
      // todo
    }
  }
}
修改目标AST

修改AST我们也需要实现符合AST节点结构的对象,手动实现是比较麻烦的,可以借助上面提到的@babel/types来创建AST节点,代码如下

const t = require('@babel/types');
const replace = (path, key) => {
  const value = DEFINE[key];
  if (value) {
    path.replaceWith(t.Identifier(value.toString())); 
  }
}

完整示例
在这里插入图片描述

相关资料

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值