维护开源软件的网站
文档对开源软件的重要性毋庸置疑,毕竟用户很少会对一上来就阅读源码感兴趣。一些简单软件可以只使用 README,但更多的软件需要上百页相互链接的文档来展现自己的功能。为了更好地展示这些文档,维护者们往往会选择创建一个网站。
此时,文档的维护变成了两件事:
- 维护内容;
- 维护网站;
内容当然是很重要的;而维护网站,同样重要。维护不当,网站会变成项目的累赘。
一个例子
以本人参与维护的 Apache APISIX 为例,整个项目的文档分散在
APISIX 主仓库、
APISIX Docker、
APISIX Dashboard、
APISIX Ingress Controller,以及
APISIX Website 等多个仓库中。
屏幕前的各位也许觉着一般。但对本人这种没怎么接触开源项目的菜鸟而言,软件和文档的复杂度已经相当高了。
由于早期并未参与网站建设,待本人接手时,网站已经遇到了不少问题,比如:
- 网站框架陈旧,缺少一些文档维护者需求的功能(Mermaid、图片懒加载等)。
- 网站会构建所有子项目、所有版本的文档,导致构建时间过长。
- 网站首页加载过多内容,非常缓慢。
- 网站的博客部分展示效果与文档相同,极其丑陋。
- 网站框架与文档内容分离,导致文档维护者无法及时预览文档的渲染效果。
目前诸位能看到的网站,是重构后的版本。受限时间和精力,当时只实现了重写博客、减少首页加载内容、改进构建方式。由于变更不够彻底,加上没能在更换框架方面说服项目主要维护者,许多问题依然存在。时至今日,有些问题已经变得难以接受,亟待改进。
从 2021 年底到现在,本人对该类问题有了些许思考,写成此篇文字,权当抛砖引玉。如能对各位有所帮助,那就太棒了。
选择
选择网站框架不是个难事。生态繁荣、持续更新、文档清晰、社区活跃的框架,很难遇到底子上的问题。唯需理解的是,也许某些网站框架能在初期快速满足一些需求,后续的定制化、维护升级成本却很高。
注意,本文希望谈论的是 SSG,即静态网站生成器。使用 CMS 似乎不是开源项目常见的选择,不仅不方便项目的维护者协作,还可能带来安全问题(比如 WordPress)。至于 SSR,目前没有找到采取这种方案的必要性。
Docusaurus
如果各位对 React 比较熟悉,想必听过同为 Facebook 出品的 Docusaurus。这也是 Apache APISIX 在使用的网站框架。
对于一个文档内容不多,没有任何定制化需求,最好是滚动更新没有版本化的网站来说,Docusaurus 用起来没问题。
然而一旦文档数量激增或需要深度定制,Docusaurus 的缺点便会暴露无遗。
Docusaurus 在构建时会把所有内容加载进内存,对于文档量巨大的项目而言,这无疑是一场灾难。早期 Apache APISIX 网站的构建时间甚至长达一小时。
想渲染一些特殊的内容,比如用 Redocusaurus 来渲染 OpenAPI 文档,不止构建时间难以接受,占用的内存也是可怕得很。在参与过的项目中,出现过渲染几个稍大一些的 OpenAPI 文档(1~2MB),就让 4C8G 的机器(Vercel Pro 套餐)直接 OOM 的情况。这自然说明了 Redocusaurus 的实现存在问题,但与 Docusaurus 将内容都加载到内存中的特性脱不开干系。
Docusaurus 虽然使用 React 开发,在主题定制方面却异常不灵活,必须使用一个名为 swizzle 的特性。而涉及到 swizzle,一旦有升级的打算,大量修改是免不了的。
Apache APISIX 的网站停留在 2.0 beta 版本,原因正是在 2.0 beta 版本后、正式版本前,Docusaurus 对 swizzle 相关的功能进行了大刀阔斧的修改。Docusaurus 的升级文档也并不清晰,导致后续很难在较短的时间内完成升级工作。当然,使用 beta 版本是那时对 Docusaurus 太有信心了,以为能和 React 一样,版本间提供良好的兼容性。
此外,在某个本人参与过的项目中,使用了 Docusaurus 2.4.x 版本,由于开启了 swizzle 特性,导致在升级到 3.8.x 版本时,需要进行增删了各 6K 行的修改。因为某些选项的调整,后续又提交了多个 PR 以修复没有在升级指引中提及的修改。──可见至今,Docusaurus 的升级指引仍不完善,swizzle 也与破坏性变更常伴。
总之,即使各位维护的软件还在早期阶段,只要能预测到未来文档会变得很多,不想构建速度太慢,或者有很多定制化需求,请不要选择 Docusaurus。
想踩坑,请先看看 Docusaurus 的 Issues,包括关闭的,有些 Issue Open 的时间可不止一两年。
此外,作为一个主要由前端开发者打造的框架,Docusaurus 的默认审美也实在有些……难以言说。
Hugo
如果项目有非常多(1k+)的文档,Hugo 是个不错的选择。Hugo 是 go 语言开发的。
Hugo 的好处是可以在不怎么修改网站框架的情况下,实现很多定制化需求,并更晚遇到性能问题。
坏处是专精 JS 的网站维护者,可能对 go 语言不够熟悉,导致在某些功能上,难以上手或产生排斥。(不过现在有 AI,简单的语法、技巧难成问题。)
请放松,Hugo 的文档和设计不错。自定义程度相当高,能满足大部分需求。只要不是已经深陷 Next.js 或类似框架的开发者,应该不难用起来。
理论上说,使用 Hugo 的大部分时间只是和 template 打交道,该由 css 和 js 实现的功能,还是需要使用前端的工具链。
VitePress,Astro 等基于 Vite 的框架
不知不觉,Vite 的使用量已经相当高了。不管是 Vue,Svelte,Solid,甚至是 React,都能使用 Vite 来构建。基于 Vite 的网站框架,也相当多。愿意只用 Vue 的,可以尝试 VitePress;想不被任何库绑架的,可以尝试 Astro。还有其他不少网站框架,这里没法一一列举了。
这些框架基于 Vite 构建,在生态上,他们能共享很多东西。除了某些库限定的功能,大概能找到任何想要的东西。
至于构建速度,大体比 Hugo 要慢不少,比 Docusaurus 要快一些。
注意,像 VitePress 没有内置 Docusaurus 中所谓的 Blog 特性,但本人不认为这是一个缺点,详细请继续看下文。
Gatsby 和其他不再更新的框架
本人参与过 OI-Wiki 网站的 Next 版本开发,当时采用的是 Gatsby。Gatsby 的设计非常不错;如果想自己做一个「内容提供层」,推荐去参考一下。不知为什么,没有流行起来,也已经很久没有更新了。
从维护角度看,请不要使用任何缺乏更新的框架。除非准备好完全自己来维护了。
结构
子项目中的文档
如果维护的内容在同一仓库中,遇上的问题都会少很多。
麻烦的是像 Apache APISIX 这样,内容分散在多个仓库中的。
本人对这种分散没有意见。反之,将多个子项目的内容放在同一仓库中,更新完子项目功能还需要在另一仓库中创建 PR、更新文档,在流程上存在割裂。
对于子项目中的文档,约定好统一的结构即可。比如:
- docs/
- latest/
- en/
- index.md
- zh/
- index.md
- v3.0/
- en/
- index.mdx
- zh/
- index.mdx
网站仓库
对于承载网站本身和剩余其他内容的仓库,推荐使用 monorepo。
且最重要的一点是:拆!
已经有非常重历史包袱的项目,拆起来费时费力,除了考虑兼容历史遗留,还需考虑其他维护者在 review 代码时的感受。所以对于新项目,请务必考虑拆分,应拆尽拆。
以 APISIX Website 为例,更好的做法是:
- 将 blog、docs 和其他页面的 代码部分 分别拆到
apps/
的各自文件夹中。 - 将 blog 和 docs 的 内容部分 拆到
content/
的各自文件夹中。(这里的 docs 是指剩余的独立文档,不包括子项目中的文档。) - 对于共用的组件(header、footer)等,拆到
pkgs/
的components
文件夹中。
最后看起来像这样:
- apps/
- blog/
- docs/
- website/
- pkgs/
- components/
- content/
- blog/
- docs/
拆开后,所有的部分都可以单独维护。对依赖的升级,完全能分开进行。某天想拆到另一个仓库,也不是问题。
这也是为什么本人认为其他框架没有内置 Docusaurus 中 Blog 特性,是一个缺点。博客本身就跟文档关系不大,而其他专门的 Blog 框架也大概率做的比 Docusaurus 的 Blog 要好得多。
还有一个额外的好处是,文档部分的代码和内容完全分离,其他子项目可用 git submodule
等方式引 apps/docs
构建预览页面,非常方便。(APISIX Website 现存的处理方式做这件事相对困难,会构建太多不需要的东西。)
构建
Docusaurus 有一个官方推荐的实践:将旧版本的文档 archive 到另一个子域名中。此时,存在一些问题:
- 旧版本如何提供版本切换按钮,看到最新的文档链接?
- 旧版本如何提供一个 Banner,告知用户最新的 release 版本(非 latest)链接?
- 旧版本文档更新(patch)时,如何更新该版本旧文档?
- 何时存档旧版本文档?如何在主站添加这些旧文档的链接?
这些问题,有的可以通过脚本自动或半自动化,有的则需要手动处理。手动处理也不难,但在开源项目中,如果能减少维护,自然是更好的。
另外本人更倾向于将所有能暴露的东西都暴露出来,像 archive 到子域名这件事,非 maintainer 肯定是操作不了了。一定程度上阻碍了潜在的维护者参与。
而基于前文的「结构」一节,可以做一件很合理的事情:将每个子项目每个版本的文档分开构建,生成到 /{project}/{version}
下。
再加一些细节,就能解决上述这几个问题:
- 版本切换的下拉框,我们可以用一个
JSON
文件维护,组件放到Header
中。 - Banner 处理方式同上。
- 旧版本更新可以自动处理,在构建时根据 git commit SHA 来判断是否需要构建即可。
- 旧版本一直在构建缓存中,已经没有这个问题了。假使历史文档太多,缓存都存不下,可以回到 Docusaurus 的方案,将旧版本文档 archive 到另一个子域名中。
如果已经采取了 Docusaurus 推荐的措施,即使像本文说的那样进行了改造,也仍然可以不处理已经 archive 的旧版本文档。 (能轻装上阵,为什么不呢?)
具体的构建实现,难度不高,不细说了。
总结
文中提出的思路,是基于本人参与维护的 Apache APISIX 网站的实践。每个开源项目有自己的情况,请根据实际情况参考。
(没错,为了吐槽 Docusaurus 才写的这篇。😅)