12 年前,我们创建了 PostCSS,一个月下载量达 4 亿次的 CSS 自动化工具,被 Google、Wikipedia、Tailwind 以及 38% 的开发者使用。在这篇文章中,我们分享了在维护这样一个受欢迎的开源项目的漫长旅程中学到的经验。
一些历史和早期经验
2013 年,我决定不再想手动管理 CSS 中的厂商前缀,比如 -webkit-。当时常见的解决方案是使用 Sass mixins,但我想要更自动化的东西。最好的 UI 就是在没有任何 UI 的情况下解决你的问题。 所以,我创建了 Autoprefixer,一个读取 CSS 作为输入并生成带有厂商前缀的新 CSS 的工具。
对于那个项目,我需要一个 CSS 解析器和 API 来处理 CSS。我找到了 TJ Holawaychuk 的 Rework。Autoprefixer 的第一个版本基于 Rework(第一个名字甚至是 rework-vendors)。
我很快发现 Rework 对 Autoprefixer 来说不够用。例如,我想保留 CSS 中的原始空白,以便能够将 Autoprefixer 用作文本编辑器插件。
由于 TJ 决定不向 Rework 添加这个功能,我意识到我需要编写自己的 CSS 工具框架。
经验-1: 与你的大用户更加合作。至少给他们一个机会发送带有原型的pull request。

乌克兰利沃夫。PostCSS 首次发布前几天。
结果,PostCSS 的第一个版本发布了。
我面临着一个截止日期,而且是一个相当不寻常的截止日期。你看,我心中有一个预设的日期来开始一个”数字排毒”项目(在这个术语被发明之前):整整一个月不接触任何带CPU的东西,看看这会如何影响我的思维。
有趣的事实:Autoprefixer 甚至不是第一个 PostCSS 插件。一个月前,pixrem 就开始使用 PostCSS 了。
正如我在流行开源指南中提到的,受欢迎程度是直接努力的结果。用于推广的资源量与编写代码本身相当。

PostCSS下载量的增长,与React下载量对比
例如,当 PostCSS 被用作 Webpack 中的 CSS 解析器时,PostCSS 的受欢迎程度得到了很大的推动。但这不是偶然的,我向其作者建议了 PostCSS,并提供了许多支持这样做的论据。
或者另一个例子,我们花了整整一天时间润色 PostCSS README.md 的前几句话。
经验0: 不要忘记为编写文档和推广你的开源项目预留大量时间。另外,不要犹豫直接接触潜在客户,建议他们使用你的库。

现在,经过这 12 年的发展,几乎所有开发者都在使用 PostCSS。即使是那些不直接使用它的人也经常将其作为依赖项使用(比如在 Webpack 或 Vite 中)。
PostCSS 的目标是帮助为 CSS 工具带来新想法。帮助开发者实验 CSS 语法和工具。实际上,我们最初将 PostCSS 视为仅供 CSS 工具开发者使用的内部框架。一个最终用户不应该知道的框架。
但很快,我们发现开发者害怕在他们的前端构建管道中添加任何新工具。相比之下,我们发现最终用户更愿意向已经在他们构建管道中的工具添加新插件。
最终,这就是我们让 PostCSS 对最终用户可见的原因。我们的目标是带来 CSS 工具的多样性。所以我们决定成为一个营销平台,帮助 CSS 工具解决最终用户对添加新工具的恐惧。
经验1:在插件和内置功能之间取得平衡
如果你有插件,请为大多数用户提供开箱即用的完整工作解决方案。只有有独特需求的人才应该禁用/启用插件。
默认情况下,PostCSS 什么都不做。要让它工作,你需要添加特定的插件,比如 Autoprefixer、Tailwind CSS、postcss-preset-env 或 postcss-mixins。
这种方法有两个问题:人们不喜欢做选择,而且大多数项目无论如何都需要相同的功能。
结果,PostCSS 用户总是抱怨在这个庞大的插件列表中苦苦挣扎,不知道他们需要什么。
将此与令人惊叹的 Lightning CSS 进行比较,它已经内置了基本功能(如 polyfills、打包和压缩),只有在特定用例中才需要插件。
一个类似的例子是 Vite vs. Webpack。Vite 在大多数情况下开箱即用,而 Webpack 即使在最标准的用例中也需要配置。
这让我想起了 Ruby on Rails 的良好实践:约定优于配置。为用户建议一些默认值;不要要求他们定义一切。
但这并不意味着一切都应该内置。PostCSS 从插件架构中获得了很多好处:
PostCSS 核心很小;我一个人支持它要容易得多。
我们没有一个有很多开发者和冲突的庞大项目,而是分成了几个有自己团队的项目:核心、CSS polyfills、压缩器 等等。
插件是在 CSS 工具中进行大量有趣实验的好方法,比如
postcss-easing-gradients,它甚至被转化为 CSSWG 的草案提案。插件是必不可少的,不仅用于公共实验,还用于特定项目中独特的内部需求。例如,在我的项目中,我经常有一些自定义插件,这些插件不可能做成通用的。
开发工具总是需要一些灵活性,因为每个项目都有独特的需求。
经验2:不要害怕太晚
就在我创建了我最受欢迎的两个项目之后,人们告诉我这些项目太晚了,已经过时了。
就在 Autoprefixer 的几个版本之后,Chrome 团队宣布他们不会再添加任何新的厂商前缀。在这一点上,我甚至考虑过放弃并完全放弃我的项目。然而,即使在 12 年后,Autoprefixer 仍然有 1 亿次月下载量。
同样,在 PostCSS 启动后不久,CSSWG 宣布了 CSS Houdini,一个让浏览器中的 CSS 更加灵活的项目。很多人告诉我,没有人会需要 PostCSS,因为人们将使用这些新 API 在浏览器中进行 CSS 自动化。12 年后,CSS Houdini 还没有完成(至少不是那些评论者当时期望的形式)。
所以,不要害怕 AI 取代你的工具或其他竞争对手。唯一能真正证明真实市场的是它在实践中的表现。新技术可能不如承诺的那样有用。新的竞争对手可能太慢而无法竞争。简而言之,最好创建一个快速原型并看到真实的结果。
经验3:对于性能,架构比编程语言更重要
PostCSS,用 JS 编写,比 Sass 快 4 倍,而 Sass 是用 C++ 编写的。不是因为 JS 是更好的编程语言,而是因为更好的架构和内存管理。
JS 社区被炒作毒害了,大公司经常使用它。结果,我们都在用原始的黑白概念思考,比如「C++ 比 JS 快」或「用 Rust 一切都更快」。
用 Rust 编写的 Lightning CSS 比 PostCSS 快(总的来说它是一个优秀的工具)。也就是说,我认为这是因为 Devon Govett 能够进一步改进架构和内存控制,而不仅仅是将相同的算法从 JS 重写为 Rust。
当然,这并不意味着 Rust 是一种糟糕的语言。它是一种用于良好内存控制的令人惊叹的语言。
最初,PostCSS 甚至比它的灵感来源 Rework 还要慢。但我的朋友 Ravil Bayramgalin 将 PostCSS 转换为适当的分词器-解析器架构。≈80% 的解析时间在分词器中,通过这种分离,他能够将优化集中在一小部分上。然后他使用了许多巧妙的技巧,比如使用正则表达式快速跳过源字符串以找到闭合的 "。
我强烈推荐 CSSTree 作者 Roman Dvornov 关于 CSSTree 优化的这个演讲。它可以为小型 JS 服务的关键部分提供许多见解。
我特别想强调内存管理在性能中的重要性。例如,同样用 JS 编写的 CSSTree 当时比 PostCSS 快约 1.5 倍,主要是通过巧妙地重用对象来最小化对垃圾收集器的调用。
经验4:通过确保问题不会重复出现来避免倦怠
当我关闭由用户错误引起的问题时,我总是尝试添加警告、输入检查或文档澄清,以防止其他人犯同样的错误。
例如,如果问题只是 API 的误用,解释错误不会阻止其他用户犯同样的错误并再次开启新问题。在这种情况下,最好的方法是添加警告以防止 API 的误用。
PostCSS 有一个设置解析器的特殊选项,但许多用户将其放在插件列表中。我可以抱怨用户并被相同的问题淹没。相反,我添加了一个特殊检查,它会直接向用户打印警告。
if (plugin.parse) { throw new Error( 'PostCSS syntaxes cannot be used as plugins. Instead, please use ' + 'one of the syntax/parser/stringifier options.' ) }
这真的帮助我作为开源维护者避免了倦怠。有趣的事实,这个想法的来源来自我阅读太空工程故事的爱好。
因此,防止问题的最佳方法是:
添加类型以防止错误使用 API 的方式。
添加额外的 JS 代码来检查 API 使用情况。
添加文档应该始终是最后一步,因为许多用户不阅读文档。但通常这是唯一的选择(Autoprefixer 文档中的巨大 FAQ 部分是减少新问题开启的一种方式)。
快速提示:要求问题创建者修复文档。这很重要,因为作为完全了解工具的人,你不太能够修复文档,因为你不知道如何学习你的工具。
倦怠的另一个风险是内疚循环。这是当你有很多没有回答的问题时,你开始责备自己,这会消耗你的动力,导致更大的积压。
我的解决方案:尝试快速回答,但要求问题创建者修复代码。
开源不仅仅是免费支持,而是关于协作。此外,你这边缺乏代码不应该阻止你工具的用户自己实现修复,因为他们知道如何编写代码。开源工具用户的主要挫折不是缺乏即时修复,而是被忽视的感觉。一旦他们看到你听到了他们的声音,他们通常很乐意自己贡献解决方案。

经验5:在第一个主要版本中弃用,在下一个版本中移除
对 API 进行重大更改的最佳方法是 tick-tack 方法:
在第一个主要版本中,将 API 标记为已弃用。
然后,仅在下一个主要版本中移除此 API。
顺便说一下,我从 Ruby on Rails 中采用了这种方法,这是一种常见做法。
当然,并不总是可能以这种方式进行(例如,我们在单个主要版本中移除旧的 Node.js 版本)。但在可能的情况下,最好尝试找到这样做的方法。
当然,在发布新的主要版本时发布分步迁移指南总是一个好主意,就像我们为 PostCSS 8.0 API 变更所做的那样。
迁移通常需要生态系统变更。例如,对于许多 PostCSS 主要版本,我们需要在 Webpack、Vite 等构建工具中进行一些更改。对于这个问题,创建一个包含生态系统迁移状态的 Wiki 页面总是一个好主意。

对于重大 API 变更,在发布前宣布它们并创建一个渠道来收集任何潜在阻碍因素的反馈是一个好主意。
经验6:提供最佳实践来塑造生态系统
使用指南、示例和样板来推广好的实践并阻止坏的实践。不要假设人们会自己发现所有最佳实践!
我们很高兴强制每个 PostCSS 插件都有清晰的输入和输出 CSS 示例。这是因为我们创建了插件样板并在模板中放入了输入/输出示例。
不要忘记,文档中的示例不仅仅是插图,而是在社区中形成习惯的东西。明智地使用它们!
经验7:与竞争对手成为朋友
不要害怕推广你领域中的新项目。
许多人认为 Sass 是 PostCSS 的竞争对手(不过这是一个更复杂的问题)。但实际上,我们没有相互斗争,而是保持了相当好的关系。
例如,我经常说可以将 Sass 与 PostCSS 一起使用。我们在命名和框架方面达成了协议以减少冲突。我们一起在 CSS 工具基准测试上合作,以正确地代表每个工具。
最终的结果是 Sass 作者将 Google 迁移到了 PostCSS 并给了我们很大帮助。
但我们也对新工具持开放态度。当 CSSTree 发布时(一个比 PostCSS 更快的 PostCSS 竞争对手),我们也在社交媒体上推广了他们。当 Lightning CSS 改进了性能和开发体验时,我们也介绍了他们。
我个人认为,在开源领域,我们都在免费工作,任何新的「竞争对手」实际上都可以让你从免费支持用户的时间中解脱出来。
经验8:人情味对社区很重要
尝试与生态系统中最有价值的开发者保持面对面的联系,例如通过发送明信片或与人们见面。
生态系统不仅仅是代码。它也是人。人际关系不仅仅是文本和流程。
有一次,我写信给每个 PostCSS 插件开发者,询问他们的家庭地址,然后给他们寄了一张带贴纸的明信片。很多人都喜欢它。但这也让我对生态系统中有多少人产生了更深的感受。在那一刻,一个抽象的数字变成了真实的东西。
唯一的问题是我没有花足够的精力来扩大明信片发送的规模!一年大约是≈20 张,但第二年增加到了≈100 张。

给每个为 PostCSS 制作插件的开发者发送新年明信片。我花了 2 周时间来签署所有这些明信片
另一个建议:创建一个记录,记录你生态系统中最活跃参与者居住的城市。然后,下次你去那里旅行时,你可以与他们面对面见面。
接下来这件事不是建议,但这是我个人喜欢做的事情。我的大多数项目背后都有一些风格。Autoprefixer 是关于骑士的(在发布帖子中有骑士艺术,骑士座右铭作为发布名称)。对于 PostCSS,我们选择了炼金术风格(因为每个骑士都有自己的梅林)。你可以在没有共同风格的情况下制作一个伟大的项目。但坚持这样做一直很有趣。例如,看看 Andrey Okonetchnikov 设计的令人惊叹的 postcss.org。

PostCSS 网站
给开源维护者的小贴士
最后,一些「有争议的」建议供思考,但可能不总是要遵循。
尽量避免在你的库中使用构建步骤。 当然,对于 Web 应用程序,几乎不可能避免构建步骤。但保持你的开源库无构建并不那么困难。在 PostCSS 中,我们将源代码保持为原生 JS 文件,并手写
.d.ts文件(或者你甚至可以像 Svelte 那样在 TypeDoc 中定义类型)。没有构建步骤,用户将能够通过pnpm add postcss@postcss/postcss#branch来测试拉取请求。或者你可以快速调试并修复node_modules/postcss中的错误,然后将其复制粘贴回仓库。React 对于项目文档等静态网站来说很糟糕。 使用 Astro 或只是静态 HTML 文件。我们为 postcss.org 尝试了 React,发现 React 需要大量资源来支持它。对于你的大型 Web 应用程序来说这没问题,但对于你一年只想访问一次的小型静态文档来说不可扩展。
有趣的事实:PostCSS 在构建步骤上做了很多尝试,但我们拒绝了它,因为我们有很多相关经验。
最初,该项目是用 CoffeeScript 编写的,然后我们尝试了大多数 ES6 到 JS 编译器(Traceur、es6-transpiler、6to5、Babel)。


