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 可以连引号一起修改。

继续阅读全文 »

uni-app开发APP之开发规范和一些注意事项

🌞个人体会

  • 开发快速。uniapp最大的优势就是整合7端开发规范在一起,配合Vue让前端开发人员可以开发出“具有基本功能”的APP(对比flutter来说,uniapp对前端更友好),可以快速开发多端应用。一次编译,多端运行。
  • 接口丰富。DCloud有h5 app经验,同时这部分的API也开放给uniapp可以直接调用,调用的时候同样也是用js引擎调用原生API。很多东西开箱即用,微信支付,消息推送等等。
  • 过于全面。一般项目不需要7端都需要,但是官方必须考虑7端,所以很多东西必须用条件编译区分平台来做。
  • 由于需要适配小程序,提升h5的性能,uniapp屏蔽了dom,规定了自己的规范,使用官方提供的方法可以选择节点,因此市面上的dom操作相关的库都不能使用。复杂特效只能用CSS配合官方接口和vue动态数据绑定来做。个人觉得做复杂动效需要费点时间。
  • 虽然,uniapp是跨7端,但是跨越多的端,由于每种平台上规范不同,即使uniapp尽量统一这些不同的规范,但是仍然需要根据不同平台进行处理,就像排列组合一样,每多一种平台,不是单单的复杂加一,而是复杂加一倍。适合只做一种平台类型软件,但是没有对应平台软件开发工程师,可以跨语言障碍,仅此而已。而不是支持7端,就去做7端。
  • 实际上,很多时候不需要开发出可以运行7、8端运行的程序,况且大部分产品也是天生只适合在某种平台上运行,一味的追求一套代码运行很多的平台,很多平台特有功能或者时说是优势能力因为妥协其他平台被牺牲掉,最后就像有好多短板的木桶一样,搞的产品在哪个平台上都没有优势。还不如针对于某一端做深耕一中平台代码,让前端人员可以用前端代码跨端运行,这也许会更符合实际开发需求,

🌞开发规范

  • 组件不能使用纯HTML标签,不能用js对dom进行操作
  • 接口能力靠近微信小程序规范,数据绑定方式靠近Vue规范,同时补充了APP和页面的生命周期
  • 为了兼容多端运行,建议使用flex布局,配合upx使用
  • 高版本安卓APP发送request请求必须使用https
  • 微信小程序request请求也需要https,同时在小程序管理员后台配置可进行request请求的域名白名单。

继续阅读全文 »

linux上git clone每次都要输入用户名密码

最近新使用一个代码部署运维平台Spug,拉取代码时配置仓库地址,部署机上如果使用http拉取的话需要每次都输入用户名,密码,平台没有提供这种功能,只会执行git clone -v 加上配置的仓库地址,根本没有办法输入用户名和密码,加上公司的私有云上部署的gitlab不支持外网使用ssh拉取仓库,所以只能用http方式拉取,而且还不能每次都要输入用户名密码,网上找了一下解决方法,做个记录。

继续阅读全文 »

npm script

原理

npm脚本的原理非常简单。每当执行npm run,就会自动新建一个 Shell,在这个Shell里面执行指定的脚本命令。因此,只要是Shell(一般是 Bash)可以运行的命令,就可以写在 npm脚本里面。

比较特别的是,npm run新建的这个Shell,会将当前目录的node_modules/.bin子目录加入PATH变量,执行结束后,再将PATH变量恢复原样。

这意味着,当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。比如,当前项目的依赖里面有Mocha,只要直接写mocha test就可以了。

继续阅读全文 »