Typescript中interface自动化生成API文档详解

发布时间:2022-12-27 09:31

最近在搞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);
TypeScript实现字符串转树结构的方法详解 网站建设

TypeScript实现字符串转树结构的方法详解

有一个多行字符串,每行开头会用空格来表示它的层级关系,每间隔一层它的空格总数为2,如何将它转为json格式的树型数据?本文就跟大家分享下这个算法,欢迎各位感兴趣的开发者阅读本文。 例如有一个字符...
Vue3学习笔记之依赖注入Provide/Inject 网站建设

Vue3学习笔记之依赖注入Provide/Inject

Provide / Inject 通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果...
Vue3全局实例上挂载属性方法案例讲解 网站建设

Vue3全局实例上挂载属性方法案例讲解

在大多数开发需求中,我们有时需要将某个数据,或者某个函数方法,挂载到,全局实例身上,以便于,在项目全局的任何位置都能够调用其方法,或读取其数据。 在Vue2 中,我们是在 main.js 中 直...