Typescript中interface自动化生成API文档详解
孟祥_成都 人气:0前言
最近在搞react组件库,这两天搞定了使用ast(抽象语法树)去把interface转为对象或者数组,这些数据就可以渲染为react组件的table或者markdown的table,啥意思呢,举个例子:
UI层面
以下是interface的demo,被转化
export interface TdAffixProps { /** * 指定滚动的容器。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body * @default () => (() => window) */ container: any; /** * @desc 距离容器顶部达到指定距离后触发固定 * @default 0 */ offsetBottom?: number; /** * @desc 距离容器底部达到指定距离后触发固定 * @default 0 */ offsetTop?: number; /** * @desc 固钉定位层级,样式默认为 500 */ zIndex?: number; /** * @desc 固定状态发生变化时触发 */ onFixedChange?: (affixed: boolean, context: { top: number }) => void; }
转化为类似:
数据层面
interface被转化为数组,数组里的每一项如下,可以传给table组件去渲染,当然有人想渲染为markdown格式,那把下面的数组渲染为markdown的table就行了,没啥难度。
{ "name": "TdAffixProps", "data": [ { "name": "container", "type": "any", "jsdoc": { "kind": 24, "description": "指定滚动的容器。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body", "tags": [ { "kind": 25, "tagName": "default", "text": "() => (() => window)" } ] } }, { "name": "offsetBottom", "type": "number", "isOptionnal": "?", "jsdoc": { "kind": 24, "description": "", "tags": [ { "kind": 25, "tagName": "desc", "text": "距离容器顶部达到指定距离后触发固定" }, { "kind": 25, "tagName": "default", "text": "0" } ] } }, { "name": "offsetTop", "type": "number", "isOptionnal": "?", "jsdoc": { "kind": 24, "description": "", "tags": [ { "kind": 25, "tagName": "desc", "text": "距离容器底部达到指定距离后触发固定" }, { "kind": 25, "tagName": "default", "text": "0" } ] } }, { "name": "zIndex", "type": "number", "isOptionnal": "?", "jsdoc": { "kind": 24, "description": "", "tags": [ { "kind": 25, "tagName": "desc", "text": "固钉定位层级,样式默认为 500" } ] } }, { "name": "onFixedChange", "type": "(affixed: boolean, context: { top: number }) => void", "isOptionnal": "?", "jsdoc": { "kind": 24, "description": "", "tags": [ { "kind": 25, "tagName": "desc", "text": "固定状态发生变化时触发" } ] } } ] }
我们需要的数据结构
上面可以看到,我们需要的数据结构是
{ name: xxx, // interface的名字, data: [ { name: xx, // interface里每一项的属性名 type: xx, // interface里每一项的类型 isOptionnal: xx, // 是否是可选项 jsDoc: {} // 后面细说 } ] }
简单解释一下jsdoc格式
JSDoc是一种文档生成工具,可以用来为JavaScript代码生成API文档。它使用特殊的注释格式来描述代码中的类型、函数、变量等的用途、参数、返回值等信息。
例如,你可以在JavaScript代码中使用如下的注释来描述一个函数:
/** * 描述文字 * @default 0 */ function sum(x, y) { return x + y; }
这段注释会被解析为:
{ "kind": 24, // 忽略 "description": "描述文字", "tags": [ { "kind": 25, // 忽略 "tagName": "default", "text": 0 } ] }
AST解析技术选择
为什么放弃babel
最开始我只知道babel,因为用webpack多了,不太了解ast相关的前端库,然后很正常的这样使用了,发现了问题:
const parser = require("@babel/parser") const traverse = require("@babel/traverse").default const generate = require('@babel/generator').default const fs = require("fs") fs.readFile('./type.ts', { encoding: 'utf-8' }, function (err, data) { if (err) throw err; const result = []; const ast = parser.parse(data, { sourceType: "unambiguous", plugins: ["typescript"] }); traverse(ast, { TSInterfaceDeclaration(path) { path.traverse({ TSPropertySignature(path) { console.log(path.node.key.name); console.log(path.node.leadingComments?.[0]?.value); }, }); } }); });
比如number这个类型在上述打印节点的时候的类型是TSNumberKeyword,但是我拿到TSNumberKeyword不是目的,我要number,这个咋办,
你说简单啊,做个映射
{ TSNumberKeyword: "number" }
好,我知道简单的映射可以,但是还有function类型,我咋映射,我需要还原的嘛,然后我想到了直接用generator把类型片段还原,但是总感觉有点low。
其次,我没法直接获得jsdoc的类型,因为注释本质上就是字符串,然后自己去折腾为jsdoc格式。
所以我去看了一下arco cli里的转换使用到了ts-morph这个库,发现这个库在我这个需求下,是非常适合的,接下来介绍。
顺便提一句,我的实现比字节团队的arco cli要简单非常非常多!
ts-morph
这个库极大的缓解了不懂typescript繁琐底层类型和方法的同学,具体的方法和属性真的也是挺多的。ts-morph是一个针对 Typescrpit/Javascript的AST处理库,可用于浏览、修改TS/JS的AST。
关于ts-morph的详细文档,参见其官网:ts-morph.com/。
下面是我实现的基本思路(可以把里面的函数抽取为中间件,这样更好维护,目前懒得改了,类型没认真写,大家可以在我的基础上自己封装适合自己业务的东西,思路还是很清晰的),后续会把它抽成一个单独的库给自己的react组件库使用。
以下代码说白了就一个简单函数,arco官方的cli工具虽然代码也就200行的样子,但是复杂度比我这个高很多。
自动化生成代码
import { Project } from "ts-morph"; const internalProject = new Project({ tsConfigFilePath: "./tsconfig.json", }); const sourceFile = internalProject.getSourceFile("./type.ts"); const interfaces = sourceFile!.getInterfaces(); const result:any[] = []; interfaces.forEach((inter_face)=>{ result.push({ name: '', data: [] }); const index = result.length - 1; result[index].name = inter_face.getName(); inter_face.getProperties().forEach((v) => { result[index].data.push({ name: v.getName(), type: v.getTypeNode()?.getText(), isOptionnal: v.getQuestionTokenNode()?.getText(), jsdoc:v.getJsDocs().map((jsDoc)=>{ return (jsDoc.getStructure()) })[0] }); }); }) console.log(result);
总结
加载全部内容