前端开发者如何用 API Extractor 管理 API
API Extractor 是由微软提供的针对 Typescript 的 API 分析工具,如果你是一个 Typescript 库的开发人员,你可能需要它。它能解决以下问题:
- API 变化如何追踪?
- 怎么避免将内部 api 暴露到外部?
- 怎么避免忘了导出 API 的类型声明?
- 发布 alpha、beta 版本的包时,api 如何管理?
- 我们导出的 d.ts 十分杂乱,如何整理(webpack 等工具能将代码打包压缩)?
- API 文档如何自动生成?
如何描述 API
API Extractor 需要配合 TSDoc 使用。TSDoc 类似于 JSDoc,是微软提议的 typescript 注释规范。参考:TSDoc 。
跟 jsDoc 不同的是,TSDoc 语法更严格,另外 JSDoc 更多的关注点在给 js 提供类型注释,但针对强类型语言 typescript 设计的 TSDoc 关注点在于文档和 API 管理。
比如下面 JSDoc 的 Tag,在 TypeScript 里面毫无用处:
@function
: 将一个变量标记为函数@enum
:将一个对象标记为 enum@access
:标注对象成员的访问级别( private、 public、 protected),typescript 本身支持 private/public/readonly 等@type
这也能说明为什么我们写 TypeScript 应当使用 TSDoc,而不是 JS Doc。
我们这里暂时只需要关注 TSDoc 中标记 API 发布状态的 4 个 Tag:
@internal
虽然我导出了这个 API,但只供本项目的维护者在其他项目中使用。@alpha
将要发布,但还在完善中,大家不要用@beta
作为预览版供大家尝试,希望收到反馈。但不应该在生产环境使用,后续该 API 可能修改或移除。@public
正式发布,并后续将保持稳定,大版本升级才能变动
当我们把所有对外发布的 API 用 TSDoc 的 Release Tag 标记起来之后,我们就有了管理和追踪的可能。原因是:
- 对外暴露的 API 是有明确标注的。库的维护者能清楚一个模块的导出哪些是给内部在用,哪些是暴露在外的。从而避免不小心修改了对外提供的 API,也降低了修改内部接口的心理压力;
- 如果发现最终打包出的 d.ts 中有未标注的暴露在外的 API,我们可以检查是否是不小心暴露出去的。
如何使用 API Extractor
工作流程
它的大致工作流程如下:
- tsc 将 ts 源码转成 js 之后,会生成一堆 *.d.ts
- API Extractor 通过读取这些 d.ts
- 可以生成 api 报告
- 将凌乱的 d.ts 打包并删减
- 生成文档描述模型(xxx.api.json)可以通过微软提供的 api-documenter 进一步转换成 Markdown 文档。
生成 api 报告
当我们标记好 Release Tag 后,API Extractor 可以帮助我们生成 API 报告。
这里用一个简单的 Demo 来测试以下,我的项目代码如下:
src/index.ts
export type Cat = {
name: string;
}
/**
* type for person
* @public
*/
export type Person = {
name: string,
age: number
}
/**
* foo function
* @Public
*/
export function foo(arg1: Person) {}
假设已经安装了 @microsoft/api-extractor
, 并在配置文件 api-extractor.json
里面开启了apiReport
。(配置项: apiReport.enabled
)那么只需要执行以下命令,就能生成一份 API 报告。
npx api-extractor run --local --verbose
生成的 API 报告如下:
## API Report File for "api-extractor-test"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
// @public (undocumented)
export type Cat = {
name: string;
};
// @public
export function foo(arg1: Person): void;
// @public
export type Person = {
name: string;
age: number;
};
// (No @packageDocumentation comment for this package)
可以看到所有的API 类型声明汇总到了一份文档。同时,在执行生成API报告时,会在命令行给出Warning
:
Warning: src/index.tsx:1:1 - (ae-missing-release-tag) "Cat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
这时我需要再去检查一下代码,会发现 Cat
这个类型其实并需不要导出,我可以去掉 export
关键字。
注意
API 报告类似于快照测试,应当放到 git 管理,这样每次提交代码能看到 API 的变化。
下面是 Api-extractor 能检测出来的问题:
- 多导出了东西,如上面的例子,多导出了
Cat
- 忘了导出类型声明
// uncallable forgotten export
enum ReportType {
Full,
Condensed
}
// forgotten export
interface IShowReportOptions {
reportTitle: string;
validation?: boolean;
reportType?: ReportType;
}
/**
* Shows a report.
* @public
*/
export function showReport(options: IShowReportOptions): void {
}
// Warning: "The symbol "IShowReportOptions" needs to be exported by the entry point src/index.d.ts."
比如说上面这个例子,我们导出了 showReport
方法,但没导出 IShowReportOptions
及 ReportType , 此时用户如果想构造出一个 option 传给 shoeReport,却不知道怎么声明类型。
- release Tag 冲突,如下面的例子:
/** @public */
interface Size {
width: number;
height: number;
}
/** @beta */
function Size(width: number, height: number): Size {
return { width, height };
}
// Warning: This symbol has another declaration with a different release tag.
- 导出了,却没标记 release tag. (@public @internal @beta 等)
这个也可以用来防止暴露了内部类型。 - 其他注释相关的检查
- 关联了一个未导出的类型 ae-unresolved-link
注意: 生成的 API 报告也应当放到 git 管理,这样每次 MR 能看到 API 的变化。
所有 API Extractor 能给出的提醒可见: Message Reference
打包 d.ts
类似于 webpack 的打包,从一个入口文件开始,将所有依赖文件打包成一个文件,api-extractor 也可以从 index.d.ts 开始,把所有导出的类型打包到一起。
d.ts trimming
简言之,可以根据发布场景裁剪 d.ts。假如是正式版本的发布,可以将 release tag 为 @internal @beta 的类型声明删掉。如果是预览版本的发布,需要保留 @beta @public。如果是开发版本,会将所有的类型声明保留。
具体配置项如下:
{
. . .
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>.d.ts",
"betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",,
"publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts"
},
}
在不同发布场景,修改 package.json 的 “typing” 字段指向来切换 d.ts
打包 d.ts 慎用的场景
打包 d.ts 的前提是你的库只有单一入口,如果你的包支持按路径引入,例如:
import {Button} from 'xxx-ui/lib/Button';
import {DatePicker} from 'xxx-ui/lib/DatePicker';
那么势必 d.ts 需要分散在各文件夹。这种情况下不要使用打包 d.ts 的功能。
基于路径的导入会使得文件架结构也成为你的库的一部分,可能文件架结构的调整也会成为 breakin change。所以是否考虑下不支持按路径导入?
生成 API 文档
API Extractor 能够生成一份 json 格式的文档模型(api-model)。相关配置在配置文件的 docModel 字段。
使用 @microsoft/api-documenter 生成 Markdown 文档
难度: 1 颗星
可以使用 @microsoft/api-documenter
直接将 api-model.json 转成 Markdown 或 yaml 格式的文档。具体步骤参考:Generating API docs 。
大致步骤只需要安装 @microsoft/api-documenter
,然后执行以下命令,指定下 Api Extractor 生成的 *.api.json 所在的文件夹以及你希望生成的文档存放在那个文件夹即可。
api-documenter markdown -i json-model-folder -o out-put-doc-folter
生成一堆 Markdown 文件之后,也很容易使用一些工具进而转成 html 或者直接生成站点。
通过@microsoft/api-extractor-model
自己生成文档
难度: 4 颗星
也可以通过@microsoft/api-extractor-model
自己解析 api-model.json。它能将 xxx.api.json 解析成以下数据结构:
- ApiModel // api-model 入口
- ApiPackage // 可能会有多个包,对应 menorepo 的每个npm 包
- ApiEntryPoint // 入口文件,可以想象为某个包的 index.js
- ApiClass // 从入口文件导出的所有的 Class 类型, 数组
- ApiMethod // 方法
- ApiProperty // 属性
- ApiEnum // 从入口文件导出的所有的 Enum 类型
- ApiEnumMember // 枚举的每一项
- ApiInterface // 从入口文件导出的所有的 Interface
- ApiMethodSignature // 方法
- ApiPropertySignature // 属性
- ApiNamespace // ts 的namespace
- (ApiClass, ApiEnum, ApiInterace, ...)
我们能很容易从 ApiModel 入口开始,逐层遍历这个树形结构。 api-documenter 实际上也是使用 api-extractor-model 来解析遍历所有 API。这里摘抄一段代码:
// 先判断 apiItem 的类型,根据不同类型采取不同的解析方法。
switch (apiItem.kind) {
case ApiItemKind.Class:
this._writeClassTables(output, apiItem as ApiClass); // 将 class 的属性和方法生成一个表格
break;
case ApiItemKind.Enum:
this._writeEnumTables(output, apiItem as ApiEnum); // 将 enum 的属性和方法生成一个表格
break;
case ApiItemKind.Interface:
this._writeInterfaceTables(output, apiItem as ApiInterface); // 将 interface 的属性和方法生成一个表格
break;
case ApiItemKind.Constructor:
case ApiItemKind.ConstructSignature:
case ApiItemKind.Method:
case ApiItemKind.MethodSignature:
case ApiItemKind.Function:
// 函数相关的类型,记录参数信息
this._writeParameterTables(output, apiItem as ApiParameterListMixin);
this._writeThrowsSection(output, apiItem);
break;
// .... 省略一些判断
default:
throw new Error('Unsupported API item kind: ' + apiItem.kind);
}
这里再摘抄一段生成 interface 文档的代码:
private _writeInterfaceTables(output: DocSection, apiClass: ApiInterface): void {
const configuration: TSDocConfiguration = this._tsdocConfiguration;
const propertiesTable: DocTable = new DocTable({
configuration,
headerTitles: ['Property', 'Type', 'Description']
});
// 遍历 interface的每一个成员
for (const apiMember of apiClass.members) {
switch (apiMember.kind) {
// 如果成员是 属性的话,记录到属性表里面
case ApiItemKind.PropertySignature: {
propertiesTable.addRow(
new DocTableRow({ configuration }, [
this._createTitleCell(apiMember), // 属性名
this._createPropertyTypeCell(apiMember), // 属性类型
this._createDescriptionCell(apiMember) // 属性描述
])
);
}
}
// 这里略去对 方法成员的处理
}
if (propertiesTable.rows.length > 0) {
// 添加文档标题, 如 XX interface Properties
output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Properties' }));
// 添加上面的 属性表格
output.appendNode(propertiesTable);
}
}
自己解析并生成文档的主要难点在于对各种 case 的处理,树的每个节点大概有 20 种 case。所以可以 魔改一下 api-documenter。