算起来接触独立博客已经有十年了,这十年来虽然产出不算频繁,但是一直都在关注博客圈子。之前简单回忆过自己写博客的经历,这次想从技术角度分析一下这十年博客系统的发展和背后的逻辑。
首先,这里的博客系统指 self-hosted blogging platform,类似 blogspot、新浪博客、简书之类的 hosted blogging platform 不在讨论范围内。虽然自己之前也在这类平台上注册并产出过一些内容,但商业公司的平台最大的问题是不可控,比如我之前在点点轻博客写的文章就因为平台倒闭而再也没法找回了。另外,这类平台的博客页面样式不好定制,往往只有少量的主题可供选择,比如像新浪博客的 UI 已经若干年没有更新了。而其优点是免费、上手快,而且不少平台有首页推荐机制,可以给小博客引流,另外大平台的稳定性和网速一般会比自己搭的服务器更好。
自托管的博客克服了上面所说的缺点。我有印象的最早的自托管博客是手写的静态网页形式,用 SSH/FTP 部署到服务器上。手写虽然很原始,但它好在可以掌控一切。当然,在信息爆炸的时代这样的方式太低效了,于是就到了 WordPress 这类 PHP CMS 大行其道的阶段。
WordPress 在十多年前就已经统治了独立博客圈子,这些年虽然日渐式微但仍不乏拥趸。WordPress 的技术栈是经典四件套 WAMP(或者 LAMP/LNMP),甚至当我在谷歌搜索 WAMP 时首页出现的就是 WordPress 搭建教程。WordPress 的优点是“动态”,想象一下对一个手动部署的静态站点修改其中一个页面的错别字,你需要在本地编辑修改,完成后再次手动上传到服务器;而在 WP 中只需要打开网站的管理后台,在页面上的富文本编辑器里做修改,点击确认就能立刻发布。这样的编写体验与商业博客平台无异,又可以随意定制自己的站点,包括站点结构、主题、插件等等,所以不难理解 WordPress 的风靡。我记得当年为了搭建 WP 博客站点,每天都在寻找各种“免费 PHP 空间”和免费域名,毕竟学生时代最缺的就是钱,最不缺的就是时间。
但是 WP 的动态性也是它最大的缺点。想象一下访问一篇博客文章需要先解析并执行 PHP 脚本,执行过程中需要连接(可能在另一台主机上的)数据库,读取数据并渲染到模板中,最终返回完整的 HTML。当然也可以做一些缓存优化,比如在 Redis 中记录每篇文章的最后更新时间,这样可以尽早地返回 HTTP 304;但静态页面的缓存更简单也符合直觉,根据静态文件的修改时间即可判断缓存是否有效。而且一个系统中的组件越多就越脆弱,比如当 MySQL 数据库出现了问题,即使 PHP 服务依然正常也无法正确地显示博客。
其实从用户(博客作者)的体验来看,“方便”是目标,而“动态”只是达成这个目标的一种手段;“速度”是另一个目标,而“动态”这种手段牺牲了一点速度。
再后来,大约是 2014 年接触到了 GitHub Pages 和 Hexo。Pages 这种免费、近乎无限制的静态页面服务是个伟大的创举,而且它和 GitHub、Git Workflow 天然融合,推送代码立即更新页面。如果愿意用 GitHub 自带的 Jekyll 系统,那博客的维护成本就只剩管理仓库里的 Markdown 文件;当然这样可定制的内容就太少了,所以类似 Jekyll 的静态站点生成(Static Site Generation,SSG)系统如 Hexo、Ghost 等纷纷流行了起来。这类系统相比 WordPress 更“极客”,WordPress 最大的搭建难度不过是上传 PHP 代码、设置数据库密码,而且很多虚拟主机提供商甚至帮用户做好了这些,剩下的其实与商业博客平台无异,比如带有 GUI 的管理后台和富文本编辑器;但 SSG 系统比这硬核得多。
在 SSG 系统中有源码、构建、产物的概念。源码指 Markdown 编写的博客文章,学习 Markdown 就需要成本。构建是指将文章内容填充到主题模板里并生成整个站点(包括归档目录、RSS XML)的过程,而产物就是最终的整个静态站点。要使用这类 SSG 系统并搭配上 Pages 服务,博客作者首先得学会使用 Git(虽然一般只需要学会 commit 和 push 即可),而且还要先安装 SSG 系统的运行环境:比如 Hexo 基于 Node.js ,要跑起来就得先安装 Node.js,然后运行 npm install 安装依赖,一旦安装过程中发生任何错误,对于没有相关经验的人就是噩梦。当然,反过来想,这一套流程对于自己同时也是开发者的博客作者而言反而更加驾轻就熟。作者编写完 Markdown 之后,可以在本地运行预览,确认没问题后用几条命令将内容构建并部署到 Pages 仓库中,这就是一条完整的操作链路。另外,要想定制博客的各种细节也很简单,站点的一切都在源码仓库中,比如要更换站点头图或调整字号,都只需搜索并替换即可。
这一套 Pages + SSG 系统既不像手动维护静态站点那么繁琐,定制性更甚于 WordPress 等系统,又能享受到静态页面的性能和稳定,而且还免费,一经推出自然就受到了大量追捧。SSG 系统的理念类似 HTML 和 CSS 之间的关注点分离——HTML(对应 Markdown 文章源码)关注内容本身,CSS(对应 Hexo 主题)关注展现方式。当然 WordPress 也是这样,数据库里的文章信息是内容,其他的都只是展现方式。随着这些年持续集成、持续部署概念的普及,Hexo 的构建部署流程也进一步简化,作者修改源码后推送到远程仓库,触发持续集成并部署到静态服务器上,整个流程往往只需要一分钟。
Hexo 已经足够完备了吗?不,它相比 WordPress 缺少了一个在线的内容管理后台(CMS)。对于独立博客作者而言,改造和定制博客也许是刚需,但其频率应该远低于创作本身。改造时需要克隆仓库并在本地的集成开发环境中修改各种代码,这样的流程无可非议,但是如果每次编写文字也需要在仓库中进行就不太理想了。虽然在本地仓库编写文字也有其好处,比如可以用自己喜欢的编辑器编写 Markdown,但如果考虑到要在不同的设备——包括移动设备——上编写文字,这种方式显然并不讨喜。一种可行的解决方案是参考 WordPress,只要能访问博客就能打开博客的管理后台并进行创作和发布,无需依赖本地环境设置。
于是我们有了 Netlify CMS。它的接入非常简单,这也得益于单页应用技术的发展,对于任何一个项目,只需要添加一条 <script> 标签,就能集成一个完备的 Web 应用。当然,JavaScript 带来的只是前端部分,而这个 CMS 的后端就是 Git。虽然 CMS 和 Git 之间的对应关系不是它首创,比如在 Hexo 等系统流行之后有很多人探索了用 GitHub issues 对应博客评论的模式(Gitalk、utterances 等),但它做得非常好。举个例子,博客文章往往不是一次就能写成,所以需要先存为草稿,在合适的时机发布。Netlify CMS 将这种流程和 Git Workflow 中的分支模型对应起来,一篇草稿就是一个分支,进入审阅流程就是提起 Pull Request,而审核通过正式发布对应 Merge。总之,Netlify CMS 是一个非常优秀的解决方案,“优秀”不在于它是否好用,而是它基于的技术和背后的逻辑很大地启发了人们的思考。
WordPress 和 Hexo 在思路上有着根本的区别,如果我们从 WordPress 的动态性出发,有没有什么改进的方向呢?答案还是单页应用。早年间 Web 工程师需要负责从数据库到前端页面的完整链路,但随着近些年前后端分离逐渐流行,Web 开发也分化为了前端和后端,单页应用也开始蓬勃发展。单页应用的好处显而易见,比如切换页面更快,后端专注于提供数据而不用负责模板渲染,所以获取一篇文章内容的接口也可以变快,这样的系统比 PHP CMS 轻量得多。其实这个思路和 Netlify CMS 很像,只需稍加改造就能得到一个基于 Git 的、在前端动态渲染 Markdown 的 SPA 博客系统。虽然牺牲了一些访问速度,但省去了部署这一步骤。不过暂时没有发现这样的例子,可以考虑开辟一下这个领域。
好了,我们的博客系统有自动部署,有后台管理页,还缺什么吗?前段时间我稍微改造了下自己的 Hexo 博客,接入了 GitHub Actions,接入了 Netlify CMS(不得不说它的文本编辑器对中文输入很不友好),延迟加载了一些脚本,做了字体的裁剪和 CDN 托管,一顿操作把 Lighthouse 评分刷到了 99。用户体验已经完美了吗?不,Lighthouse 虽然是一个完备的性能评估工具,但它测试的只是当前页面,得出的测试成绩无法代表整站的浏览体验。Hexo 博客站点全部由静态文件组成,想象一下用户打开了博客首页,点击某一篇文章的链接,此时浏览器需要请求另一个 HTML 页面,注意这是一个完整的 HTML 页面,哪怕它的结构与当前的首页有 50% 的相似,这些字节也必须逐一地从服务器传输到浏览器中。通过 Chrome DevTools 可以观察到,在没有缓存的情况下从我的博客首页点击链接进入文章页,光是请求 HTML 就需要 100~300 毫秒左右的时间;即使有缓存(GitHub Pages 设置了 max-age=600 即 10 分钟),从头构建一个页面的时间也不容忽视,而且在这过程中用户很有可能观察到屏闪,体验算不上完美。
基于这个现状,我尝试构思了一个类似服务端渲染(Server-Side Rendering)的方案,即用户访问首页时得到的是预先生成的首页 HTML(其实是 SSG 而不是 SSR),该页面会加载一个 JS bundle,从而为当前页面补水(hydrate)得到一个功能完整的 SPA,剩下的路由就交给前端掌控。当用户从首页进入文章页时,应用动态地请求文章数据并增量地更新 DOM。这样,页面切换所需的网络开销和浏览器绘制开销都降到了最小,也不会出现屏闪。即使首页和文章页的 HTML 结构布局大不相同也没关系,差异的 HTML 只需要请求一次,当用户切换到其他文章页时所需的开销依然极小。这就是我设想的极致的博客系统方案,可以算是 Hexo 方案(SSG)和上面提到的 SPA 方案(CSR)的结合。
巧合的是,真的有一款产品把这两者结合得很好,那就是 Next.js。前不久偶然看到一个 GitHub 项目 notion-blog,发现它实现了我想要的一切效果。它的原理简单讲就是请求 Notion 的接口并把内容渲染到页面。我们分析一下前面各种博客系统的各种问题是如何在这里被解决的:
- 编写体验:Notion 的体验非常棒(虽然在国内网络时常有波动),作为 CMS 它有足够强大的树形结构,作为编辑器它的体验类似 Typora 而且支持更多功能,作为 Web 应用它达到了原生应用的流畅程度,而且在各个平台都有 WebView 套壳,启动非常方便;
- 首屏速度:Next.js 支持 SSG,用户从浏览器地址栏访问的一定是生成好的静态页面;
- 切换速度:用户浏览器打开页面后,Next.js 编写的 SPA 接管路由调度,链接由 next/link 处理,只需要请求恰好够用的数据就能展示对应的页面(因为 Next.js 生成的不只是 HTML,还有只包含数据的 JSON,切换路由时请求的是数据 JSON 而不是完整的 HTML),DOM 也是增量式更新;
- 部署:Hexo 博客每次写完都需要推送到远程仓库并触发 CI,而 Next.js 可以通过 Incremental Static Regeneration 自动触发页面级别的重新构建。对比一下,CI 是全量的构建,对于没有任何更改的页面也需要重新生成;而 ISR 是增量且按需的构建,极大减少了所需的计算资源。
另外还有非常重要的是 Vercel 支持 Next.js 应用的一键部署,看到这个项目之后,点击 Deploy on Vercel,几下操作就能立刻得到一个博客站点。可以说这个项目满足了我对博客系统的所有要求和想象,也越发引起了我对 Next.js 和 Vercel 的兴趣。Vercel 的 CEO 说他们的愿景是 build a faster Web,至少从 notion-blog 这个项目来看,他们做得不赖。
以上就是这十年来我对博客系统的观察和感受,希望未来能出现更惊喜更有创意的方案。不过最重要的,还是希望独立博客圈子不要消失,博客作者们持续输出内容,让这个日渐闭塞的中文互联网变得丰富一点。