30秒速览
- 不要想着纯自动化就能产出好文档,注释质量跟不上生成出来的就是垃圾,先用lint和PR Review把源头管住。把文档生成绑到CI里,但一定要做缓存和增量构建,否则队友会找你算电费。让文档适配不同角色,别只盯着开发者,给前端生成类型、给测试出矩阵、给产品画流程,这样文档才不会被晾在一边。
Swagger、TypeDoc还是自己糊?我比较了三个方案后在CI里埋了个大雷
我接手这个Node.js后端项目的时候,文档基本靠口头传述和一篇过期的Confluence页面。三十几个微服务,每个都有几十个接口,注释写得那叫一个随性——有的用JSDoc标了参数类型,有的就一行“获取数据”。我第一反应是:用swagger-jsdoc把现有的注释转成OpenAPI规范,再拿个好看的渲染器展示出来,能糊弄过去就糊弄过去。毕竟团队里没人愿意手动维护文档,能自动生成就算革命性进步了。
我就在一个中等复杂度的订单服务上做实验。给每个路由加了一堆@swagger注释,标记入参、出参、状态码。swagger-jsdoc确实能跑,但生成的spec文件里的Schema简直没法看。因为我们的请求体很多是嵌套对象,但注释里只能用基础类型,像{ id: string, items: Array }这种东西,JSDoc里写完自己都不想看第二遍。生成的OpenAPI spec里这些对象都变成了object,没有详细字段,前端看了还是懵。而且有些接口的查询参数我们定义在middleware里,swagger-jsdoc根本捕捉不到,得手动在注释里重复声明,写两遍又丑又容易不同步。试了一周,我觉得这条路走不通——不是工具不行,是我们的注释底子太薄,硬套规范只会把脏数据放大。
接下来我盯上了TypeDoc,因为团队已经在用TypeScript,而且类型定义比JSDoc精确得多。TypeDoc可以根据TS类型自动推导出函数签名,再配合typedoc-plugin-markdown,直接生成Markdown文档扔到内部Wiki里。我兴奋地在新开的用户权限微服务上试了,效果确实不错:接口的入参类型能完整展开,嵌套对象也看得清。但问题来了——TypeDoc生出来的是给开发者看的API参考,不是给前端或者产品用的交互式文档。没有请求示例、没有认证说明、没有错误码表格。我得手动在Markdown里补这些,补着补着就又回到了手工维护的老路。而且TypeDoc不支持给同一个接口生成多语言示例,前端小哥想看curl示例,测试想看Python示例,光靠TypeDoc根本满足不了。
后来我折中了一下,决定用TypeScript的装饰器直接定义元数据,再写个小工具把这些元数据转成OpenAPI spec。框架用了tsoa,因为它天然支持Express和装饰器,像@Route、@Get、@Body这样的装饰器写起来很自然,类型也能完全推断。我在一个支付回调服务上跑了完整流程:把所有路由改成用控制器类加装饰器,然后运行tsoa生成swagger.json,最后用Redocly渲染成一个漂亮的单页面。结果那次生成的文档前端看完说“终于知道传什么了”,我自己也意外发现了好几个之前没注意到的可选字段处理逻辑——因为类型系统强制我写清楚到底是不是optional,一写下去就暴露了设计的不一致。说实话,这个发现比我生成文档本身还值。但这个方案对现有代码侵入性大,要强行改造所有服务,推行阻力不小,我当时没敢在全量铺开,就先搁在几个新服务上了。
我把文档生成塞进CI后每次build都触发,结果队友骂我浪费电费
有了生成工具只是第一步,真正让文档活起来得把它嵌进开发流程里。我的理想是:开发写完代码push到分支,CI自动生成最新的文档预览链接贴在PR里,别人review代码的同时就能检查文档有没有毛病。于是我在GitHub Actions里加了一个job,每次push到非main分支都跑tsoa生成spec,再用redoc-cli构建静态页面,最后deploy到Netlify的一个临时子域名。这个流程跑通的时候我特别得意,觉得文档工程终于从石器时代跨进了自动化。
但只跑了三天,我们组的DevOps就在群里@我,说CI的分钟数消耗突然涨了40%,而且每次生成文档的job要跑将近4分钟,其中大半时间花在npm install上。我们的monorepo里有十来个包,文档生成需要把所有服务都扫一遍,安装依赖就得两分多钟。更蠢的是我一开始没设缓存,每次构建都从零装包,白白浪费资源。被骂完之后我才认真做优化:先用actions/cache把node_modules缓存下来,key里带上lock文件的哈希;再把文档生成拆成按变更路径触发,哪个服务的代码改了就只生成哪个服务的文档。但这里我又踩了个坑——tsoa生成spec的时候需要加载TypeScript的整个项目配置,会连带编译其他未变更的服务,根本做不到真正的增量。我试过用nx的依赖图来精确构建,但配置复杂度一下子上去了,考虑到团队里还没人用nx,引入成本太高,我只得妥协:把文档生成分成“全量”和“增量”两种模式,PR里跑增量(只build变更服务但实际还是编译了全部,至少没重新安装依赖),main分支合并后跑一次全量生成正式文档站。这样CI时间压到了1分20秒左右,大家才不再念叨电费的事。
另一个头疼的问题是部署预览的链接管理。Netlify每次部署会生成随机子域名,PR里的评论要自动更新链接,还得在预览过期后清理掉。我写了个自定义Action,用Netlify CLI创建site时指定一个基于分支名的alias,比如deploy-preview-feat-order-123,这样链接固定,也方便reviewer直接点开。但多个PR合并到同分支时alias会冲突,后来我又给分支名后拼了个短commit hash,总算稳定了。这些小细节花了我快两周的零碎时间,说出来都嫌啰嗦,可真把它理顺了,文档才真正成为CI流水线的一部分,不再是个“偶尔想起来跑一下”的独立脚本。
注释写得烂,生成的文档就是垃圾:我们怎么用lint和review把质量顶上去
工具和流水线搭好了,但文档质量这个事,说实话一度让我怀疑自动化到底有没有意义。有个服务生成的文档里,有一个订单状态字段的描述写着“状态”,枚举值只有0、1、2,没有任何说明。前端同事拿着这份文档问我0代表什么,我只能打开代码去找——发现原注释就只写了“状态”。这种文档生成了等于没生成,甚至更糟,因为它给了一种“有文档”的错觉。我意识到如果不从源头抓起,自动化的结果就是漂亮的垃圾。
我们开始从注释规范下手。TypeScript项目里,我们强制开启了tsdoc/syntax的eslint规则,要求所有导出的函数和接口必须有标准的@param和@returns标记,并且描述不能为空。还自定义了一个eslint规则,禁止用“获取数据”这种废话描述,必须说明获取什么数据、在什么条件下会失败。刚开始推行的时候组里骂声一片,说写注释比写代码还累,我就在一次组会上当面打开那个“状态”注释的接口,给他们看因为注释缺失导致的一次线上事故回放——一个运营错把3当成取消订单的状态码传入,结果触发了一个隐藏的退款逻辑。看完录像他们都不吭声了,之后lint规则的通过率从50%慢慢涨到了90%。
光靠lint还不够,因为lint只能检查有没有和格式对不对,语义对不对它管不了。我们在PR模板里加了一个check项:“如果新增/修改了API,是否已更新对应的文档注释并确认生成的预览无误?” 同时要求至少一个队友在review时点开预览链接,对照代码逻辑检查文档描述是否准确。这一步其实挺花时间,但因为我们把预览链接直接评论在PR里,点开很方便,大家逐渐养成了习惯。我还写了一个简单的Node脚本,在CI生成spec后跑spectral lint,用一些开源的OpenAPI规则检查是否有缺少example、是否有未定义的安全认证、是否有operationId重复等。spectral配置写得比较激进,头几次直接把好几个服务的spec挂红了,负责的开发不得不去改注释。虽然过程有点痛苦,但这种“门禁”效果特别明显,文档质量肉眼可见地提升。
中间出过一次比较搞笑的事:有个同事为了过lint,把@returns的描述写成了“返回一个对象”,spectral没有检查描述的有效性,就这么放过了。生成的文档里那个接口的返回体就是一个大括号,里面什么都没有。前端同事差点把桌子掀了。后来我干脆写了个小工具,用GPT-3.5的API去批量扫描接口注释,如果AI认为描述过于笼统,就自动在PR里追加一个评审建议,提醒负责人润色。AI当然也有判断不准的时候,偶尔会把一些技术性很强的描述判成笼统,但作为辅助手段,它帮助抓住了不少漏网之鱼。这个方法我们现在还在用,不过我也得承认,它不是一个严谨的方案,更像是一个善意的骚扰。
技术部门爱得要死,但产品经理说看不懂:我们怎么把文档变得所有人都能用
文档生成稳定之后,我以为这事儿就算干成了。直到有一天产品经理在群里截了张图,问“这个文档里为什么没有业务流程图?我想看一个订单从创建到完成的完整调用链路。”我才意识到我们一直埋头搞的是给开发看的API参考,其他角色根本插不上手。测试同事也抱怨,说文档里没有给出每个错误码对应的测试场景,他们还得去和开发逐个对齐。
我决定把文档从单一视图扩展成多角色视图。基于OpenAPI spec,我们利用x-custom扩展字段加了不少信息:比如每个接口增加了x-business-flow标记它属于哪个业务阶段,增加了x-tester-notes给测试人员写备注,还在operation的描述里用特殊格式标注了触发条件和副作用。然后写了一个生成器,读取这些扩展字段,分别渲染出面向前端的TypeScript类型文件和请求函数、面向测试的Markdown测试矩阵、以及面向产品的Mermaid流程图。前端的部分最简单,我们用openapi-typescript-codegen直接从spec生成类型和fetch封装,每次发布后自动作为npm包推送到私有源,前端安装更新就能同步。测试矩阵那块,我们把每个接口的错误码、触发条件、预期表现转成表格,测试同事可以直接拿去做测试用例设计,省了他们很多沟通时间。
最有意思的是业务流程图。我们从x-business-flow里提取接口之间的关系,再根据一套预设的链接规则生成Mermaid的sequenceDiagram。比如有x-business-flow: “order-create”的接口完成后,通常下一步是x-business-flow: “order-pay”,我们就自动画一条箭头。这套规则写死了一些常规流程,但复杂场景还是覆盖不全,产品经理说希望看到的分支逻辑没体现出来。我后来在注释里增加了一个@flow标签,允许开发手动描述业务流程,然后用NLP解析成Mermaid。这种半自动的方式效果还行,虽然人工维护的成本又涨了一点,但至少让文档变成了跨角色的沟通介质。技术团队用着爽,其他部门也愿意看了,文档才算真的活了起来。
在团队培训上我们也下了功夫。搞了一次“注释即文档”的工作坊,我把之前踩过的坑和生成的质量数据拿出来展示,给大家看一个清晰注释能节约多少沟通成本。还专门给新人对VSCode配置了snippet,输入`/**`自动补全带TODO标记的注释骨架,降低书写阻力。现在新人写的注释已经比很多老员工还规范,有时候我看到他们的PR还会觉得有点羞愧,自己当年写的注释可没这么细致。说到底,工具只是放大器,真正决定文档好坏的是团队对文档价值的认同度。我们花了半年让这个理念落地,过程中吵过架、赔过不是、也重构过好几次方案,但现在回过头看,这套工作流已经成了我们交付质量的一部分,再也回不去以前“口头API”的日子了。
注释质量才是卡脖子环节,不是工具链
流水线跑顺之后,我以为文档问题就算解决了。结果第一个月,前端同事在群里@我:”你这接口文档里userId到底传string还是number?示例里是字符串,Schema里标的integer。”我点进去一看,那个接口的JSDoc注释写的是@param {string|number} userId 用户ID——这注释本身就是糊弄学巅峰。工具老老实实地把联合类型转成了OpenAPI的oneOf,Swagger UI上渲染出来就是一个暧昧的下拉菜单,前端看了更懵。
类似的坑接二连三冒出来。有人写@param {Object} data,里面嵌套了七八个字段全都不标注,工具只能生成一个空壳Schema。有人把错误码的@returns写成了”返回错误信息”,连HTTP状态码都没标,Redoc渲染出来的响应示例就是一行"error"。还有人在注释里大段粘贴业务需求原文,真正该写的边界条件——比如”这个接口在并发场景下会返回409″——反而只字不提。我这才意识到,工具链只是在忠实地放大注释的质量:注释写得好,文档就漂亮;注释糊弄,文档就翻车。
我去翻了几个核心模块的注释覆盖率,用eslint-plugin-jsdoc扫了一轮,发现@returns的缺失率超过四成,@throws几乎是零覆盖。流水线跑得再顺,产出的文档本质上还是一个精致的空壳。我后来在CI里加了个注释lint的步骤,设了个最低阈值——覆盖率低于70%的模块直接在MR里标黄警告。这个改动比换任何渲染器都管用,因为它逼着人面对问题本身。