站在巨人的肩膀上之给JS开发者看的AST

401 阅读7分钟

最近认识了一个有趣的朋友,真是19年的一大小确幸。她给我分享了一个问题,vue和小程序语法类似,如何复用呢?虽然现在公司现在业务场景在用taro生成适配多小程序的场景,但是却未曾想过如何实现。停留在框架上的自己终究是太狭隘了。细究下来,原来里面大有门道,顺便还牵扯了一个相当基础的概念AST,恶补了一下,突然有了一个小目标。哈哈,突然挺自豪自己是一个程序员,很多知识回过头来常读常新,还能认识十分有趣的人。

下面是Bohdan大大的一篇AST科普读完感觉大有裨益,然后又不自觉的翻译了下来留念一下,哈哈哈。

给JS开发者看的AST

为什么是抽象语法树?

如果你留意下现在前端项目的开发依赖(devDependcies),便会惊讶于这些年它的成长有多大。我们在这列举下分组列举下他们:JS编译,代码压缩,CSS预处理器们,eslint等。这些都是我们不需要放到生产包的JS模块,但他们在开发流程中起了至关重要的作用性。而所有这些工具都是基于抽象语法树(AST)处理的。

下面是今天主题的讲解计划。我们先从抽象语法树的定义以及如果从代码中搭建抽象语法树。然后,我们将简要的接触一些最常用的使用场景以及基于AST处理的工具。最后我会以我的项目js2flowchart收尾(一个展示AST构建的好🌰)

什么是抽象语法树?

它是分层编程的一种展示(a hierarchical program representation):根据编程语言的语法展示源码结构,每个AST节点对应源代码的一部分。

是不是一脸懵逼,让我们看下实例

这是主要思想。从纯文本中获取树状数据结构,代码中节点与树节点对应。

那么如何从纯文本代码获取AST呢?我们知道已经有编译器做这样的事了。让我们梳理下编译器的工作方式吧

幸运的是,我们不需要贯穿所有的解析,把高级语言转换成字节。我们只需要关注词法和语法解析。

第一步,词法解析,也叫扫描仪,它会读取字节流(我们的代码)并把它们按照定义的规则转换成词法单元。当然,它也会移除空格字母,注释等。最终,所有的代码会被切分成一张词法列表;

当词法解析器读取源代码的时候,它会逐个字母扫描大妈;当他遇到空格,运算符或者特殊符号的时候,它会认为这个单词已经结束。

第二部,语法分析器,也叫解析器,将在词法分析之后将它们转化成树状展示(校验语言语法,如果有错误就会抛出语法错误)。

当生成树的时候,有些解析器会产生不必要的词法单元(譬如多余的括号),以至于树并不是100%和代码匹配,但是知道如何处理它就足够了。另一方面,解析器包含所有大妈生成的树叫做具体语法树(Concrete Syntax Tree)

关于编译器想要了解更多么?

The-super-tiny-compiler。这是一个超级简化的例子。它中有部分内容像200行将代码将Lisa转化成C语言。所有的代码都包含有注释。

LangSandbox.另外一个可以参考的好项目,它阐述了如何打造一门变成语言。它里面有一系列的文章来说明如何去做。因此,它会切入的深一些,不只是把LISA编译成C语言,你可以写你自己的语言并把它编译成C/bytecode然后执行。

那么我能用一个库吗?当然,这里有不少的库。你可以浏览astexplorer然后挑一个你中意的。这是一个使用AST编辑器的实时的编辑器

我想特地推荐一个库,Babylon

他在Babel找那个应用,这或许是它流行的原因。因为背后有Babel项目支持,我可以预计它会跟上最新的JS特性。所以,并且,它拥有相当好的API而且容易被推广应用。

现在让我们将目光放到现实场景的应用。

第一个应用场景我想提及的编译,当然是,Babel。

Babel 不是一个具有ES6支持的工具。当然,他是,但它远不只是这样(这蹩脚的英文表达)。

许多把Babel联系在ES6/7/8特性的支持。事实上,这也是为什么我们经常使用的原因。但这只是诸多插件中的一种。我们也可以把它应用在代码压缩,React相关语法编译(像JSX),作为Flow的插件等。

Babel是一个JS编译器。在一个高等级层面,它有3个运行阶段:解析,转换以及生成。你给Babel一些JS代码,它会修改这些代码并且声称一个段新的代码进行支持。它是怎么修改代码的呢?毫无疑问!它建立了一个AST,并进行转换,通过插件的基础上进行修改,最后从修改的AST中声称新的代码。

让我们看下新的实例代码的例子。

正如我之前提到的,Babel使用Babylon,所以,我第一件事是解析代码,然后抓换成AST并且转换所有的变量的名字。最后的一个步骤————声称代码。正如你所看到的,第一阶段(解析)和第三阶段(代码声称)。

当你在开发Babel插件时,你只是在描述将转换成AST“访客”节点。

如果你想了解更多如何构建你的第一个babel-plugin,可以仔细翻阅Babel-handbook

让我们继续,下一个使用常见的使用场景是自动代码修复工具以及JSCodeshift.

可以把所有过时的匿名函数替换成短熊精悍的箭头函数。

你的代码编辑器大概率不能做到这样,因为只不是简单的查找替换操作。这就是jscodeshift出来承担的角色。

如果你听说过‘jscodeshift’大多数可能是和codemods一起听说的(两个容易高欢的概念)。Jscodeshift是一个运行“codemods”的工具集。“codemod”是一种能准确描述什么能转换成AST的代码。它的思想和Babel以及它的插件相似。

所以,如果你想创建自动把你的代码从旧的框架迁移到新的框架的方式。举个例子,react 16的prop-types重构。

啊哈,已经有很多codemodes已经创建了,敬请品尝吧,无需手动修改

github.com/reactjs/rea… github.com/facebook/js…

最后一个我想提及的是Prettier,可能大家在日常的工作中最常使用。

Prettier格式化我们的代码,它会断句,清理空格,闭合等。因此它会将代码输入并且返回修改过的代码作为输出。听起来十分熟悉,是嘛?

思想依旧类似。首先,获取代码,生成AST。接下来要发生的事正是prettier的魔力所在。AST将转换成中间态或者说是文档。在高层面将,AST节点将会扩展成为了格式化相关的信息。举个例子,函数的一系列参数会被当成一组相关值。因此如果这个列表很长而且不能放在一行,便会拆散每个参数到一个独立的杭等。这个主要的算法叫做'printer'(将会贯穿整个IR,并且基于整体决定如何去格式化代码)。

如果你想了解更多pretty printing,看起来当然不简单,你可以深入翻阅这本书

接下来最后一件事情就是我刚提及的自己的库js2flowchart

bogdan-lyashenko.github.io/js-code-to-… 它能干什么呢?首先,从生成的代码块抽离出来,你可以研究抽象的等级如何展示

那么,它是如何工作的呢?

首先,将代码转成AST,然后我们将AST转换成另外一个树,我叫它FlowTree。它会忽略一系列小的不重要的词法但是会将关键块像函数,循环,条件等,放到一起。之后,我们转换FlowTree,并从其中创造成一个新的ShapesTree.每个ShapesTree的节点包含关于它虚拟类型的信息,位置和树中的关联。最后一步,我们将遍历所有的类型,生成SVG图github.com/Bogdan-Lyas…