不知道现在大家写博客、文章还多不多,我一直在用 Obsidian + Markdown 写文章,然后用 Hexo 生成静态站点发布到 GitHub Pages,绑定到域名 xiaoming.io。
几年前我写过一篇文章,分享我是怎么构建笔记和博客系统的。
遇到问题
最近我在 Obsidian 里面用了一个新插件,它会把我本地的图片转成 WebP 格式保存。这样能大大节省存储空间,图片画质也还不错。
用了 WebP 之后,我发现原先那套发博客的代码只支持JPG、PNG、GIF,没有正常把 WebP 图片复制到发布目录里。
我的第一反应是让 Codex 直接帮我解决这个问题,它 Debug 了半天,没有什么进展,看来还是需要我人工参与一下。
我又重新研究了一下 Hexo,已经很多年没研究它了。之前它有个问题,在本地写 Markdown 时,如果用相对路径引用本地图片,在有些情况不会把图片复制到 public 目录。
几年前,为了解决这个问题,我魔改了 Markdown 的渲染器,也就是 hexo-renderer-markdown-it。在它解析完图片以后,我会把每一张图片都复制到固定目录,同时修改渲染的图片 URL 让其对应上。
当时之所以这么做,是因为我发现 Hexo 本身的 API 不会把每个 post 里的图片都暴露给我。为了拿到一篇 Markdown 里所有图片的路径,我就不得不在渲染器里插一段自己的代码,来实现这个效果。
但这种实现思路非常不好,耦合性太强。我改了现有渲染器以后,只要渲染器一更新,我又得重新改。这么多年来,我一直用的都是旧版本渲染器,没有更新。
最近我又看了一下,时隔多年,Hexo 还是没有原生很好的解决这个问题。尽管它有一个 post_asset_folder 参数,但这个参数只支持和 Markdown 文件同名的文件夹里的图片上传,也就是推荐在 Hexo 里面创建 Markdown,这太局限了。
我也尝试在网上找现成插件,确实有一些插件支持这个功能,例如 hexo-asset-image,但这个项目已经无效并且不维护了。有人Fork了一个版本修复,但是看起来和我想要的效果不太一样。
决定重新开发(第一版)
所以我决定让 Codex 帮我开发一个。我本来不知道这件事会不会很复杂,没想到 Codex 直接帮我写了一个不到 100 行的 script,放进项目里以后,图片确实就能复制了,比我原先的实现优雅很多。
Codex 的实现思路很直接,就是拿到 Markdown 源文件,然后用几个正则表达式去找里面所有图片,再把它们复制到目标目录。
其实当年我一开始想到的也是这种方式。正则表达式比较难写,担心写错,识别出错误图片或者漏掉,所以当时就直接走了魔改 Markdown 渲染器这条路,快速把问题解决了。
但在今天,AI 写代码已经这么强了,让 Codex 直接实现一个新的插件,反而是一个更快、更简单的办法。
于是我把这个插件发布到了 NPM 上,也把我原来那个魔改插件去掉了,重新改回原版的 Markdown 渲染器,并且升级到了最新版。
后来我又去 Hexo 的 GitHub 主页看了一下,到现在还是一直有人提这个问题。我把这个插件网址回复在了那个 GitHub Issue 里。不知道这些提 Issue 的人现在还在不在用 Hexo 写博客,还是已经迁移到别的工具上了。
发现更多问题(第二版)
当我实际用这个插件时,发现它对图片的判断还是太简单了。Markdown 里引用图片有很多种情况。
大概有这么几类:
-
相对路径,比如当前目录、子目录,或者父目录里的图片
-
绝对路径,比如以斜线开头的 Linux 路径,或者 Windows 上带盘符的路径
-
不是本地文件路径的情况,比如
data:这种 Base64 形式、#开头的路径,或者http、https这种链接
我去和 Codex 讨论,它只处理了 https 链接等场景,还有一些场景都没有处理。
于是我就和 Codex 继续讨论,把我想到的场景都补充进去了,也写了针对这些场景的测试代码。
写 test 也有一些细节。因为这是一个 Hexo 插件,理论上最彻底的测试方式,应该是端到端测试:准备一个真实的 Hexo 环境,放一些测试用的 Markdown 文件,运行插件和编译过程,再判断处理结果对不对。
但 Codex 认为这样做会依赖 Hexo 的版本,太重了,比较容易出现兼容问题。所以它建议我用集成测试:Mock 一个假的 Hexo 环境调用整个插件,判断它是不是按照设计的方式工作,有没有准确复制 Markdown 文件里引用的图片。
当时我觉得 Codex 说的有道理,集成测试也就够了,因为主要是测试这些格式的解析逻辑。
于是第二版就发出来了。
更全面系统的设计(第三版)
在我写这篇文章的时候,又去网上搜了一些相关的资料,包括 Hexo 官方文档,我发现事情没有这么简单。
例如 Hexo 里 relative_link 的配置选项可能会导致渲染不一样,还有些用户可能会在图片文件名里有特殊字符等。
我觉得有必要用更系统化的方式去实现这个工具。
在之前,我用 AI 做过好多个小工具,说实话,这个应该是真实解决现实问题当中功能最简单的一个项目。
但是正因为它是最简单的一个,我反而可以更清晰地学习如何用 AI 开发项目。因为它每一个步骤都相对简单,我可以把重点放在思考开发流程上,而不是让注意力陷入过多设计实现细节里出不来。
同时它也不是简单到完全不需要思考的程度,它也在解决一个有多种可能分支的现实问题,有一定的研究价值。
1、确定核心目标
首先是理清这个项目的核心目标。核心目标的重点不只是这个项目要做什么,还要关注这个项目不做什么。
这个项目的名字叫 hexo-relative-post-image,顾名思义,它的核心目标应该是只处理相对路径引用的图片文件。如果是绝对路径,我们直接不处理,并且在 log 里面报一个 warning,通知用户自行处理。
我们的设计思路是,只做图片复制,不去干涉 Markdown 渲染成 HTML 的过程。
2、确定调研方向并调研
确定了这个核心目标以后,我们需要对 Markdown 有关的东西进行足够深入、系统的调研。
这里面包括 CommonMark 对 Markdown 语法的规定是什么样的、支持什么样的图片引用方式。
现有的 marked 和 markdown-it 这两个渲染器是怎么实现 Markdown 图片渲染的,这两个渲染器在 Hexo 上又有什么样的图片相关的配置参数。
目前已有的 Hexo 渲染器,用户都报了什么样的 Issue,能从这个 Issue 里面学习到哪些兼容性问题。
我让 AI 把这些调研来源和结果都放在了 Reference 文件中。
3、Spec 开发
在这个调研完成以后,就需要做一个详细的需求文档,一般称为 Spec (Specification,产品规格)。这个 Spec 里面除了有项目的核心目标,还有从调研结果学来的东西,有哪些语法格式需要考虑,有哪些可能的兼容性问题需要处理。
根据我们前面定的核心目标,有些兼容问题是可以在插件里处理的,而有些兼容问题则是和我们的设计目标冲突的,应该不做处理,而是直接报错抛给用户,例如绝对路径引用图片。
一开始我并没有把这个项目当成一个正经项目去做,只是从一个 100 行的小脚本慢慢补全了,所以前面的版本也没有 Spec。但是现在既然要更系统地实现这个项目,Spec 就是必要的。
一开始都是跟 AI 用几句话来描述需求,但是当需求的边界条件、各种分支越来越多,几句话就描述不清楚了。如果下次需要 AI 修改维护,Spec 就是最重要的依据。聊天结束了,上下文就没了,而 Spec 是以文件形式保存在项目里的,一直在。
4、代码实现
有了这个 Spec 以后才是写代码,代码分为插件本身的实现代码和测试代码两块。
实际上这个插件本身的代码量并不是很大,只有一个几百行的文件。但它的文档以及它的测试代码反而比实现代码更复杂。
5、测试
在测试代码的实现上,之前的集成测试也满足不了需求了,因为现在已经明确发现了不同的渲染插件可能也会有兼容性问题。
所以我让 AI 又加了真正的端到端测试,直接调用 Hexo 去验证兼容性问题。
6、README 文档
最后,还有一个精简的 README,是中英双语版的,为了更加国际化,默认英文版。README 分为两块。
1、给普通用户看的,要告诉用户怎么配置这个项目,支持什么场景,什么是不支持的。
2、给想要参与开发项目的人和 AI 看的。会介绍环境配置、开发约定、具体的发布流程。
实际上我的开发和发布都是让 AI 完成的,这个时候文档就非常重要了,如果没有文档,AI 没办法每次都按照固定的流程开发和发布项目,到时候就乱了。
一点思考
这是一个很简单的小项目,用 AI 帮忙写也很轻松。但你会发现,即使是这样的小项目,也还是需要人工参与,而且人工参与的程度并不低。
这不是我不会用 AI,而是很多实际问题本身就带着一堆隐性的背景知识。
比如 Markdown 引用图片这件事,由于Markdown编辑器五花八门,用户真的会用各种奇奇怪怪的方式去写,不只是标准语法,还可能有 HTML 标签、文件名有特殊字符、不同语法格式、网络图片,甚至不同渲染器还有自己的扩展。这些东西本身就没有一个固定范围。
AI 默认不会把这些情况全都想一遍,它一般只会给你一个“常见写法”的方案,而不会一上来就帮你把所有边界情况都覆盖掉。
如果要让 AI 自主探索所有可能的情况,复杂度可能会指数级增加。
这时候人的作用就体现出来了。我会用自己的经验去引导 AI,比如我会想到要看现有生态,比如 CommonMark 语法规范,Marked、Markdown-it 这些主流渲染库,相似插件以及它们的 Issue 里别人踩过的坑。
这些东西 AI 不是查不到,而是如果你不说,它通常不会主动意识到“这一步是必要的”,也不会自己把这些上下文串起来。
本质上就是,有些问题需要很大的上下文才能想清楚,而这些上下文很多是隐性的,不是直接搜就能出来的。
人能做的,是主动把这些上下文补上,确定问题范围;AI 更擅长的是,在这个范围里面把事情做快、做全。
那么利用 AI 开发项目的过程就是这样的:
1、我会利用我的知识经验,引导 AI 去做调研,确定问题范围,让 AI 写 Spec,写完了我再核查,再让它修改,直到 Spec 达到我比较满意的程度。
2、让 AI 根据 Spec 把代码实现出来。理论上只要 Spec 写得足够完备,代码实现就已经不重要了,不需要太多人工参与就能被 AI 自动完成。甚至可以把 AI 当成编译器来看待,AI 把 Spec 编译成代码,就像过去编译器把高级语言编译成机器码一样。
3、在编译过程中,如果由于 Spec 写的有硬性的逻辑问题导致编译报错,或者最终运行出来的效果和设想的不一样,我不会单独去干预代码,而是去完善 Spec,然后重新编译成代码,可以是增量编译的过程,不用全部重写。
整个项目的维护核心,从过去的代码,开始转向 Spec。用 AI 的话来说,Spec 是 Single Source of Truth。Spec 可能需要花很长的时间去开发,并且它是人工参与程度最高的文件。
之后无论是实现代码,还是测试代码,还是 README,只要有不对的,就应该从 Spec 里面找参考依据。每次修改项目的时候,都应该先改 Spec,然后才是代码和 README。
项目链接
如果你也在用 Hexo,遇到类似问题没解决,可以留意一下这个插件。
我发现似乎还有人在用我以前开发的那个版本的 Markdown 渲染器,因为我看到 NPM 上还有下载量。我把原先那个项目标记成了 Deprecated,也链接到了这个新项目。
如果你是单纯想了解这个项目的 Reference、Spec 和 Test 是怎么写的,也可以在项目中看到。
这个项目的 GitHub 主页在这里:
如果觉得文章有帮助,欢迎分享转发,也欢迎关注我的公众号“搬砖的小明”,及时获取更新