高效编写技术文档的实用技巧与最佳实践

a'ゞ彦会 工具 阅读 2,530
赞 18 收藏
二维码
手机扫码查看
反馈

文档这事儿,一开始真没当回事

上个月收尾一个内部工具项目,前端三人小团队,后端甩过来一堆接口文档,格式是 Markdown。我心想:不就是写文档嘛,能有多难?结果第一周就翻车了——同事按文档调接口,愣是调不通,最后发现是我漏写了某个必填参数的说明。那一刻我才意识到,文档不是“顺便写写”,而是产品的一部分。

高效编写技术文档的实用技巧与最佳实践

于是我们决定认真搞一套文档方案。需求很明确:支持多人协作、能嵌入代码示例、最好还能自动生成 API 列表。对比了几个方案(Docusaurus、VuePress、Storybook),最后选了 Docusaurus v2,主要因为它的版本管理、搜索功能和 React 集成比较顺手,而且我们团队都熟悉 React。

跑起来很简单,但细节全是坑

初始化项目确实快,npx create-docusaurus@latest my-docs classic 一敲,本地服务就跑起来了。首页、侧边栏、Markdown 渲染都没问题。但真正开始写业务文档时,问题一个接一个冒出来。

第一个问题是代码高亮。Docusaurus 默认用 Prism.js,但我们的 API 示例里有 JSON、curl、JavaScript 三种格式混排,Prism 对 curl 的支持不太友好,经常识别错语言。折腾了半天,发现得手动指定语言:

curl
curl -X POST https://jztheme.com/api/login
-H “Content-Type: application/json”
-d ‘{“email”: “user@example.com”, “password”: “123456”}’

但更麻烦的是动态数据。比如某个接口返回的 id 是 UUID,每次都不一样,写死在文档里容易误导。我试过用占位符 {user_id},但新来的实习生还是照着复制粘贴,结果报错。后来干脆在代码块下面加一行注释:

// 注意:实际响应中的 id 是随机生成的 UUID,此处仅为示例

最大的坑:API 文档自动化

手动维护 API 文档太痛苦了,尤其后端三天两头改字段。我一度想直接放弃,但想到之前踩过的坑,还是咬牙搞自动化。

后端用的是 OpenAPI 3.0 规范,理论上可以用 swagger-uiredoc 直接渲染。但 Docusaurus 不原生支持,得自己集成。我先试了 @docusaurus/plugin-content-docs + 自定义 MDX 组件,结果发现每次构建都要重新拉取 OpenAPI 文件,CI/CD 经常超时。

后来调整方案:写个脚本,在 prebuild 阶段把 OpenAPI JSON 转成 Markdown 片段,存到 docs/api/ 目录下。核心逻辑就这几行:

// scripts/generate-api-docs.js
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');

async function generateApiDocs() {
  const response = await fetch('https://jztheme.com/api/openapi.json');
  const spec = await response.json();

  let mdContent = '# API 文档nn';
  for (const [path, methods] of Object.entries(spec.paths)) {
    for (const [method, operation] of Object.entries(methods)) {
      mdContent += ## ${operation.summary || ${method.toUpperCase()} ${path}}nn;
      mdContent += httpn${method.toUpperCase()} ${path}nnn;
      if (operation.parameters) {
        mdContent += '### 参数nn';
        operation.parameters.forEach(param => {
          mdContent += - ${param.name} (${param.in}): ${param.description || ''}n;
        });
        mdContent += 'n';
      }
    }
  }

  fs.writeFileSync(path.join(__dirname, '../docs/api/generated.md'), mdContent);
}

generateApiDocs().catch(console.error);

然后在 package.json 里加个钩子:

{
  "scripts": {
    "prebuild": "node scripts/generate-api-docs.js",
    "build": "docusaurus build"
  }
}

这样每次构建前都会自动更新 API 文档。亲测有效,但有个小问题:如果 OpenAPI 服务挂了,整个构建就失败。后来加了重试和缓存机制,不过还是偶尔会因为网络波动中断。目前折中方案是:本地开发时跳过这步,只在 CI 环境强制执行。

样式和体验的微调

默认主题太素了,产品经理看了直摇头。我们花了一天时间定制 CSS,主要是调整代码块背景色、字体大小,还有侧边栏的折叠逻辑。Docusaurus 的主题定制还算友好,通过 src/css/custom.css 覆盖变量就行:

/* src/css/custom.css */
:root {
  --ifm-code-font-size: 90%;
  --ifm-code-background: #f8f9fa;
  --ifm-menu-link-padding-vertical: 0.4rem;
}

.docusaurus-highlight-code-line {
  background-color: rgba(0, 0, 0, 0.1);
  display: block;
  margin: 0 -0.5rem;
  padding: 0 0.5rem;
}

另外,我们加了个“复制代码”按钮。Docusaurus 官方插件 @docusaurus/theme-live-codeblock 功能太重,最后用了个轻量级方案:在 docusaurus.config.js 里注入一段 JS,监听页面上的所有 <pre> 标签:

// docusaurus.config.js
module.exports = {
  themes: [],
  scripts: [
    {
      src: '/js/copy-code.js',
      async: true,
    },
  ],
};

对应的 static/js/copy-code.js 内容如下:

document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('pre').forEach(pre => {
    const button = document.createElement('button');
    button.textContent = 'Copy';
    button.className = 'copy-button';
    button.style.position = 'absolute';
    button.style.top = '4px';
    button.style.right = '4px';
    button.style.padding = '2px 6px';
    button.style.fontSize = '12px';
    button.style.background = '#eee';
    button.style.border = '1px solid #ccc';
    button.style.borderRadius = '3px';
    button.style.cursor = 'pointer';

    button.onclick = () => {
      const code = pre.querySelector('code').innerText;
      navigator.clipboard.writeText(code).then(() => {
        button.textContent = 'Copied!';
        setTimeout(() => button.textContent = 'Copy', 2000);
      });
    };

    pre.style.position = 'relative';
    pre.appendChild(button);
  });
});

这里注意我踩过好几次坑:一是要等 DOM 加载完再执行,二是 navigator.clipboard 在非 HTTPS 环境可能被禁用,得加 try-catch。不过内部工具走 localhost,问题不大。

回顾与反思

折腾了两周,文档站点终于上线了。效果嘛,整体还行:API 自动同步省了大量人力,代码示例清晰,新人上手快了不少。但也有几个没完全解决的问题:

  • OpenAPI 生成的 Markdown 缺少请求/响应体的详细结构,还是得手动补充
  • 移动端体验一般,代码块横向滚动不够流畅
  • 搜索功能只能搜标题,搜不到代码块里的内容

不过这些都不影响主流程,暂时搁置了。毕竟文档的核心价值是“准确”和“及时”,美观和高级功能可以慢慢迭代。

回头想想,其实文档工具选型不是关键,关键是团队有没有共识:文档不是附属品,而是交付物的一部分。现在每次 PR 合并,我们都会检查是否更新了相关文档,这个习惯比任何工具都重要。

以上是我个人对这个文档系统的完整折腾过程,有更优的实现方式欢迎评论区交流。比如你们是怎么处理动态 API 示例的?或者有没有更好的 OpenAPI 集成方案?这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论