详解 AST 抽象语法树及其应用

发布于 2023-07-19 12:56:05 字数 8611 浏览 80 评论 0

一 AST 是什么?

1 AST:Abstract Syntax Tree - 抽象语法树

当我们查看目前主流的项目中的 devDependencies,会发现各种各样的模块工具。归纳一下有:JavaScript 转译、css 预处理器、elint、pretiier 等等。这些模块我们不会在生产环境用到,但它们在我们的开发过程中充当着重要的角色,而所有的上述工具,都建立在 AST 的基础上。

2 AST 工作流程

  • parse:把代码解析为 AST。
  • transform:对 AST 中的各个节点做相关操作,如新增、删除、替换、追加。业务开发 95%的代码都在这里。
  • generator:把 AST 转换为代码。

3 AST 树预览

AST 辅助开发工具: https://astexplorer.net/

二 从一个简单需求上手

代码压缩的伪需求:将 square 函数参数与引用进行简化,变量由 num 转换为 n:

解法 1:使用 replace 暴力转换

const sourceText = `function square(num) {
sourceText.replace(/num/g, 'n');

以上操作相当的暴力,很容易引起 bug,不能投入使用。如若存在字符串 "num",也将被转换:

// 转换前

解法 2:使用 babel 进行 AST 操作

module.exports = () => {

通过定义 Identifier visitor,对 Identifier(变量) 进行遍历,如果 Identifier 名称为 "num",进行转换。以上代码解决了 num 为字符串时也进行转换的问题,但还存在潜在问题,如代码为如下情况时,将引发错误:

// 转换前

由于 window.num 也会被上述的 visitor 迭代器匹配到而进行转换,转换后出代码为 window.n,进而引发错误。分析需求“将 square 函数参数与引用进行简化,变量由 num 转换为 n”,提炼出的 3 个关键词为 “square 函数、参数、引用”,对此进一步优化代码。

解法 2 升级:找到引用关系

module.exports = () => {

上述的代码,可描述流程为:

转换结果:

// 转换前

在面向业务的 AST 操作中,要抽象出“人”的判断,做出合理的转换。

三 Babel in AST

1 API 总览

// 三剑客

2 @ babel/parser

通过 babel/parser 将源代码转为 AST,简单形象。

const ast = parser(rawSource, {

3 @ babel/traverse

AST 开发的核心,95% 以上的代码量都是通过 @ babel/traverse 在写 visitor。

const ast = parse(`function square(num) {

visitor 的第一个参数是 path,path 不直接等于 node(节点),path 的属性和重要方法组成如下:

4 @ babel/generator

通过 @ babel/generator 将操作过的 AST 生成对应源代码,简单形象。

const output = generate(ast, { /* options */ });

5 @ babel/types

@ babel/types 用于创建 ast 节点,判断 ast 节点,在实际的开发中会经常用到。

// is 开头的用于判断节点

6 @ babel/template

@ bable/types 可以创建 ast 节点,但过于繁琐,通过 @ babel/template 则可以快速创建整段的 ast 节点。下面对比了获得 import React from 'react' ast 节点的两种方式:

// @ babel/types
// 使用 @ babel/template

7 定义通用的 babel plugin

定义通用的 babel plugin,将有利于被 Webpack 集成,示例如下:

// 定义插件
// 配置 babel.config.js

在 babel plugin 开发中,可以说就是在写 ast transform callback,不需要直接接触“@ babel/parser、@ babel/traverse、@ babel/generator”等模块,这在 babel 内部调用了。

在需要用到 @ babel/types 能力时,建议直接使用 @ babel/core,从源码[1]可以看出,@ babel/core 直接透出了上述 babel 模块。

const core = require('@ babel/core');

四 ESLint in AST

在掌握了 AST 核心原理后,自定义 ESlint 规则也变的容易了,直接上代码:

// eslint-plugin-my-eslint-plugin
// .eslintrc.js

体验效果

IDE 正确提示:

执行 eslint 命令的 warning:

查阅更多 ESLint API 可查看官方文档[2]。

五 获得你所需要的 JSX 解释权

第一次接触到 JSX 语法大多是在学习 React 的时候,React 将 JSX 的能力发扬光大[3]。但 JSX 不等于 React,也不是由 React 创造的。

// 使用 react 编写的源码



// 通过 @ babel/preset-react 转换后的代码

JSX 作为标签语法既不是字符串也不是 HTML,是一个 JavaScript 的语法扩展,可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 会使人联想到模版语言,它也具有 JavaScript 的全部功能。下面我们自己写一个 babel plugin,来获得所需要对 JSX 的解释权。

1 JSX Babel Plugin

我们知道,HTML 是描述 Web 页面的语言,axml 或 vxml 是描述小程序页面的语言,不同的容器两者并不兼容。但相同点是,他们都基于 JavaScript 技术栈,那么是否可以通过定义一套 JSX 规范来生成出一样的页面表现?

2 目标

export default (
<!-- 输出 Web HTML -->
<!--输出小程序 axml -->

目前的疑惑在于:AST 仅可用作 JavaScript 的转换,那 HTML 和 axml 等文本标记语言改怎么转换呢?不妨转换一种思路:将上述的 JSX 代码转化为 JS 的代码,在 Web 端和小程序端提供组件消费即可。这是 AST 开发的一个设计思想,AST 工具仅做代码的编译,具体的消费由下层操作,@ babel/preset-react 与 react 就是这个模式。

// jsx 源码

明确了目标后,我们要做的事为:

  1. 将 jsx 标签转为 Object,标签名为 type 属性,如 转化为 { type: 'view' }
  2. 标签上的属性平移到 Object 的属性上,如 <view onTap={e => {}} /> 转换为 { type: 'view', onTap: e => {} }
  3. 将 jsx 内的子元素,移植到 children 属性上,children 属性为数组,如 { type: 'view', style, children: [...] }
  4. 面对子元素,重复前面 3 步的工作。

下面是实现的示例代码:

const { declare } = require('@ babel/helper-plugin-utils');

六 总结

我们介绍了什么是 AST、AST 的工作模式,也体验了利用 AST 所达成的惊艳能力。现在来想想 AST 更多的业务场景是什么?当用户:

  • 需要基于你的基础设施进行二次编程开发的时候
  • 有可视化编程操作的时候
  • 有代码规范定制的时候

AST 将是你强有力的武器。

注:本文演示的代码片段与测试方法在 https://github.com/chvin/learn_ast

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

向日葵

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

13886483628

文章 0 评论 0

流年已逝

文章 0 评论 0

℡寂寞咖啡

文章 0 评论 0

笑看君怀她人

文章 0 评论 0

wkeithbarry

文章 0 评论 0

素手挽清风

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文