带你揭开神秘的Javascript AST面纱之Babel AST 四件套的使用方法

这篇具有很好参考价值的文章主要介绍了带你揭开神秘的Javascript AST面纱之Babel AST 四件套的使用方法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

作者:京东零售 周明亮

写在前面

这里我们初步提到了一些基础概念和应用:

  • 分析器
  • 抽象语法树 AST
  • AST 在 JS 中的用途
  • AST 的应用实践

有了初步的认识,还有常规的代码改造应用实践,现在我们来详细说说使用 AST, 如何进行代码改造?

Babel AST 四件套的使用方法

其实在解析 AST 这个工具上,有很多可以使用,上文我们已经提到过了。对于 JS 的 AST 大家已经形成了统一的规范命名,唯一不同的可能是,不同工具提供的详细程度不一样,有的可能会额外提供额外方法或者属性。

所以,在选择工具上,大家按照各自喜欢选择即可,这里我们选择了babel这个老朋友。

初识 Babel

我相信在这个前端框架频出的时代,应该都知道babel的存在。 如果你还没听说过babel,那么我们通过它的相关文档,继续深入学习一下。

因为,它在任何框架里面,我们都能看到它的影子。

  • Babel JS 官网
  • Babel JS Github

作为使用最广泛的 JS 编译器,他可以用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

而它能够做到向下兼容或者代码转换,就是基于代码解析和改造。接下来,我们来说说:如何使用@babel/core里面的核心四件套:@babel/parser、@babel/traverse、@babel/types及@babel/generator。

1. @babel/parser

@babel/parser 核心代码解析器,通过它进行词法分析及语法分析过程,最终转换为我们提到的 AST 形式。

假设我们需要读取React中index.tsx文件中代码内容,我们可以使用如下代码:

const { parse } = require("@babel/parser")

// 读取文件内容
const fileBuffer = fs.readFileSync('./code/app/index.tsx', 'utf8');
// 转换字节 Buffer
const fileCode = fileBuffer.toString();
// 解析内容转换为 AST 对象
const codeAST = parse(fileCode, {
  // parse in strict mode and allow module declarations
  sourceType: "module",
  plugins: [
    // enable jsx and typescript syntax
    "jsx",
    "typescript",
  ],
});

当然我不仅仅只读取React代码,我们甚至可以读取Vue语法。它也有对应的语法分析器,比如:@vue/compiler-dom。

此外,通过不同的参数传入 options,我们可以解析各种各样的代码。如果,我们只是读取普通的.js文件,我们可以不使用任何插件属性即可。

const codeAST = parse(fileCode, {
  // parse in strict mode and allow module declarations
  sourceType: "module"
});

通过上述的代码转换,我们就可以得到一个标准的 AST 对象。在上一篇文章中,已做详细分析,在这里不在展开。比如:

// 原代码
const me = "我"
function write() {
  console.log("文章")
}

// 转换后的 AST 对象
const codeAST = {
  "type": "File",
  "errors": [],
  "program": {
    "type": "Program",
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {
              "type": "Identifier",
              "name": "me"
            },
            "init": {
              "type": "StringLiteral",
              "extra": {
                "rawValue": "我",
                "raw": "\"我\""
              },
              "value": "我"
            }
          }
        ],
        "kind": "const"
      },
      {
        "type": "FunctionDeclaration",
        "id": {
          "type": "Identifier",
          "name": "write"
        },
        "generator": false,
        "async": false,
        "params": [],
        "body": {
          "type": "BlockStatement",
          "body": [
            {
              "type": "ExpressionStatement",
              "expression": {
                "type": "CallExpression",
                "callee": {
                  "type": "MemberExpression",
                  "object": {
                    "type": "Identifier",
                    "computed": false,
                    "property": {
                      "type": "Identifier",
                      "name": "log"
                    }
                  },
                  "arguments": [
                    {
                      "type": "StringLiteral",
                      "extra": {
                        "rawValue": "文章",
                        "raw": "\"文章\""
                      },
                      "value": "文章"
                    }
                  ]
                }
              }
            }
          ]
        }
      }
    ]
  }
}

2. @babel/traverse

当我们拿到一个标准的 AST 对象后,我们要操作它,那肯定是需要进行树结构遍历。这时候,我们就会用到 @babel/traverse 。

比如我们得到 AST 后,我们可以进行遍历操作:

const { default: traverse } = require('@babel/traverse');

// 进入结点
const onEnter = pt => {
   // 进入当前结点操作
   console.log(pt)
}
// 退出结点
const onExit = pe => {
  // 退出当前结点操作
}
traverse(codeAST, { enter: onEnter, exit: onExit })

那么我们访问的第一个结点,打印出pt的值,是怎样的呢?

// 已省略部分无效值
<ref *1> NodePath {
  contexts: [
    TraversalContext {
      queue: [Array],
      priorityQueue: [],
      ...
    }
  ],
  state: undefined,
  opts: {
    enter: [ [Function: onStartVist] ],
    exit: [ [Function: onEndVist] ],
    _exploded: true,
    _verified: true
  },
  _traverseFlags: 0,
  skipKeys: null,
  parentPath: null,
  container: Node {
    type: 'File',
    errors: [],
    program: Node {
      type: 'Program',
      sourceType: 'module',
      interpreter: null,
      body: [Array],
      directives: []
    },
    comments: []
  },
  listKey: undefined,
  key: 'program',
  node: Node {
    type: 'Program',
    sourceType: 'module',
    interpreter: null,
    body: [ [Node], [Node] ],
    directives: []
  },
  type: 'Program',
  parent: Node {
    type: 'File',
    errors: [],
    program: Node {
      type: 'Program',
      sourceType: 'module',
      interpreter: null,
      body: [Array],
      directives: []
    },
    comments: []
  },
  hub: undefined,
  data: null,
  context: TraversalContext {
    queue: [ [Circular *1] ],
    priorityQueue: [],
    ...
  },
  scope: Scope {
    uid: 0,
    path: [Circular *1],
    block: Node {
      type: 'Program',
      sourceType: 'module',
      interpreter: null,
      body: [Array],
      directives: []
    },
    ...
  }
}

是不是发现,这一个遍历怎么这么多东西?太长了,那么我们进行省略,只看关键部分:

// 第1次
<ref *1> NodePath {
  listKey: undefined,
  key: 'program',
  node: Node {
    type: 'Program',
    sourceType: 'module',
    interpreter: null,
    body: [ [Node], [Node] ],
    directives: []
  },
  type: 'Program',
}

我们可以看出是直接进入到了程序program结点。 对应的 AST 结点信息:

  program: {
    type: 'Program',
    sourceType: 'module',
    interpreter: null,
    body: [
      [Node]
      [Node]
    ],
  },

接下来,我们继续打印输出的结点信息,我们可以看出它访问的是program.body结点。

// 第2次
<ref *2> NodePath {
  listKey: 'body',
  key: 0,
  node: Node {
    type: 'VariableDeclaration',
    declarations: [ [Node] ],
    kind: 'const'
  },
  type: 'VariableDeclaration',
}

// 第3次
<ref *1> NodePath {
  listKey: 'declarations',
  key: 0,
  node: Node {
    type: 'VariableDeclarator',
    id: Node {
      type: 'Identifier',
      name: 'me'
    },
    init: Node {
      type: 'StringLiteral',
      extra: [Object],
      value: '我'
    }
  },
  type: 'VariableDeclarator',
}

// 第4次
<ref *1> NodePath {
  listKey: undefined,
  key: 'id',
  node: Node {
    type: 'Identifier',
    name: 'me'
  },
  type: 'Identifier',
}

// 第5次
<ref *1> NodePath {
  listKey: undefined,
  key: 'init',
  node: Node {
    type: 'StringLiteral',
    extra: { rawValue: '我', raw: "'我'" },
    value: '我'
  },
  type: 'StringLiteral',
}

  • node当前结点
  • parentPath父结点路径
  • scope作用域
  • parent父结点
  • type当前结点类型

现在我们可以看出这个访问的规律了,他会一直找当前结点node属性,然后进行层层访问其内容,直到将 AST 的所有结点遍历完成。

这里一定要区分NodePath和Node两种类型,比如上面:pt是属于NodePath类型,pt.node才是Node类型。

其次,我们看到提供的方法除了进入 [enter]还有退出 [exit]方法,这也就意味着,每次遍历一次结点信息,也会退出当前结点。这样,我们就有两次机会获得所有的结点信息。

当我们遍历结束,如果找不到对应的结点信息,我们还可以进行额外的操作,进行代码结点补充操作。结点完整访问流程如下:

  • 进入>Program
    • 进入>node.body[0]
      • 进入>node.declarations[0]
        • 进入>node.id
        • 退出<node.id
        • 进入>node.init
        • 退出<node.init
      • 退出<node.declarations[0]
    • 退出<node.body[0]
    • 进入>node.body[1]
      • ...
      • ...
    • 退出<node.body[1]
  • 退出<Program

3. @babel/types

有了前面的铺垫,我们通过解析,获得了相关的 AST 对象。通过不断遍历,我们拿到了相关的结点,这时候我们就可以开始改造了。@babel/types 就提供了一系列的判断方法,以及将普通对象转换为 AST 结点的方法。

比如,我们想把代码转换为:

// 改造前代码
const me = "我"
function write() {
  console.log("文章")
}

// 改造后的代码
let you = "你"
function write() {
  console.log("文章")
}

首先,我们要分析下,这个代码改了哪些内容?

  1. 变量声明从const改为let
  2. 变量名从me改为you
  3. 变量值从"我"改为"你"

那么我们有两种替换方式:

  • 方案一:整体替换,相当于把program.body[0]整个结点进行替换为新的结点。
  • 方案二:局部替换,相当于逐个结点替换结点内容,即:program.body.kind,program.body[0].declarations[0].id,program.body[0].declarations[0].init。

借助@babel/types我们可以这么操作,一起看看区别:

const bbt = require('@babel/types');
const { default: traverse } = require('@babel/traverse');

// 进入结点
const onEnter = p => {
  // 方案一,全结点替换
  if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') {
    // 直接替换为新的结点
    p.replaceWith(
      bbt.variableDeclaration('let', [
        bbt.variableDeclarator(bbt.identifier('you'),           
        bbt.stringLiteral('你')),
      ]),
    );
  }
  // 方案二,单结点逐一替换
  if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') {
    // 替换声明变量方式
    p.node.kind = 'let';
  }
  if (bbt.isIdentifier(p.node) && p.node.name == 'me') {
    // 替换变量名
    p.node.name = 'you';
  }
  if (bbt.isStringLiteral(p.node) && p.node.value == '我') {
    // 替换字符串内容
    p.node.value = '你';
  }  
};
traverse(codeAST, { enter: onEnter });

我们发现,不仅可以进行整体结点替换,也可以替换属性的值,都能达到预期效果。

当然 我们不仅仅可以全部遍历,我们也可以只遍历某些属性,比如VariableDeclaration,我们就可以这样进行定义:

traverse(codeAST, { 
  VariableDeclaration: function(p) {
    // 只操作类型为 VariableDeclaration 的结点
    p.node.kind = 'let';
  }
});

@babel/types提供大量的方法供使用,可以通过官网查看。对于@babel/traverse返回的可用方法,可以查看 ts 定义:
babel__traverse/index.d.ts 文件。

常用的方法:p.stop()可以提前终止内容遍历, 还有其他的增删改查方法,可以自己慢慢摸索使用!它就是一个树结构,我们可以操作它的兄弟结点,父节点,子结点。

4. @babel/generator

完成改造以后,我们需要把 AST 再转换回去,这时候我们就需要用到 @babel/generator 工具。只拆不组装,那是二哈【狗头】。能装能组,才是一个完整工程师该干的事情。

废话不多说,上代码:

const fs = require('fs-extra');
const { default: generate } = require('@babel/generator');

// 生成代码实例
const codeIns = generate(codeAST, { retainLines: true, jsescOption: { minimal: true } });

// 写入文件内容
fs.writeFileSync('./code/app/index.js', codeIns.code);

配置项比较多,大家可以参考具体的说明,按照实际需求进行配置。

这里特别提一下:jsescOption: { minimal: true }这个属性,主要是用来保留中文内容,防止被转为unicode形式。

Babel AST 实践

嘿嘿~ 都到这里了,大家应该已经能够上手操作了吧!

什么?还不会,那再把 1 ~ 4 的步骤再看一遍。慢慢尝试,慢慢修改,当你发现其中的乐趣时,这个 AST 的改造也就简单了,并不是什么难事。

留个课后练习:

// 改造前代码
const me = "我"
function write() {
  console.log("文章")
}

// 改造后的代码
const you = "你"
function write() {
  console.log("文章")
}
console.log(you, write())

大家可以去尝试下,怎么操作简单的 AST 实现代码改造!写文章不易,大家记得一键三连哈~

AST 应用是非常广泛,再来回忆下,这个 AST 可以干嘛?

  1. 代码转换领域,如:ES6 转 ES5, typescript 转 js,Taro 转多端编译,CSS预处理器等等。
  2. 模版编译领域,如:React JSX 语法,Vue 模版语法 等等。
  3. 代码预处理领域,如:代码语法检查(ESLint),代码格式化(Prettier),代码混淆/压缩(uglifyjs) 等等
  4. 低代码搭建平台,拖拽组件,直接通过 AST 改造生成后的代码进行运行。

下一期预告

《带你揭开神秘的Javascript AST面纱之手写一个简单的 Javascript 编译器》文章来源地址https://www.toymoban.com/news/detail-411488.html

到了这里,关于带你揭开神秘的Javascript AST面纱之Babel AST 四件套的使用方法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 揭开神秘面纱,会stream流就会大数据

    目录 准备工作 1.map类 1.1 java stream map 1.2 spark map 1.2.1 MapFunction 1.2.2 MapPartitionsFunction 2.flatMap类 2.1 java stream flatMap 2.2 spark flatMap 3 groupby类 3.1 java stream groupBy 3.2 spark groupBy 3.3 spark groupByKey 4 reduce类 4.1 java stream reduce 4.2 spark reduce 其它常见操作类 小结 如果你会任意一门语言的stream流

    2023年04月27日
    浏览(48)
  • 【Linux】揭开套接字编程的神秘面纱(上)

    ​🌠 作者:@阿亮joy. 🎆 专栏: 《学会Linux》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 源IP地址和目的IP地址 源 IP 地址指发送方的 IP 地址,而目的 IP 地址是指接收方的 IP 地址,源 IP 地址和目的

    2023年04月09日
    浏览(103)
  • 06-揭开神秘面纱:Golang method的魅力解析

    📃个人主页:个人主页 🔥系列专栏:Golang基础 💬Go(又称Golang)是由Google开发的开源编程语言。它结合了静态类型的安全性和动态语言的灵活性,拥有高效的并发编程能力和简洁的语法。Go被设计用于构建可扩展、高性能的软件系统,具有优秀的内存管理和快速的编译速度

    2024年02月09日
    浏览(41)
  • 面试篇-揭开Spring Bean加载的神秘面纱

      启动spring容器(创建beanfactory)-加载配置(注解、xml)-实例化bean(执行构造方法)-注入依赖-初始化bean(设置属性值)-使用-销毁 解析和读取 XML 配置文件或注解配置类,获取 Bean 定义信息。 根据 Bean 定义信息实例化 Bean 对象。根据不同的作用域(如 singleton、prototype 等),S

    2023年04月17日
    浏览(47)
  • 华为鸿蒙系统:揭开全新操作系统的神秘面纱

    在全球科技竞争日益激烈的背景下,我国科技巨头华为公司自主研发了一款全新的操作系统——鸿蒙。这款操作系统旨在为华为自家设备提供强大的性能、安全性和智能化特性,以满足用户不断增长的需求。为了帮助广大开发者充分挖掘鸿蒙系统的巨大潜力,华为发布了一本

    2024年02月04日
    浏览(45)
  • 揭开黑客的神秘面纱:黑客文化、技术手段与防御策略

    黑客一词源于英文单词\\\"hacker\\\",它在过去的几十年中经历了很多变化和演变。黑客的定义因其不断变化的含义而变得复杂,但总体上,黑客是指那些具有出色计算机技术与知识的人。他们利用这些技能来探索、发现和改进计算机系统的弱点与缺陷。然而,黑客行为并不一定都

    2024年02月08日
    浏览(61)
  • 1. HBase中文学习手册之揭开Hbase的神秘面纱

    1.1.1 什么是 Hbase? Apache HBase 是 Hadoop 数据库,一种分布式,可扩展的大数据存储。 1.1.2 Hbase的前世今生 Apache HBase 是一个开源、分布式、版本化、非关系数据库, 模仿了 Google 的Bigtable: Chang等人的结构化数据分布式存储系统。 Apache HBase 在 Hadoop 和 HDFS 之上提供类似 Bigtable 的功

    2024年02月12日
    浏览(43)
  • 逍遥自在学C语言 | 揭开while循环的神秘面纱

    循环是一种重要的控制结构,可以使程序重复执行一段代码,直到满足特定条件为止。 在C语言中,while和do-while是两种常用的循环结构,本文将详细介绍这两种循环的用法。 第一位闪亮登场,有请今后会一直教我们C语言的老师 —— 自在。 第二位上场的是和我们一起学习的

    2024年02月06日
    浏览(60)
  • 解密Vue 3:透过原理看框架,揭开它的神秘面纱

    Vue 3 是一种用于 构建用户界面的现代 JavaScript 框架 。它基于 响应式编程 和 虚拟 DOM 技术 ,并通过 组件化 的方式来实现可重用的 UI 组件。 下面是 Vue 3 的主要原理: Vue 3 的核心是其响应式系统,它用于 追踪数据变化并使其自动更新 。Vue 3 使用 ES6 的 Proxy 对象来 实现监听

    2024年02月12日
    浏览(43)
  • AI辅写疑似度检测PaperPass:揭开神秘面纱,探索无尽可能

    大家好,小发猫降ai今天来聊聊AI辅写疑似度检测paperbert:揭开神秘面纱,探索无尽可能,希望能给大家提供一点参考。降ai辅写 以下是针对论文AI辅写率高的情况,提供一些修改建议和技巧,可以借助此类工具: 还有: AI辅写疑似度检测paperbert:揭开神秘面纱,探索无尽可能

    2024年03月08日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包