都2025年了,还在用Markdown写文档吗?

3,979 阅读5分钟

俗话说得好:“程序员宁愿写 1000 行代码,也不愿意写 10 个字的文档。”

不愿写文档的原因,一方面是咱理科生文采确实不好,另一方面则是文档的更新维护十分麻烦。

每次功能有变更的时候, 时间又急(其实就是懒),很难想得起来同时去更新文档。

特别是文档中代码片段,总是在几天之后(甚至更久),被用户找过来吐槽:“你们也太不专业了,文档里的代码都跑不通。”

作为有素养的程序员,被人说 “不专业”,简直不能忍,一定要想办法解决这个问题。

专业团队

文档代码化

很多开发者喜欢用语雀,飞书或者钉钉来写文档。

不得不承认,它们的编写和阅读的体验更好,样式也更丰富。

甚至就算是版本管理,语雀,飞书做得也不比 git 差。

不过对于开发者文档,我觉得体验,样式都不是最重要的。毕竟这些都是锦上添花。

更重要的是,文档内容有效性的保证,如果文档上的代码直接复制到本地,都要调试半天才能跑通,那不管它样式再好看开发者都要骂娘了。

所以文档最好就和代码放在同一个仓库中,这样代码功能有更新时,顺便就把文档一起改了。团队前辈 Review 代码时,也能顺便关注下相关的文档是否一同更新。

如果真的一定要搞一个语雀文档,也可以考虑用 Git Action,在分支合并到 master 时触发一次文档到语雀的自动同步。

Markdown 的问题

程序员最常用的代码化文档就是 Markdown 了,估计也是很多开发者的首选,比如我这篇文章就是用 Markdown 写的。

不过 Markdown 文档中的代码示例,也没有经过单元测试的验证,还是会出现文档中代码跑不通的现象。

Python 中有一个叫做 doctest 的工具,能够抽取文档中的所有 python 代码并执行,我们只要在分支合并前,确保被合并分支同时通过了单元测试和 doctest,就能保证文档中的代码示例都是有效的。

在 Java 中我找了半天没有找到类似工具,很多语言(比如 Go, Rust 等等)据我所知也没有类似的工具。

而且对于 Java,文档中给的一般都是不完整的代码片段,无法像 Python 一样直接就能进入命令行执行。

有句俗话 ”单元测试就是最好的文档“。我觉得没必要将单元测试和文档分开,最好的方式就是从单元测试中直接引用部分代码进入文档。

在变更功能时,我们一定也会改单元测试,文档也会同步更新,不需要单独维护。

在合并分支或者发布版本之前,肯定也会有代码门禁执行单元测试,这样就能确保文档中代码示例都是有效的。

目前我发现了一个能解决该问题的方案就是 adoc 文档。

adoc 文档

adoc 的全称是 Asciidoctor, 官网链接

Github 已经对其实现了原生支持,只要在项目中将 README 文件的后缀改成 README.adoc,Github 就会按照 adoc 的语法进行解析展示。

adoc 最强悍的能力就是可以对另一个文件的一部分进行引用。以我负责的开源项目 QLExpress 为例。

在单元测试 Express4RunnerTest 中,用 // tag::firstQl[]// end::firstQl[] 圈出一个代码片段:

// import 语句省略...

/**
 * Author: DQinYuan
 */
public class Express4RunnerTest {
    // 省略...

    @Test
    public void docQuickStartTest() {
        // tag::firstQl[]
        Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
        Map<String, Object> context = new HashMap<>();
        context.put("a", 1);
        context.put("b", 2);
        context.put("c", 3);
        Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS);
        assertEquals(7, result);
        // end::firstQl[]
    }


    // 省略...
}

然后在文档 README-source.adoc 中就可以 firstQl 这个 tag 引用代码片段:

=== 第一个 QLExpress 程序

[source,java,indent=0]
----
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl]
----

include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl] 用于引用 Express4RunnerTest 文件中被 firstQl tag 包围的代码片段,其他的部分,等价于 Markdown 下面的写法:

### 第一个 QLExpress 程序

```java
```

这个 adoc 文档在渲染后,就会用单测中真实的代码片段替换掉 include 所占的位置,如下:

adoc渲染示例

缺点就是 adoc 的语法和 Markdown 相差还挺大的,对以前用 Markdown 写文档的程序员有一定的熟悉成本。但是现在有 AI 啊,我们可以先用 Markdown 把文档写好,交给 Kimi 把它翻译成 Markdown。我对 adoc 的古怪语法也不是很熟悉,并且项目以前的文档都是 Markdown 写,都是 AI 帮我翻译的。

Github 渲染 adoc 文档的坑

我最开始尝试在 Github 上用 README.adoc 代替 README.md,发现其中的 include 语法并没有生效:

Github 对于 adoc include的渲染逻辑还挺诡异的,既不展示引用文件的内容,也没有原样展示 adoc 代码

Github对于adoc的错误渲染

查询资料发现 Github 根本不支持 adoc 的 include 语法的渲染(参考)。不过好在参考文档中也给了解决方案:

  • 源码中用 README-source.adoc 编写文档
  • 使用 Git Action 监听 README-source.adoc 文件的变化。如果有变动,则使用 asciidoctor 提供的命令行工具先预处理一下 include 语法,将引用的内容都先引用进来。再将预处理的后的内容更新到 README.adoc 中,这样 README.adoc 就都是 Github 支持的语法了,可以直接在 Github 页面上渲染

Github Action 的参考配置如下(QLExpress中的配置文件):

name: Reduce Adoc
on:
  push:
    paths:
      - README-source.adoc
    branches: ['**']
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3
      - name: Install Asciidoctor Reducer
        run: sudo gem install asciidoctor-reducer
      - name: Reduce README
        # to preserve preprocessor conditionals, add the --preserve-conditionals option
        run: asciidoctor-reducer --preserve-conditionals -o README.adoc README-source.adoc
      - name: Commit and Push README
        uses: EndBug/add-and-commit@v9
        with:
          add: README.adoc

添加这个配置后,你会发现很多额外的 Commit,就是 Git Action 在预处理 README-source.adoc 后,对 README.adoc 发起的提交:

image.png

至此,就再也不用担心被人吐槽文档不专业啦。