.NET手擼繪制TypeScript類圖——上篇
近年來隨着交互界面的精細化,TypeScript越來越流行,前端的設計也越來復雜,而類圖正是用簡單的箭頭和方塊,反映對象與對象之間關系/依賴的好方式。許多工具都能生成C#類圖,有些工具也能生成TypeScript類圖,如tsuml,但存在一些局限性。
我們都是.NET開發,為啥不干脆就用.NET擼一個TypeScript類圖呢?
說干就干!為了搞到類圖,一共分兩步走:
- 解析
.ts文件,生成抽象語法樹(AST),並轉換為簡單的類、屬性、方法等對象 - 將這個對象繪制出來
本文將分上下兩部分,上篇將介紹我移植的一個.NET Standard 2.0的TypeScript解析庫,下篇將介紹如何將AST轉換為真正的圖,並實現一些基本的交互。
.ts文件生成抽象語法樹
正常來說編譯原理挺難的,但好在有人趕在了我的前頭😁。
TypeScript解析庫
我在Github上找到了一個叫TypeScriptAST的項目,它剛好就能將.ts文件轉換為AST。但它僅提供了.NET Framework版本。我看了一下實現方式,它是從微軟官方的TypeScript倉庫按源代碼翻譯的。其中Parse.cs高達近8000行代碼,能把如此巨大的工作翻譯完成,可見作者花了不少時間。
我拿了過來,稍微改造了一下,移植到了.NET Core。NuGet包地址為:
我移植的這個版本源代碼也開放到了Github,使用相同的Apache-2.0協議開源,開源項目鏈接如下:
雖然不知道是不是第一個移植的,但可以確定的是今后.NET Core也能解析TypeScript了:)
注意:官方沒有提供
TypeScript的.NET解析工具,也沒建議用.NET,使用ts解析是正常做法,官方的包用起來顯然也更有自信——但這就是騷操作,不挑戰一下怎么知道極限在哪呢?
簡單使用
假如有如下TypeScript代碼:
class Class1
{
td: number = 3;
ts: string = 'hello';
doWork(): string {
return `${3+this.td}-${this.ts}`;
}
}
var tc = new Class1();
我們可以使用TypeScriptAST的類進行分析,只需使用TypeScriptAST類:
var ast = new TypeScriptAST(source: tsSourceStringContent);
該類有許多對象,提供了豐富的解析方式,使用如下代碼,即可將代碼中的類抽出來:
var classAsts = ast.OfKind(SyntaxKind.ClassDeclaration);
由於AST中的屬性太多,我們調試時抽重要的顯示出來,並轉換為JSON:
JsonSerializer.Serialize(classAsts.Select(c => new
{
c.IdentifierStr,
Children = c.Children.Skip(1).Select(x => x.IdentifierStr),
}), new JsonSerializerOptions { WriteIndented = true}).Dump();
結果如下:
[
{
"IdentifierStr": "Class1",
"Children": [
"td",
"ts",
"doWork"
]
}
]
有了這個,我們即可定義一些類型,用於后續繪制AST:
class ClassDef
{
public string Name { get; set; }
public List<PropertyDef> Properties { get; set; }
public List<MethodDef> Methods { get; set; }
}
class PropertyDef
{
public string Name { get; set; }
public bool IsPublic { get; set; }
public bool IsStatic { get; set; }
public string Type { get; set; }
public override string ToString() => (IsPublic ? "+" : "-") + $" {Name}: " + (String.IsNullOrWhiteSpace(Type) ? "any" : Type);
}
class MethodDef
{
public string Name { get; set; }
public bool IsPublic { get; set; }
public bool IsStatic { get; set; }
public List<ParameterDef> Parameters { get; set; }
public string ReturnType { get; set; }
public override string ToString() =>
(IsPublic ? "+" : "-")
+ $" {Name}({String.Join(", ", Parameters)})"
+ (Name == ".ctor" ? "" : $": {ReturnType}");
}
class ParameterDef
{
public string Name { get; set; }
public string Type { get; set; }
public override string ToString() => $"{Name}: {Type}";
}
借助於.NET強大的LINQ,可以將代碼寫得特別精練,最后可以達到“一行代碼*”完成.ts到AST的轉換:
static Dictionary<string, ClassDef> ParseFiles(IEnumerable<string> files) =>
files
.Select(x => new TypeScriptAST(File.ReadAllText(x), x))
.SelectMany(x => x.OfKind(SyntaxKind.ClassDeclaration))
.Select(x => new ClassDef
{
Name = x.OfKind(SyntaxKind.Identifier).FirstOrDefault().GetText(),
Properties = x.OfKind(SyntaxKind.PropertyDeclaration)
.Select(x => new PropertyDef
{
Name = x.IdentifierStr,
IsPublic = x.First.Kind != SyntaxKind.PrivateKeyword,
IsStatic = x.OfKind(SyntaxKind.StaticKeyword).Any(),
Type = GetType(x),
}).ToList(),
Methods = x.OfKind(SyntaxKind.Constructor).Concat(x.OfKind(SyntaxKind.MethodDeclaration))
.Select(x => new MethodDef
{
Name = x is ConstructorDeclaration ctor ? ".ctor" : x.IdentifierStr,
IsPublic = x.First.Kind != SyntaxKind.PrivateKeyword,
IsStatic = x.OfKind(SyntaxKind.StaticKeyword).Any(),
Parameters = ((ISignatureDeclaration)x).Parameters.Select(x => new ParameterDef
{
Name = x.OfKind(SyntaxKind.Identifier).FirstOrDefault().GetText(),
Type = GetType(x),
}).ToList(),
ReturnType = GetReturnType(x),
}).ToList(),
}).ToDictionary(x => x.Name, v => v);
兩個函數稍微提取一下,代碼能更精練:
static string GetReturnType(Node node) => node.Children.OfType<TypeNode>().FirstOrDefault()?.GetText();
static string GetType(Node node) => node switch
{
var x when x.OfKind(SyntaxKind.TypeReference).Any() => x.OfKind(SyntaxKind.TypeReference).First().GetText(),
_ => node.Last switch
{
LiteralExpression literal => literal.Kind.ToString()[..^7].ToLower() switch
{
"numeric" => "number",
var x => x,
},
var x => x.GetText(),
},
};
使用
我對這個ShootR項目進行了分析,分析代碼如下:
ParseFiles(Directory.EnumerateFiles(
path: @"C:\Users\dotnet-lover\source\repos\ShootR\ShootR\ShootR\Client\Ships", "*.ts")
).Dump();
分析結果:

成功找到了完整的7個類,並將類名、字段名、字段類型、方法名、方法參數和返回值等信息都解析出來了。
總結
在本篇我們介紹了如何使用.NET解析TypeScript,並推薦了我移植的一個NuGet包:Sdcb.TypeScriptAST。
下篇將在這篇的基礎上,介紹如何使用代碼將類圖渲染出來。
本文所用到的完整代碼,可以在我的Github倉庫中下載:
https://github.com/sdcb/blog-data/tree/master/2019/20191113-ts-uml-with-dotnet
喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】

