前端AST详解,手写babel插件
创始人
2024-04-08 23:47:10

🐱个人主页:不叫猫先生
🙋‍♂️作者简介:专注于前端领域各种技术,热衷分享,关注我会给你带来一些不一样的认知和成长。
📝个人签名:不破不立

🍬本文目录

  • 🥙一、前言
  • 🥪二、节点介绍
  • 🌮三、Babel基础
  • 🍰四、案例展示
  • 🍔五、手写babel插件

🥙一、前言

抽象语法树(Abstract Syntax Tree,AST),是源代码(不仅限于JavaScript,同时还应用于其他语言,例如: Python,Rust等)语法结构的⼀种抽象表示。它以树状的形式表现编程语⾔的语法结构,树上的每个节点都表示源代码中的⼀种结构。
AST 运⽤⼴泛,⽐如:

  • ⾼级语⾔的编译、机器码的⽣成⼀些⾼级编辑器的错误提示、代码⾼亮、代码⾃动补全;
  • 对于前端来说很多⼯具,例如 elint 、 pretiier 对代码错误或⻛格的检查,babel、typescript对代码的编译处理等等。

AST在线预览网站
Bable AST官网

🥪二、节点介绍

本文示范数据:

window.a = 3;
let a = 2, b = 3;
let obj = {name: '张三',age: "18",interest: ["篮球", "羽毛球"],add: function (a, b) {setTimeout(() => {})return a + b + 1000},multiplication: function (a, b) {if (a) {b = a} else {}return a * b + 1000}
}
  • type:标识节点的类型。
  • Identifier(标识符):简单来说就是我们写 JS 时自定义的名称,如变量名,函数名,属性名,都归为标识符,值存放于字段name中。
    在这里插入图片描述
  • CallExpression(函数表达示):比如:setTimeout(()=>{})。callee 属性是一个表达式节点,表示函数,arguments 是一个数组,元素是表达式节点,表示函数参数列表.
    在这里插入图片描述
  • MemberExpression(成员表达式节点):即表示引用对象成员的语句,object是引用对象的表达式节点,property 是表示属性名称,computed 如果为 false,是表示 . 来引用成员,property 应该为一个 Identifier 节点,如果 computed 属性为 true,则是 [] 来进行引用,即 property 是一个Expression 节点,名称是表达式的结果值。window.a对应的AST如下:
    在这里插入图片描述
  • AssignmentExpression(赋值表达式节点):operator 属性表示一个赋值运算符,left 和 right是赋值运算符左右的表达式
  • ArrayExpression(数组表达式节点): interest:[“篮球”,“羽毛球”],elements 属性是一个数组,表示数组的多个元素,每一个元素都是一个表达式节点。
    在这里插入图片描述
  • VariableDeclaration(变量声明表达式):kind 属性表示是什么类型的声明,值可能是var/const/let。declarations表示声明的多个描述,因为我们可以这样:let a = 2,b=3
    在这里插入图片描述
  • VariableDeclarator(变量声明的描述):id 表示变量名称节点,init 表示初始值的表达式,可以为 null
    在这里插入图片描述
  • IfStatement(if表达式):if(true),test 属性表示 if (…) 括号中的表达式。
    • consequent 属性是表示条件为 true 时的执行语句,通常会是一个块语句。
    • alternate 属性则是用来表示 else 后跟随的语句节点,通常也会是块语句,但也可以又是一个 if 语句节点,即类似这样的结构:if (a) { //… } else if (b) { // … }。alternate 当然也可以为 null。

在这里插入图片描述

  • Literals字面量
    • StringLiteral 字符串字面量(“foo”)
    • NumericLiteral 数值字面量(123)
    • BooleanLiteral 布尔字面量 (true)
    • TemplateLiteral 模板字面量 (${obj})

🌮三、Babel基础

Babel 是一个 JavaScript 的转译器,其执行过程就是一个编译转换的过程。作为一个js转译器,babel暴露了很多 api,利用这些 api 可以完成源代码到 AST 的 parse,AST 的遍历与处理以及目标代码的生成。babel将这些功能的实现放到了不同的包里面,下面逐一介绍。

  • @babel/parser 解析源码得到AST
  • @babel/traverse 遍历 AST节点
  • @babel/types 用于构建AST节点和判断AST节点类型
  • @babel/generate 打印 AST,生成目标代码和 sorucemap(即将ast转换成js代码)

babel的处理步骤:主要有三个阶段:解析(parse), 转换 (transform),生成(generate)。

  • parse
    将源码转成 AST,用到@babel/parser模块。

  • transform
    对AST 进行遍历,在此过程中对节点进行添加、更新及移除等操作。因此这是bebel处理代码的核心步骤,是我们的讨论重点,主要使用@babel/traverse@babel/types模块。

  • generate
    打印 AST 成目标代码并生成 sourcemap,用到@babel/generate模块。

接下来我们来重点了解转换这一步,上面我们提到,转换的第一步是遍历AST。说到这里就不得不提到一个设计模式——访问者模式。

访问者模式,即将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式,简单来说,就是定义了用于在一个树状结构中获取具体节点的方法。当访问者把它用于遍历中时,每当在树中遇见一个对应类型时,都会调用该类型对应的方法。

🍰四、案例展示

从 babel7 开始,所有的官方插件和主要模块,都放在了 @babel 的命名空间下。从而可以避免在 npm 仓库中 babel 相关名称被抢注的问题,并且采用了Babel Monorepo风格的仓库。在测试之前需要安装@babel/core@babel/cli@babel/preset-env

yarn add @babel/core @babel/cli -D

@babel/core 是Babel 实现转换的核心,他是依赖能力更底层的 @babel/parser @babel/code-frame@babel/generator@babel/traverse@babel/types等。

  • @babel/parser: 接受源码,进行词法分析、语法分析,生成AST。
  • @babel/traverse:接受一个AST,并对其遍历,根据preset、plugin进行逻辑处理,进行替换、删除、添加节点。
  • @babel/generator:接受最终生成的AST,并将其转换为代码字符串,同时此过程也可以创建source map。
  • @babel/types:用于检验、构建和改变AST树的节点

@babel/cli 是 Babel 提供的命令行,它可以在终端中通过命令行方式运行,编译文件。
@babel/preset-env' Babel 只是一个’编译器’你需要告诉他转换规则,需要在transformer,利用我们配置好的 plugins/presets把 Parser生成的 AST转变为新的 AST,即@babel/preset-env'就是一套转换规则集合。
下图为转换流程let声明转换为var声明
在这里插入图片描述

const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const generator = require('@babel/generator');
const transToLet = code => {const ast = parser.parse(code);// 访问者对象const visitor = {// 遍历声明表达式VariableDeclaration(path) {if (path.node.type === 'VariableDeclaration') {// 替换if (path.node.kind === 'var') {path.node.kind = 'let';}}},};traverse.default(ast, visitor);// 生成代码const newCode = generator.default(ast, {}, code).code;return newCode;
};
const code = `const a = 1
var b = 2
let c = 3`;
console.log(transToLet(code)) 

通过parse解析得到了ast,具体如下:

Node {type: 'File',start: 0,end: 31,loc: SourceLocation {start: Position { line: 1, column: 0, index: 0 },end: Position { line: 3, column: 9, index: 31 },filename: undefined,identifierName: undefined},errors: [],program: Node {type: 'Program',start: 0,end: 31,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},sourceType: 'script',interpreter: null,body: [ [Node], [Node], [Node] ],directives: []},comments: []
}

执行

node babel.js

输出

const a = 1;
let b = 2;
let c = 3;

可见var都变成了let

🍔五、手写babel插件

该插件为superLog,源码如下:

const generator = require('@babel/generator');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const types = require('@babel/types');
const trans = require('./trans.js')
const addNode = code => {const ast = parser.parse(code);// 访问者对象const visitor = {// 遍历调用表达式CallExpression(path) {const { callee } = path.node;if (types.isCallExpression(path.node) && types.isMemberExpression(callee)) {const { object, property } = callee;if (object.name === 'console' && property.name === 'log') { const newArg = trans(path.node.arguments);path.node.arguments = [...newArg];}}},};traverse.default(ast, visitor);// 生成代码const newCode = generator.default(ast, {}, code).code;return newCode;
};

//callee

Node {type: 'MemberExpression',start: 86,end: 97,loc: SourceLocation {start: Position { line: 8, column: 0, index: 86 },end: Position { line: 8, column: 11, index: 97 },filename: undefined,identifierName: undefined},object: Node {type: 'Identifier',start: 86,end: 93,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: 'console'},name: 'console'},computed: false,property: Node {type: 'Identifier',start: 94,end: 97,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: 'log'},name: 'log'}
} 

//path.node.arguments的值

[Node {type: 'Identifier',start: 98,end: 99,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: 'a'},name: 'a'},Node {type: 'MemberExpression',start: 101,end: 108,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},object: Node {type: 'MemberExpression',start: 101,end: 106,loc: [SourceLocation],object: [Node],computed: false,property: [Node]},computed: false,property: Node {type: 'Identifier',start: 107,end: 108,loc: [SourceLocation],name: 'b'}},Node {type: 'CallExpression',start: 110,end: 118,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},callee: Node {type: 'MemberExpression',start: 110,end: 116,loc: [SourceLocation],object: [Node],computed: false,property: [Node]},arguments: []}
]

新建trans,js文件

const types = require('@babel/types');
// 获取父辈节点并拼接
const getNodeName = node => {const getPreValue = node => {if (node.object && node.property) {return `${node.property.name}.${getPreValue(node.object)}`;} else {return node.name;}};return getPreValue(node).split('.').reverse().map((item, index, arr) => (index === arr.length - 1 ? item : `${item}.`)).join('');
};
const actionMap = {// 调用表达式CallExpression: node => getNodeName(node.callee),// 标识符Identifier: node => node.name,// 成员表达式MemberExpression: node => getNodeName(node),// 字符串StringLiteral: node => '',
};
const trans = list => {//初始化一个数组长度为传的参数2倍let res = new Array(list.length * 2).fill(null);list.forEach((node, index) => {res[index * 2 + 1] = node;console.log(node.type,'1111111111111111')console.log(node,'22222222222222222')const strNodeName = actionMap[node.type](node);res[index * 2] = strNodeName ? types.stringLiteral(`${strNodeName}`) : '';});return res;
};
module.exports = trans;

其中node.type分别是:

Identifier
MemberExpression
CallExpression

最后打印结果为

const obj = {a: {b: 'xiaom'},fn: () => null
};
const a = 2;
console.log("a", a, "obj.a.b", obj.a.b, "obj.fn", obj.fn());

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...