写点什么

前端 AST 详解,手写 babel 插件

作者:不叫猫先生
  • 2023-06-07
    北京
  • 本文字数:5090 字

    阅读完需:约 17 分钟

前端AST详解,手写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 = 1var b = 2let 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 分别是:


IdentifierMemberExpressionCallExpression
复制代码


最后打印结果为


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


发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2022-10-18 加入

前端领域优质创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀!

评论

发布
暂无评论
前端AST详解,手写babel插件_6 月优质更文活动_不叫猫先生_InfoQ写作社区