代码迭代速度快、文档更新跟不上,是绝大多数团队的常态。等前端找上门说接口对不上时,后端才慌忙翻代码补文档,效率极低。与其靠人肉维护,不如让机器来做:只要在代码里写好规范的注释,文档的生成、构建、发布全部交给CI流水线,每次合并代码就自动产出一版最新的API文档。下面以Node.js项目为例,完整讲一遍这套方案怎么落地。

一、为什么选择从代码注释生成文档
API文档的维护方式大致分三类:手写Markdown、用Postman等工具单独管理、以及从代码直接生成。手写文档的最大问题是无法和代码强关联,改了接口签名文档还是旧的;Postman集合虽然好用,但它和代码库是分离的,同样存在同步成本。
从代码生成文档的核心优势在于单一数据源:接口的真实定义只存在于代码中,文档只是代码的一种呈现形式。只要生成过程接入了CI,每次代码变更都会触发重新构建,文档永远和代码保持一致。这种方式还有一个隐性好处,就是倒逼开发者写注释,因为注释就是文档本身,不写注释文档就是空白,评审时一目了然。
在Node.js生态里,主流方案有两套:一套是JSDoc配合docdash等模板,适合纯JS库文档;另一套是OpenAPI(Swagger)规范配合swagger-jsdoc,适合RESTful API服务。做HTTP接口文档,推荐后者,因为它能直接对接Swagger UI,前端可以在页面上调试接口。
二、在Express项目中编写注解并生成文档
首先安装依赖:
npm install swagger-jsdoc swagger-ui-express --save npm install jsdoc-to-markdown --save-dev
接着在路由文件中给每个接口写上OpenAPI格式的注释。注意注释必须以@openapi或@swagger开头,swagger-jsdoc靠这个标记识别哪些注释需要收集:
/**
* @openapi
* /api/users:
* get:
* summary: 获取用户列表
* description: 分页返回系统中的用户数据
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* responses:
* 200:
* description: 成功返回用户列表
* content:
* application/json:
* schema:
* type: object
* properties:
* total:
* type: integer
* list:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/
router.get('/api/users', UserController.list);然后在项目入口注册文档路由:
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '订单服务API',
version: '1.0.0',
},
},
apis: ['./src/routes/*.js'], // 扫描所有路由文件中的注解
};
const spec = swaggerJsdoc(options);
app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec));这样本地启动服务后访问/docs就能看到可交互的文档页面。但这只是第一步,我们的目标是让文档脱离本地服务也能访问,这就需要把OpenAPI规范导出成静态JSON文件,交给CI去构建发布。
写一个独立的生成脚本scripts/gen-docs.js:
const fs = require('fs');
const swaggerJsdoc = require('swagger-jsdoc');
const spec = swaggerJsdoc({
definition: {
openapi: '3.0.0',
info: { title: '订单服务API', version: process.env.npm_package_version || '1.0.0' },
},
apis: ['./src/routes/*.js'],
});
fs.mkdirSync('./dist-docs', { recursive: true });
fs.writeFileSync('./dist-docs/openapi.json', JSON.stringify(spec, null, 2));
console.log('文档已生成到 dist-docs/openapi.json');在package.json中加一条script:"gen-docs": "node scripts/gen-docs.js"。这个脚本不依赖任何运行中的服务,非常适合在CI环境里执行。
三、接入CI流水线自动构建发布
有了生成脚本,CI集成就很简单了。以GitHub Actions为例,配置文件.github/workflows/docs.yml如下:
name: Deploy API Docs
on:
push:
branches: [main]
paths:
- 'src/routes/**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run gen-docs
- name: 部署到GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist-docs这里有个值得注意的细节:paths过滤条件配置成了只在路由文件变化时触发,避免改个README也重新构建文档,浪费CI资源。
纯JSON不太适合人阅读,可以再用redoc渲染成漂亮的静态页面,在流水线里加一步:
- run: npx @redocly/cli build-docs dist-docs/openapi.json --output dist-docs/index.html
如果是GitLab CI,思路完全一致,用pages这个内置job把产物放到public目录即可:
pages:
stage: deploy
image: node:20
script:
- npm ci
- npm run gen-docs
- npx @redocly/cli build-docs dist-docs/openapi.json --output public/index.html
artifacts:
paths:
- public
only:
- mainJenkins用户则可以在Jenkinsfile里用一个stage执行npm run gen-docs,然后通过publishHTML插件或者rsync把产物推到内部Nginx服务器,逻辑没有任何区别。
四、几个容易踩的坑
第一个坑是YAML缩进错误。OpenAPI注解本质是YAML语法嵌在注释里,缩进差一个空格swagger-jsdoc就可能解析失败或静默忽略某个接口。建议在CI里加一个校验步骤,用npx @redocly/cli lint dist-docs/openapi.json检查规范文件的合法性,有问题直接让流水线失败,早发现早处理。
第二个坑是版本号不更新。文档里的version字段最好从package.json读取,并且要求涉及接口变更的提交必须升级版本,这样文档页面上能清楚看到当前是哪一版接口,出问题时方便回溯。
第三个坑是敏感接口泄露。内部管理接口如果也被扫描进文档并发布到公网,会带来安全隐患。可以在apis配置里区分扫描路径,或者利用OpenAPI的tag分组,在生成脚本中过滤掉标记为internal的接口:
// 过滤内部接口,只保留对外暴露的部分
spec.paths = Object.fromEntries(
Object.entries(spec.paths).filter(([path, methods]) =>
!path.startsWith('/internal')
)
);最后一个建议是把文档的链接直接输出到钉钉或企业微信的群机器人里,CI最后一步用Node写个十几行的webhook脚本,每次发布成功就推送一条带文档链接的消息,团队成员自然会被引导去使用最新文档,形成正循环。
整体来看,这套方案的成本集中在初期注解规范的建立上,一旦团队养成习惯,后续的维护开销几乎为零。代码合并到主干,几分钟后一份带版本号的、可交互的API文档就自动上线,这才是工程化该有的样子。