AST抽象语法树和应用案例实践

介绍

AST概述

抽象语法树(AST)是JavaScript编译器处理代码创建的一种树形结构。是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

  1. 它有什么用?
    更好的理解代码结构,使用代码进行应用开发,写个页面,写个业务功能,可能并不需要了解AST,但是一旦想用代码表达代码,用代码操作代码,就绕不开抽象语法树。

    比如:代码转换:

    这是一个原生的小程序,转换成uniapp的代码工具,uniapp出来之后,很多用原生小程序写法写的代码,转换成uniapp,然后再编译到其他多个小程序或者APP。

    原生的小程序写法和uniapp的写法还是有差别的,具体的包括:
    咱们以微信小程序为例:(其他都是模仿,除了差不多的语法,其他就是不同的平台特殊的功能API差异了,比如抖音会有商城,拍视频,抖音群等等,百度小程序有搜索,支付宝的支付交易组件)

    包括WXML和WXS,对应uniapp或者说vue,的template和css

    还有比较典型的是给data里面的数据赋值,用setData这种。

    所以想要完成这样一个转换工具,需要用一个东西,来描述原生的小程序语言,对于前端来说,最熟悉的就是json格式,然后去操作这套json,让它可以描述成uniapp,不就可以转换成uniapp了吗。

    所以,我们看下这个转换工具的代码,确实是这样的逻辑。

    miniprogram-to-uniapp/

    我们看他的package.json文件,这里有一个库,叫gogocode,这个就是市面上的其中一种AST操作工具库。

    这里面,核心的逻辑,就是通过AST操作工具库,把原生小程序写的文件内容,包括:静态文件,云函数,wxml里面的模板,生命周期等等,都转换成抽象语法树的json,然后去操作它,比如用正则表达式去替换标签,然后再拼接成uniapp支持的文件。

应用场景介绍

  1. 代码转换:就行上面说的,把一种代码转换成另一种代码。
  2. 代码格式化:使用AST来重新格式化代码,实现代码的统一风格和格式。比如咱们常用的prettier。
  3. 代码优化:可以更容易的分析代码结构和含义,对代码进行审查,或者重构。找出代码中的性能瓶颈并进行优化。批量去除代码里面的console.log,也是使用了AST。
  4. 代码安全:利用AST检查代码中的安全漏洞和不符合安全规范的地方。

常用的工具

常见的AST节点

  1. 字面量(Literal):固定的值
    1. 比如:let name = 'fengjiaheng',其中这个'fengjiaheng'就是一个字符串字面量,其他的还有数字字面量(NumericLiteral)、布尔字面量(BooleanLiteral)、正则表达式字面量(RegExpLiteral)、BigInt字面量(BigintLiteral)、null字面量(NullLiteral)等等。
  2. 标识符(Identifier):定义的符号

    1. 变量名、属性名、参数名等,所有声明和引用的名字,都是标识符。我们都知道,JS 中的标识符只能包含字母或数字或下划线(“_”)或美元符号(“$”),且不能以数字开头。这也是 Identifier 的词法特点。
      比如下面这段代码,一共有多少标识符呢?
      1
      2
      3
      4
      5
      6
      7
      8
      9
      const name = 'feng';

      function say(name) {
      console.log(name);
      }

      const obj = {
      name: 'feng'
      }

    答案是8个。name/say/name/console/log/name/obj/name

  3. 语句(Statement):

    statement 是语句,它是可以独立执行的单位,比如 breakcontinuedebuggerreturn或者 if 语句while 语句for 语句,还有声明语句表达式语句等。我们写的每一条可以独立执行的代码都是语句。

    语句末尾一般会加一个分号分隔,或者用换行分隔。

    下面这些我们经常写的代码,每一行都是一个 Statement:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    break;
    continue;
    return;
    debugger;
    throw Error();
    {}
    try {} catch(e) {} finally{}
    for (let key in obj) {}
    for (let i = 0;i < 10;i ++) {}
    while (true) {}
    do {} while (true)
    switch (v){case 1: break;default:;}
    label: console.log();
    with (a){}

语句是代码执行的最小单位,可以说,代码是由语句(Statement)构成的。

  1. 声明语句(Declaration)
    声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量、函数、class、import、export 等。

    比如下面这些语句都是声明语句:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const a = 1;
    function b(){}
    class C {}

    import d from 'e';

    export default e = 1;
    export {e};
    export * from 'e';

  1. 表达式(Expression)

    1. expression 是表达式,特点是执行完以后有返回值,这是和语句 (statement) 的区别。下面是一些常见的表达式:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      [1,2,3]
      a = 1
      1 + 2;
      -1;
      function(){};
      () => {};
      class{};
      a;
      this;
      super;
      a::b;

  2. 类(Class)
    class 的语法也有专门的 AST 节点来表示。

    整个 class 的内容是 ClassBody,属性是 ClassProperty,方法是ClassMethod(通过 kind 属性来区分是 constructor 还是 method)。

1
2
3
4
5
class Guang extends Person{
name = 'guang';
constructor() {}
eat() {}
}

  1. 模块(Modules): es module 是语法级别的模块规范,所以也有专门的 AST 节点。
    1. import
      1. name import:import {c, d} from 'c';
      2. default import:import a from 'a';
      3. namespaced import: import * as b from 'b';
    2. export
      1. name export:export { a, d }; 需要 xxx 中导出 a、b
      2. default export: export default a; 需要 xxx 中导出 default
      3. all export:export * from './xxx' 会把 xxx 中所有的非default导出
  2. 程序体(Program & Directive)

    program 是代表整个程序的节点,它有 body 属性代表程序体,存放 statement 数组,就是具体执行的语句的集合。还有 directives 属性,存放 Directive 节点,比如”use strict” 这种指令会使用 Directive 节点表示。

  1. AST 最外层节点是 File,它有 program、comments、tokens 等属性,分别存放 Program 程序体、注释、token 等,是最外层节点。

注释分为块注释和行内注释,对应 CommentBlockCommentLine 节点。

AST的公共属性

每种 AST 都有自己的属性,但是它们也有一些公共的属性:

  1. type: AST 节点的类型

  2. start、end、locstartend 代表该节点在源码中的开始和结束下标。而 loc 属性是一个对象,有 linecolumn 属性分别记录开始和结束的行列号。

  3. leadingComments、innerComments、trailingComments: 表示开始的注释、中间的注释、结尾的注释,每个 AST 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,想拿到某个 AST 的注释就通过这三个属性。

  4. extra:记录一些额外的信息,用于处理一些特殊情况。比如 StringLiteralvalue 只是值的修改,而修改 extra.raw 则可以连同单双引号一起修改。

    比如:const name = 'feng'; ,修改 value 只能修改值,修改 extra.raw 可以连引号一起修改。

常用的库(把源码转换成AST)

  • HTML
    • htmlparser2
    • parse5
  • JS
    • recast
    • uglify-js
    • @babel/parser
  • Vue
    • vue-template-compiler

其他,还有一些支持转换多种语言,比如GoGoCode。

其中用的比较多的有@babel/parser,babel parserbabylon,是基于 acorn 实现的,扩展了很多语法,可以支持 es next(现在支持到 es2020)、jsxflowtypescript 等语法的解析。

还有下面会提到的vue-template-compiler,是vue最常用的解析器。

实战-手写parser(解析)、traverse(转化)、generator(生成代码)

一个代码插件:使用vue-template-compiler把提取template中的class结构

功能:template生成css/less/scss结构树

这里原理很简单,

拓展应用场景(代码混淆、prettier)

代码混淆

我们的代码是给人看的,对于计算机来说,变量名或者执行逻辑没必要容易读。为了让怀有恶意的人很难阅读代码,但不会改变执行的结果。代码混淆的目的就达到了。
其中,最常用就是名字转换。
变量名、函数名,这些,我们平时会注意命名要有含义,但是编译后的代码就不需要了,可以把各种 identifiername 重命名为没有含义的 abcd,修改作用域中某个变量的名字,同时还要修改用到它的地方,这个可以通过 babel来演示一下
使用它其中一块,path.scope.rename 的 api。

ast_demo

参考资料:
AST(抽象语法树)以及AST的广泛应用🔥
抽象语法树(AST):理解JavaScript代码的抽象语法树
AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解

推荐文章