AST 简单来说就是将 typescript 语法解析为一个一个的节点,在节点信息中包括了该节点的 position
, text
, kind
等信息,更详细的介绍大家可以查看 AST,在这里也给大家推荐一个查看解析后的 AST 结构的网站,直接将 ts 代码粘贴到其中观察其结构能够更好的帮助你的理解。
如果是第一次点进来的同学也可以先看看这篇文章了解一下什么是 Angular Schematics.
为了让大家有更好的理解,这篇文章将会结合其在 Schematics 中插入代码的方式来进行介绍。
1 读取 ts 文件
在使用 AST 来分析 ts 文件之前,我们需要先读取 ts 文件将其转为一个 ts.Node
类型的数组
// 在这里我们借助 Schematics 中 tree 来读取
import { Tree } from '@angular-devkit/schematics';
import * as ts from 'typescript';
// 在这里实现两个函数,将文件读取为 ts.SourceFile
function readIntoSourceFile(tree: Tree, filePath: string): ts.SourceFile {
// 读取文件的方法使用 `fs` 也可,由于该文章主要是与 Angular Schematics 相结合,所以使用的 tree 读取
const text = tree.read(filePath);
if (text === null) {
console.log('File does not exist.');
return;
}
const sourceText = text.toString('utf-8');
return ts.createSourceFile(
filePath,
sourceText,
ts.ScriptTarget.Latest,
true
);
}
function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
const nodes: ts.Node[] = [sourceFile];
const result = [];
while (nodes.length > 0) {
const node = nodes.shift();
if (node) {
result.push(node);
if (node.getChildCount(sourceFile) >= 0) {
nodes.unshift(...node.getChildren());
}
}
}
return result;
}
接下来我们打印一下 readIntoSourceFile
和 getSourceNodes
得到的结果看一下。
在打印的结果中可以看到我们之前提到的相关属性:
position
:pos
和end
kind
:kind
打印的结果并不是非常的直观,还是推荐大家将自己的代码拷贝到前面提到过的 Typescript AST Viewer 中查看。
2 通过 AST 在特定位置插入代码
一样的我们还是先看代码,假设我们要在如下的 ts 文件中插入代码在 people
数组中。
const people = ['XiaoHua', 'XiaoLi', 'LiHua'];
首先我们先实现下面的方法来进行对数组的插入
// 由于这里主要结合了 Angular Schematics,所以会有一些特定的用法,但是核心代码与框架无关
/**
* path: ts 文件路径
* insertText: 插入的内容,对应我们的例子即为一个数组的元素如 'ZhangSan'
* target: 插入的目标,对应我们的即为数组的名字 people
* insertPosition: 插入的位置,对应我们的例子即为在数组的开头或者结尾插入
*/
function insertArray(
tree: Tree,
path: string,
insertText: string,
target: string,
insertPosition: 'start' | 'end'
): Tree {
// 读取成 ts 源文件的方法使用 `fs` 也可
const sourceFile = readIntoSourceFile(tree, path);
const nodes = getSourceNodes(sourceFile);
let arrayNodeSiblings = null;
let expressionNode = null;
// 根据传入的数组名字 `people` 来定位到位置
const arrayNode = nodes.find(
(n) =>
(n.kind === ts.SyntaxKind.Identifier ||
n.kind === ts.SyntaxKind.PropertyAccessExpression) &&
n.getText() === targetText
);
if (!arrayNode || !arrayNode.parent) {
console.log('Get Component Node Filed.');
return;
}
arrayNodeSiblings = arrayNode.parent.getChildren();
// 找到我们需要插入的数组元素的位置
let arrayNodeIndex = arrayNodeSiblings.indexOf(arrayNode);
arrayNodeSiblings = arrayNodeSiblings.slice(arrayNodeIndex);
expressionNode = arrayNodeSiblings.find(
(n) => n.kind === ts.SyntaxKind.ArrayLiteralExpression
);
if (!expressionNode) {
console.log('The target node is not defined');
return;
}
// 根据 SyntaxKind 为 SyntaxList 的节点定位到 people 数组后 [] 的位置
const listNode = expressionNode
.getChildren()
.find((n) => n.kind === ts.SyntaxKind.SyntaxList);
if (!listNode) {
console.log(`${targetText} The target node is not defined`);
return;
}
/* 到此我们寻找指定数组的位置的过程已经结束了,目前我们需要插入代码的位置信息已经得到了,接下来我们将新的元素插入到数组中 */
// 在这里我们可以指定在数组的开始或者结尾插入代码
const changePosition =
insertPosition === 'start' ? listNode.getStart() : listNode.getEnd();
const change = new InsertChange(path, changePosition, insertText);
const declarationRecorder = tree.beginUpdate(path);
if (change instanceof InsertChange) {
declarationRecorder.insertRight(change.pos, change.toAdd);
}
tree.commitUpdate(declarationRecorder);
return tree;
}
以上已经完成了核心的方法,我们再总结一下,首先将 ts
文件读取为 ts 源文件形式,之后再获取到代码的所有节点,然后根据节点(node)的类型(SyntaxKind)定位到我们需要插入的任何元素,不仅限于数组,也包括 object
, constructor
, Function
等等都是能够找到具体位置的。
在上述方法中大家也注意到了 SyntaxKind
,大家可以在 node_modules
中 typescript
里查看到更多详细信息。
3 更多的用法
这里我们以一段简单的 Angular 组件代码为例做简单的介绍,除了插入代码外,我们还能够通过 AST
来获取文件中我们想要获取的内容,大家可以自行尝试,会有更深的理解。
import { Component, Input } from '@angular/core';
@Component({
selector: 'selector',
template: `<div></div>`
})
export class AppComponent {
@Input() input1: string;
@Input() input2: number;
@Input() input3: boolean = true;
constructor() {}
}
在通过将改文件解析为 AST
之后,我们可以读取到每一个 Input
的名字,类型,默认值来生成我们的 api
文档。
function getComponentAttribute(tree: Tree, path: string) {
const sourceFile = readIntoSourceFile(tree, filePath);
const nodes: ts.Node[] = getSourceNodes(sourceFile);
const propertyNodes: ts.Node[] = [];
nodes.forEach((n) => {
if (n.kind === ts.SyntaxKind.Decorator && n.getText() === '@Input()') {
propertyNodes.push(n.parent);
}
});
// 之后就可以使用 propertyNodes,再根据 SyntaxKind 来提取你想要的内容了
......
}
4 总结
本文给大家提供了一种通过 AST
来解析 ts
文件的方法,也给大家提供了一种修改 ts
文件的方法,使用 AST
能够准确的定位到位置。
利用 AST
几乎可以帮我们做一切的操作对一个 ts
文件。这里也推荐大家可以了解一下 Angular Schematics (原理图),其也是借用了 AST 来进行很多文件的操作。了解之后可以定制更加契合你的 cli。
更多的你甚至可以实现在 vscode 中打开一段 ts 代码一样,推断出一个变量的类型。总而言之,除了本文中提到的 插入代码
,信息提取
,还有很多是可以通过分析 AST
来实现的。