将博客改造成数字花园

重构内容发布

这个网站作为博客存在超过十年,期间每隔几年都会进行“基于质量的大刀阔斧式的过滤”——随着对内容品质要求的逐步提升,删除掉不再符合要求的内容。上一次大刀阔斧式的“质量过滤”是迄今为止最严格,因此也是变化最大的一次——整个网站都被重新编写了。
当我以新的标准去回顾我的创作时,我发现它们大多都无法满足我对可维护性的苛刻要求。如果文章是不可维护的,那么文章的价值会随着时间流失,最终变得没有价值,甚至成为一种债务——事实上没有人需要它,但作者又舍不得删除它
相对的,如果文章是可维护的,那么作者就能通过更新文章来保持它的长期价值。虽然将低可维护性的内容提升为高可维护性的内容是可能的,但这是不划算的,所以我只好让它们消失,这是大多数文章被删除的原因。

文章的可维护性

从文章的可维护性这一概念中可以进一步引申出这些关键因素:题材,篇幅,结构和模式。题材,篇幅和结构决定了内容的可维护性,模式决定了维护是否会发生。

题材

题材对可维护性起决定性影响,因为一些题材显然比其他题材更容易随时间失去价值。
容易变得过时和无效的题材所包含的信息本质上更像是对其关联事物的一种端到端测试。信息与具体事物之间的关联性往往十分脆弱:测试要么不完善,要么很难适应变化,要么成本太高昂。因此即使是信息的创作者也很难验证和维持它的有效性,甚至信息可能一开始就是错的,却没有被注意到。

篇幅

有可维护性的文章应该尽可能精简。
我在写长文章方面还算有能力,在博客时代的末期,已经能够撰写单篇超过2万字的文章。然而,这些文章在完稿后都是不可维护的,因为我维护不了连自己都要花十几分钟才能读完的东西。

结构

维持良好的文章结构很难,因为文章本质上是一种树形结构,树形结构在处理分类问题时很不堪用。
实践中经常会遇到一节内容的主题与其他节的主题交叉的情况,删去交叉的部分又会严重损害这一节的主题,其结果多半是将节与节之间的差异改得越来越模糊。当文章的结构开始变得可有可无时,文章中的每处内容实际上都是在以整篇文章作为上下文,每处内容都与文章中的其他部分相耦合。对于这样的文章,任何修改都可能破坏掉整篇文章的结构,进而迫使编辑者重新阅读整篇文章来修复它,可维护性当然会很低下。
我最终意识到,如果需要维持良好的文章结构,就不得不停止过度照顾读者的感受。对读者妥协越多,结构就越差,反之,结构就越好。参考手册往往是很难读的东西,读到一半时,可能会需要跳转到其他部分,以获取必要的前置知识,但参考手册的结构通常都组织得不错。在我看来,良好的文章结构的优先级要高于文章的可读性,因为前者对可维护性更重要。

模式

“模式”这个词代表着“形式”,“框架”,或者“习惯”。“模式”不那么容易觉察,但对维护很重要。
不鼓励可维护性的模式
人们很难意识到自己对文章的理解实际上是一种路径依赖:在人们的潜意识中,文章往往是一经发布就很难再修改的东西,因此发表正式的文章应该经过严谨的校对;即使文章是可以事后修改的,也不应该大幅修改它的结构,因为这会导致它变成别的文章。
上述认识显然是从印刷品时代,乃至更早的时代流传下来的,这种内容创作模式是否仍然适用于数字时代?答案恐怕是否定的。
这个时代的信息是指数增长的,一天产出的信息量比人类进入信息时代以前的信息的总和还要多。文章的维护职责经常会在作者按下“发布”按钮之后就消失殆尽,因为信息普遍是以从新到旧的顺序以信息流的形式展示的,旧信息的展示空间会很快会被新的信息挤占。人们之所以能接受这种现象,是因为我们有天文数字级别的空间来存储这些垃圾信息,尽管这些信息在未来被机器以外的读者见到的机会渺茫,但那些已经从视线里消失,被打入冷宫的信息可以在尽量不触发人类损失厌恶的本性的情况下被保留着。
以上都导致了只有那些对内容品质有偏执要求的创作者才会在发布后仍然反复修改自己的作品,但即使是这些人也会很快发现自己修改的动力在慢慢消失。在这种内容创作模式下,即使文章是可维护的,也不会得到有效维护,因为这些缺乏维护的文章很容易被忽视,不会成为亟需解决的障碍。要打破这种模式,就得反其道而行,抛弃博客转向数字花园。
文章是展示笔记最合适的选择吗?
不难发现,很多坚持写了多年博客的博主其实并不是在写文章,而是在以写文章的形式记笔记。博客文章是写笔记和公开笔记的最佳方式吗?我不这么认为:
  • 笔记应该是一种内聚性更高的东西,同一主题的内容应该尽可能聚集在一起,而不是被拆成多个页面。
  • 笔记更适合以大纲视图展示,而不是以文章视图展示,因为文章视图的层级结构不够明显,缺乏导航价值。
  • Markdown不是一种适合用来记笔记的格式:Markdown天然缺乏层级结构(Markdown的标题级数受到HTML的严格限制),大多数Markdown编辑器也缺乏编辑层级结构内容的能力。举例来说,我的笔记都是Org-Mode格式的,如果这些笔记是Markdown格式,根本不可能组织。
大多数人无法脱离文章这一形式的根本原因在于缺乏脱离它的能力,因为:
  1. 1.
    市面上绝大多数博客程序都是面向文章设计的,展示文章以外的结构化内容很难。
  2. 2.
    缺乏一个足够简单流行的大纲文档格式和编辑器:Org-Mode作为最主流的大纲文档格式,要求用户掌握Emacs才能编辑,而Emacs以学习曲线陡峭著称。Org-Mode同时还是一个工作流程管理器,因此承载了太多(不必要)的功能,初学者很容易迷失方向。

从博客到数字花园

数字花园并不是新概念,它只是互联网早期阶段被称为个人主页的网站类型在这个时代的复兴
数字花园的重点在于重新审视内容创作的流程,将创作比喻为园艺,从而解开来自博客时代的桎梏。
作为一种与博客相对的模式,标榜自己是数字花园的网站往往具有以下一个或多个与博客截然不同的特征,其中蕴涵的很多理念都与我对可维护性的要求不谋而合:
  • 文章的排列排序是任意的,文章可能不显示发表和更新日期。
  • 文章没有“草稿”和“已发布”的分别,只通过“内容的成熟度”来描述文章的完善程度。
  • 创建新内容不重要,对已有内容的“修剪”和“浇灌”才是重心。
  • 不对文章列表分页。

在数字花园的基础上改进

发布和更新日期不重要

对有可维护性的文章而言,文章的发布日期和更新日期都不重要,因为它应该是长期有效的,思考不应因时间而褪色,读者也不需要理解文章的写作背景。根据极简主义的观点,既然它并非必要,就应该舍弃。

评论框不是必需品

评论框是一种模式,虽然方便用户发表自己的想法,但也变相鼓励了低价值信息的传播。过滤掉低价值信息的最好方法,就是彻底关闭这个功能。评论框对这个网站来说曾经是有意义的,但那个时代已经过去,如今能被我视作有价值的论点已经非常鲜见了,我不想假装自己还在意互联网上随机出现的路人发表的内容。
过去的经历也使我意识到缺乏评论框不会阻碍那些真正想要交流的人:如果真的有必要,即使在没有评论框的情况下,这些人也能通过各种方式向我发送信息,这种体验本身是非常奇妙的。我相信那些肯付出更多努力去传达信息的人,所传达的信息相比之下会更有价值。因此我能够心安理得地将没有评论框这件事,作为用户生成内容的过滤器来使用。
防御型写作
一些作者在与读者通过评论来异步交流的过程中会无意识地养成一个写作习惯,我称之为防御型写作。这种习惯的表现是事无巨细地往文章里填充细节和花大量时间来让语言表达精确完美,以防止那些“杠精”指出你文章中的错误或遗漏的部分。
这种习惯之所以不可取,是因为作者与读者在权力上的不对等。作者需要构筑一座高塔,而读者只需要找到其中脆弱的部分就可以瓦解掉这座高塔。
如果文章中脆弱的部分是真实存在的,那还好说,因为这是一个可以修复的问题。人们写作时会理想化地高估这类优质读者的数量,而经验告诉我现实世界里这样的人类的比例不会超过十分之一。实际情况是大部分人连“正确地提出问题”这样似乎理所当然的技能都需要开个班来强化学习。
最常见的情况是那些被指为脆弱的部分是源自读者的误解,或者干脆就是一种有迷惑性的无理取闹——毕竟世界上确实有很多无聊的人。在这种情况下作者什么也做不了,因为你不可能解决一个并不真实存在的问题。如果作者足够真诚(或者说天真),则大概率会出现作者越是澄清,水就被搅得越浑的情况。
由于防御的成本跟攻击的成本相比,根本不在一个量级,防御型写作会逐渐变成作者的负担,这导致好的内容也变得越来越难发布。破除这种习惯的关键取决于作者什么时候发现事情的真相——意识到这些都是不值得付出的努力。

CC协议没有现实意义

以经济学的视角看待版权,会发现那些防君子不防小人的规则都没有意义,因为惩罚小人的成本多半会高到不值得实行。由于这些额外规则的存在,反而可能把事情搞复杂,在法律执行时引入更多争议。
此网站过去使用的CC协议就是这种防君子不防小人的规则的典型代表。实际情况是,极少有人理解和遵守CC协议,重视著作权的人会直接找到我请求授权,不重视著作权的人会进行不规范转载。考虑到互联网上存在一种叫做超链接的东西,全文转载的形式是否真的有必要,也使我怀疑:尤其是在大多数网站并不重视内容展示的情况下,转载实际上是在劣化原有的内容。此外,在将文章变成可维护的文章后,文章内容随时可能出现变更,这种情况下,转载对于作者和转载者都没什么好处。
我找不到继续使用CC协议的理由。

减少外部内容发布平台

同时使用和维护多个内容发布平台效率低下,且并非每个内容发布平台都有能力显示足够复杂的内容。一个很好的例子就是微信公众号,将内容同步到这些平台上相当费力,加上在中国运营的互联网服务普遍存在的特色风险,除非是出于利益,否则相当难以坚持。
数字极简主义思潮让我意识到大多数互联网服务都没有继续被使用的价值,所以我进一步减少了其他内容发布平台的使用。

内容成熟度指标

数字花园里的内容根据成熟度被分为五个阶段,这是它们的纯文本表示形式:
  • 🌱
  • 🌿
  • 🌲
  • 🌳
  • 🍂
使用emoji表情是为了方便在纯文本编辑器里使用,即使在不同的平台上,这几个表情也很少有歧义。每个阶段的图案所代表的含义很大程度上是不言自明的,仅凭直觉就能理解它们的含义。数字花园的内容成熟度很大程度上是给作者自己看的,访客可以轻易忽略掉它们,不理解其含义也不构成影响。
内容并不总是按照“内容成熟度”的先后顺序发展的,例如一些内容可能永远也不会到达🍂阶段,而一些处于🍂阶段的内容可能重新回到🌱或🌿。
内容成熟度也并非是“草稿”与“已发布”状态的替代品,将它们视作内容潜在的改善余地会更合适。
在实际使用时,直接显示emoji表情可能会破坏掉界面的美感,因此我会将它们转换成对应的颜色来使用。

在一个页面里列出所有条目

将条目在一个页面里列出很有意义:分页和其他的行为会导致条目被隐藏,一旦条目被隐藏,它就不会得到足够多的关注,进而无法得到维护。

将数字花园作为个人知识管理的公开方案

在博客时代,我摸索过个人知识管理系统及其内容公开方案,但始终并没有得出满意的方案。后来,我从Zettelkasten等方法论中取经,总结出了一个新的方案。你现在可以在网站的首页上看到“笔记”一节,每个条目都是以大纲视图呈现的Org-Mode格式的笔记内容。这个方案并不完美,但已经是不自己开发编辑器的前提下能够达到的最优解,并且能够和数字花园很好地融合起来。
有了这个方案之后,我就可以尽可能避免写过度涉及技术细节的文章——它们是最缺乏可维护性的那类题材。相反,技术细节很适合写成笔记,因为笔记对文本完成度的要求比文章低得多:写笔记时,我不需要在意文体和结构,不需要斟酌用词,一切都是以方便使用和理解为目标编写的。笔记能随着使用而逐步更新完善,也可以轻易舍弃。

唯独纯文本

这个数字花园的内容基本上完全是由纯文本文件表示的,没有所谓的网站后台页面。
采用纯文本的好处很多,但最重要的是我可以用我习惯的编辑器直接编辑一切内容。

保存即发布

在我编辑完文档以后,包括静态文件在内,都会被自动同步到服务器上,由一个服务自动完成内容发布。待页面缓存过期,新内容就会显示,对作者来说,一切都在不知不觉间被发布。
这种发布方式本身并不新鲜,在很多年前就已经存在了,但并没有真正广泛流行过。考虑到实施的复杂性和稳定性,其被冷落是可以理解的:只有实践过才会知道,即便是“自动发布内容”这种看似简单的自动化需求,也有很多出人意料的难点。尤其是在你不愿意做太多妥协的情况下,自动化会变得难上加难。
自动内容发布是有风险的,例如编写到一半的文档会被发布,或由于文档不符合要求的格式,导致渲染失败,服务器直接向访客显示一个错误页面等。我会说这些风险没有抵消其好处,保存即发布与其说是一种内容发布方式,不如说是一种自由,一旦体验过就不会想再回去了。

建造数字花园

接下来将讲解建造数字花园使用到的部分技术,有助于具备相应技能的开发者复制这个站点。

发展历程

本站的站点程序历经多次改变,了解发展过程可能有助于理解最终的技术选择。

Micolog

这个网站是从Google App Engine平台上开始的,而Micolog是那个时代GAE上最流行的博客程序。
如今看来,Micolog最大的缺陷可能在于它与GAE平台高度绑定这一点,这种供应商锁定迫使用户需要忍受GAE的诸多限制。

WordPress

为了从GAE中解放出来,博客迁移到了WordPress。
WordPress是一个扩展性很强的CMS,折腾各种WordPress主题和插件对当时的我来说还是一件有趣的事。遗憾的是WordPress很慢,以至于有很多关于如何优化WordPress的文章。这些优化手段用今天的眼光来看,非常接近于货物崇拜编程可以说在某种层面上,整个PHP生态环境就是从货物崇拜编程文化起家的,因此出自那个蛮荒年代的PHP程序都或多或少沾染着糟糕的程序设计。WordPress显然是在货物崇拜编程里陷得最深的那个,最后你会意识到优化WordPress的尽头就是摆脱WordPress。

Ghost

Node.js和V8曾经掀起用JavaScript重写一切现有事物的浪潮,Ghost就是那股浪潮中以替代WordPress为己任冉冉升起的项目。
从WordPress切换到Ghost主要有以下几个原因:
  1. 1.
    WordPress的性能只能用可悲来形容,我再也无法忍受它了。
  2. 2.
    Ghost的Markdown文档编辑器很有吸引力,因为使用Markdown更容易编写干净的文章。相比之下WordPress基于HTML的所见即所得编辑器简直是一切混乱的源头
Ghost带来的大部分用户体验都是愉快的,但缺点也一直很明显:没有内置的评论功能,也没有插件功能。为了解决评论的问题,当时所有的Ghost博客都被迫外挂第三方评论框服务
插件功能的缺失在当时看来是其次,因为官方承诺会有插件功能,尽管直到最后我也没等到这个功能。
压垮Ghost的最后一根稻草是官方在新版本里用新的区块编辑器替代了原来的Markdown编辑器,并且没有留下任何继续使用Markdown编辑器的余地。尽管新的编辑器可以提供类似Medium的用户体验,但我更喜欢原来的Markdown编辑器。

代号Mask:静态博客程序

创建一个单用户静态博客程序很容易,毕竟实现这一点本质上只需要将Markdown文件翻译为HTML页面。
Mask是我从零开始开发的Ghost的替代品。它同时是一个静态博客生成器和服务器,整个程序的代码应该没超过4000行。Mask完美模仿了Ghost的URL结构和界面设计,除了页面最下方的博客程序名称从Ghost变成了Mask,以及输出的HTML代码变得更语义化以外,访客是无法区分两者的。
Mask在底层采用了一些实验性解决方案,比如支持类似WordPress的主题功能用户可以直接在主题里编写JavaScript代码来增强主题,而不是只能依赖博客程序提供的功能。回过头看,这有点像另一个叫做ejs的模板引擎。
Mask的主要缺陷是我没有持续维护它,而是在它满足我最低限度的要求后就停止开发它了。因此,一些糟糕的设计因为没有得到及时纠正而变得越来越难纠正。最终,整个项目的代码老化到捡起来改进已经没有任何意义的程度。

代号Mass:数字花园程序

老化的项目只能用重写来拯救,但对Mask的重写从未完成。由于思想上的变化,它被当前的数字花园程序Mass取代了。在试图重写Mask时,我对Mask有一些期望,这些期望最终在Mass里得到体现。
Mass是一个具有以下特点的程序:
  • 使用现代Web技术。
  • 具有长期的可维护性。
  • 具有自动发布内容的能力。
  • 设计和支持一种与文档格式无关的,索引文档、元数据、及其附件的组织结构。不满足此组织结构的内容不会被视作合法的文档,这使得纯文本文档具有更好的内聚性,避免埋下维护性问题的种子。
  • 支持Org-Mode文档。
  • 在Markdown文档里支持自定义组件。
  • 实现最大限度的渲染结果定制性。

Mass如何实现X

受限于篇幅以及解决方案的复杂性,我只会重点讲解我认为值得一提的部分。

Mass如何实现最大限度的渲染结果定制性

Mass看待文档的方式与一般解决方案完全不同。
一般来说,一个基于Markdown的CMS会在数据库里保存预建好的HTML,文档到HTML的转换是由静态网站生成器、在线编辑器、网站后端之一完成的。Mass的区别在于它没有保存HTML,作为替代,它保存了Markdown的AST和Org-Mode的AST。因此,在Mass里,呈现给用户的HTML并非事先预建好的,而是在前端服务器动态生成的。
这之所可以实现更多的定制性,是因为HTML是一颗丢失了语义、难以操作的树,而AST是没有丢失语义,容易操作的树。
通过递归实现AST至React组件树映射
一切定制性的来源只不过是通过递归来将一颗树(AST)映射为另一颗树(React组件树),你可以将这个步骤称之为“渲染”。具体实现说白了不过是编写一堆互相递归的函数,让这些函数返回所需的渲染结果而已。
当然,这里有一些前提条件需要满足,主要的前提条件是需要有便于渲染目的的AST。由于AST本身是一种低级的数据结构,只有少数人会需要使用AST,市面上并没有太多直接基于AST的项目。在那些基于AST的项目里,AST在设计时并未考虑递归访问,以至于需要在树的节点里跳来跳去收集所需的信息。为了满足渲染目的,我们需要将这些现成的AST改编。这涉及到设计适合用于渲染目的的AST,以及对现成的AST执行变换操作。Mass使用rmdastromast处理Markdown的AST和Org-Mode的AST,最终将渲染AST变得很容易。

Mass如何在Markdown文档里支持自定义组件

当我们提到扩展,是指让它支持原本不支持的功能,而不是将原本支持的功能改编。从AST映射组件树已经可以做很多定制化,但它仍然属于后者,因为它没有拓展Markdown的功能边界。这是区块编辑器胜过Markdown的地方,区块编辑器可以扩展,而Markdown无法扩展。
在Markdown的方言CommonMark里,有一个现成的通用指令语法提案,提供了用于扩展Markdown功能的语法。尽管该提案在大部分编辑器和工具链里都没有得到支持,但Mass在这件事上比较走运:
  • Mass底层依赖的AST定义里已经有对此语法提案的官方插件支持,所以很容易引入此语法。
  • 由于Mass一开始就着眼于将AST映射为组件树,因此将通用指令翻译为React组件很容易,只需要添加对应的映射关系。
常见问题
为什么不用AsciiDoc替代Markdown?
你可能听说过AsciiDoc,这是一种与Markdown竞争的标记语言,它提供的内置功能比Markdown要多很多。然而,AsciiDoc在我看来是一个设计得很差的标记语言,从符号的选择到功能的选择,充满着不成熟的实用主义。
扒下AsciiDoc最后一层皮的是它缺乏语言规范的事实:AsciiDoc并没有事先被标准化为一份规范,而是由实现决定其结果,这意味着很高的供应商锁定风险。对AsciiDoc的规范化尝试从2015年就开始了,直到我写下这段字的时候也没有孵化出任何有用的东西。在这种情况下,你可以期望在接下来五年内孵化出规范的概率仍然相当小,并且AsciiDoc发展得越快,孵化出规范的概率就越小。
作为竞争对手的Markdown,其规范虽然是以方言的形式(CommonMark、Github Flavored Markdown)出现,但仍然可以说是有规范的。我会说这有一部分得益于Markdown的简单性,而这种简单性是AsciiDoc在一开始就失去了的东西。
为什么不用MDX替代Markdown?
不难发现扩展Markdown的需求其实有点像MDX已经在做的事。当你想要一个功能时,最幸运的事莫过于已经有有能的开发者替你实现了。
遗憾的是,MDX是一个“支持Markdown的JSX”,而不是“支持JSX的Markdown”,这使得MDX无法被用于替代Markdown。MDX在宣传时似乎刻意混淆两者之间的区别,以便它在需要时将自己伪装成标记语言以获取它并不具有的优势。要如何区分两者的不同?需要从语言视角来审视它们。
已知JSX是一种编程语言变体,而Markdown是一种标记语言,因此你会对它们有这些期望:
  • 对于JSX,可以通过主动引入更多代码来扩展自身的功能。
  • 对于Markdown,受限于语言设计本身提供的有限功能。
对编程语言来说,引入更多代码,尤其是依赖来自不可控的外部代码总是需要慎重考虑,因为代码的耦合性可以衍生出很多问题。但是,编程语言又不可能舍弃这些功能,因为通过编程来为程序添加功能本身就是编程的目的。因此,通常是由开发者遵守好的编码习惯,以防止代码变得一团糟,而不是去除对开发有利的功能。
对标记语言来说,它能做什么是通过标准/规格实现定义好的,超出标准/规格的功能不会被提供。你可以将这种特性视作是一种基于接口的控制反转,即依赖注入
哪一种语言更容易维护是显而易见的,“支持JSX的Markdown”会比“支持Markdown的JSX”容易维护,因为前者杜绝了失控的可能。更容易理解的说法是,你可以将Markdown交给开发者以外的人编写,但永远不应该将MDX交给开发者以外的人编写。
为什么不用Web Components实现扩展性?
利用Web Components扩展Markdown是我最先尝试的方法。已知Markdown可以内嵌HTML代码,而Web Components就是HTML代码,所以用Web Components来扩展似乎是水到渠成的。那么将Web Components用于扩展Markdown会有什么问题?
Web Components完全作用在客户端上,每种Web Component都需要动态加载组件的相应脚本。如果你不想把整个站点可能用到的所有Web Components都一次性载入,或是不想单独在每篇Markdown文章里都声明需要载入的组件,又或是你只是想利用Web Components里Custom Elements的语法,你都会需要在解析Markdown时识别其中的Web Components。
对于只需要在客户端动态加载Web Components的情况,有一种脏解决方案。通过正则表达式来尝试提取出所有可能是Web Components的字符串,再通过过滤来得出使用到的自定义组件。我对采用这种方案有本能般的抗拒,原因如下:
  • 时间证明Web Compoents是一种糟糕透顶的技术,采用Web Components后患无穷。比如Web Components组件的API表面积很大(因为组件本质上是一种继承自HTML的元素),维护不便。比如Web Components缺乏一个具有长生命周期的解决方案:从Google官方维护的Polymer v0到Polymer v3,再到LitElement,没有一个解决方案是真正成熟可用的。在这些解决方案的生命周期的大部分时间里,甚至连一套完整的开发工具链都很难凑齐。
  • 大量脚本的动态加载在现代Web开发中属于不良实践,即使在有HTTP/2的情况下,它还是要比打包方案差。
综上所述,最好情况下,我们希望只使用Custom Elements的语法,实际插入的是React组件。次好情况下,我们希望识别Markdown中的Web Components,在服务器生成响应时就把Web Components的脚本打包进去。无论哪一个,Markdown解析器都需要能够正确解析HTML。遗憾的是Mass使用的Markdown解析器并不支持正确解析HTML,具体表现在内联HTML在AST里会被拆分为两个互不关联的节点等问题上。
内联HTML在AST里被拆分为两个互不关联的节点
对Markdown解析器的修改是很难维护的,得出这个结论是因为我确实修改过。Mass底层使用remark作为Markdown解析器,remark内部存在两代Markdown解析器。第一代解析器的实现与战锤40K里的兽人科技产物相似,属于那种乱七八糟搞出来但不知为何可以工作的东西。值得一提的是,MDX的第一个版本就是魔改这第一代解析器搞出来的东西,如果你阅读源代码,会觉得它也是兽人科技产物。在捏着鼻子修改第一代解析器后,我成功往里面加了一个HTML解析器,一切如预期工作。估计是作者自己都维护不动第一代解析器,所以后来又有了第二代解析器micromark我会说第二代解析器即使第一版代码都比第一代解析器要好多了,至少更接近人类而不是兽人的思维方式。由于第二代解析器的发布,所有与第一代解析器有关的产品都很快失去了继续维护的价值。理所当然的,我尝试在micromark上如法炮制相同的HTML解析器,但这次尝试是失败的:由于底层解析方法的根本性改变,原先对HTML解析的支持在micromark里只能实现一半,另一半需要大幅修改micromark的代码,而这其实就相当于无法支持,因为这样的修改是不可维护的。
当你需要实现非凡的需求时,你最好有非凡的能力和毅力。如果要实现这条Web Components路线,最好的情况是自己从头开始设计一个Markdown解析器,从而让相关功能打一开始就被考虑在内。

Mass如何实现自动内容发布?

自动内容发布是Mass的一个另类功能,因此它被制作成一个单独的服务,以便可以随时把它替换成后备的手动方案。
自动内容发布是典型的talk is cheap项目,它本质上只是执行这样两个阶段:
  1. 1.
    启动阶段:收集本地文档的数据,对比本地与服务器的差异,执行差异同步。
  2. 2.
    守护阶段:观察文件变化,当文件停止改变时,响应文件变化,执行增量同步。
实施时会发现有无穷无尽的边缘问题需要解决,例如你会遇到“差异同步期间本地文档已经被修改,所以观察文件变化需要早于差异同步”等细节问题。这些边缘问题之多,加上差异同步和增量同步的本质复杂性,项目代码会快会乱成一锅粥。最终你需要用一大堆测试用例来保证程序执行正确的行为,但它仍然可能时不时出点问题。
响应文件系统上的变化
在自动内容发布方面,最经典的问题是“文件系统已经发生了新的变化,但服务却还在处理旧的变化”。举例来说,当自动内容发布服务向后端服务器上传文档时,文档中依赖的附件却可能已经被删除了。甚至整篇文档都已经被删除了,而程序却还没有适应这种变化,程序可能因此出错,然后崩溃。
为什么寻求文件锁定不是解决问题的方法
一种看似合理的解决方案是寻求文件锁定,只要我们能锁定文件,就能无视外部变化,一步一步完成同步。
在寻求文件锁定的过程中首先会遇到的问题是Windows和Linux系统上最常见的文件系统对于文件锁定有不同的看法:
  • Windows上的文件系统倾向于使用强制锁来独占文件的访问,用户会经常遇到文件被锁定的情况。
  • Linux上的文件系统倾向于使用咨询锁来约定对文件的共同访问,想要实现强制锁反而困难重重。
这种设计思路的根本区别会导致自动内容发布服务需要与特定平台绑定。Mass实际使用的自动内容发布服务是针对Linux开发的,这主要是出于对Linux的路径依赖。然而,Linux文件系统缺乏强制锁的现实让相关功能的实现变得很棘手。
据我所知,在Linux上有几种类似文件锁定的方案:
  • 最理想的解决方案是文件系统支持快照功能,这样自动编辑器可以通过访问快照来暂时性获得一个静止的文件系统,直到任务完成。然而,在Linux上,支持快照的文件系统是少见的,不支持快照的文件系统才是常见的,一般不会只是为了实现一个功能就去更换文件系统。
  • 事先保持文件句柄打开。该解决方案能够工作是因为已经打开的文件句柄不会受到删除和移动的影响,采用这种方案需要在一定程度上改变代码的写法。然而,Linux内核对同时打开的文件句柄有数量限制,默认的软限制为1024个,实在是有点太低了,打开太多文件句柄可能会撞到这个限制。
  • 用硬链接模拟快照。文件系统上的文件是否被删除取决于inode的链接数量,创建硬链接可以避免文件被删除和移动造成的影响,不过仍然无法防止文件被修改。如果文件系统创建硬链接的速度很快,我们就可以把硬链接视作一种类似快照的东西:需要快照时,就立即通过硬链接复制一份相同的文件,在任务完成时再将硬链接删除。
最大的可能是你哪种方案都不想选,因为总是要妥协一些东西,或是修改一些系统配置。
任其崩溃
解决此类问题时,最佳解决方案是任其崩溃,甚至主动要求程序崩溃。为什么采用这么脏的解决方案?因为会遇到的问题并不只有我列出来的这些,而其中大多数问题都可以通过稍后重试来解决。当出错的可能性太多,覆盖错误太困难的时候,任其崩溃的哲学可以让人重新找回理智。
有两种任其崩溃的方案,一种是线程级的,一种是进程级的,区别在于任务的最小粒度不同。在实践中,Mass的自动内容同步服务采用进程级的崩溃方案,因为该服务会在每次启动时执行差异同步,刚好适配这项需求。需要注意的是,连续重启的成本可能会很高,这可以通过指数退避算法来减轻影响。服务实际的进程重启策略被委托给一个叫做undead的CLI程序,就像它的名字所暗示的那样,无论进程倒下几次,都会重新爬起来。采用任其崩溃方案仍然需要重视代码的执行顺序,因为代码运行到一半就崩溃所造成的负面影响应该尽可能小。此外,也不应该就此放任那些容易识别和处理的错误。