每个开发者都有这样一个文件夹。那个名为 utils 或 helpers 的文件夹,你会把它从一个仓库复制到另一个仓库。你把它粘贴进去,花二十分钟删除对旧数据库模式的引用,剥离不适用的身份验证检查,并重命名变量,好让你的新 linter 不再报错。我以前也这样处理我构建的主题系统——Dynamic Theme Kit。它最初只是一个应用程序内部的功能,好几个月来,我都把它当作一个便携式工具。但我错了。复制代码并不等于复用。它只是增加了额外步骤的重复。
单一项目思维的陷阱
当你在项目内部构建一个功能时,你会做出数百个无形的假设。颜色调色板可能假设了某种特定的 CSS-in-JS 配置。间距比例(spacing scale)可能会引用公司品牌指南中的设计令牌(design token)。深色/浅色模式的切换可能会调用该应用后端特有的用户偏好端点(endpoint)。这些依赖项在项目内部看起来是无害的,因为它们本就属于那里。
问题在于当你试图将代码剥离出来时。你会发现这个“可复用”的组件实际上是一个由隐藏字符串交织而成的网,将其与那唯一的代码库连接在一起。我在 DTK 上学到了这一点。它确实生成了主题变量,但它还要求特定的文件夹结构。它从原应用类型目录的深处导入了一个类型定义。它假设存在一个仅存在于该特定仓库中的全局配置对象。我以前从未注意到,因为在那个项目内部,一切都是现成的。
将 DTK 变成一个独立的包(package)意味着要做“手术”,而不是“扩张”。我不需要更多功能,我需要更少的连接。
提取 Dynamic Theme Kit
最艰巨的工作是坐下来审视代码库,并对每一个函数和每一个导出项进行询问:它是为主题逻辑服务的,还是为项目服务的?我剥离了样式预设。我移除了“使用者必须是一个 React 应用”的假设。我完全删除了默认的颜色调色板。原项目在默认设置中内置了海军蓝和板岩灰的企业审美,这些必须去掉。一个包不能携带你的品牌色彩。
新的工具包只做一件事:它接收一个配置对象——一些颜色值、一些间距数值、一些排版比例——然后生成 CSS 自定义属性(CSS custom properties)。仅此而已。它不会应用这些属性,不会决定它们在 DOM 中的位置,也不关心你使用的是 Tailwind、Styled Components 还是原生 HTML。它为你的应用程序提供变量,而你的项目决定如何使用它们。
这种约束起初让人觉得受限,但结果却带来了解放。
当你真正尝试复用时,哪些地方会崩溃
在发布任何内容之前,我需要证明这种抽象确实站得住脚。我从存档中抽出了三个小型个人项目:一个 Markdown 预览工具、一个习惯追踪器和一个活动的落地页。它们之间没有共同的框架或文件夹结构。我在每个项目中都本地安装了 DTK,并尝试为它们设置主题。
第一次尝试立即失败了。DTK 生成的变量名过于具体。它输出的 token 像 --primary-action 和 --background-overlay,这暗示了某种特定的 UI 布局。在 Markdown 预览器中,这些名称毫无意义——那里没有“动作按钮”,也没有“遮罩层”。我重新设计了生成逻辑,使其产生中性的、结构化的名称,描述的是数值本身,而不是组件(widget)。
我还发现我的默认值过于“激进”。当用户传递了一个不完整的配置时,DTK 会用一些值来填补空白,这些值在密集的仪表盘(dashboard)中看起来很正常,但在稀疏的落地页上却会出错。我改用了透明的默认值,即缺失的 token 直接不进行渲染,让使用该包的项目自行定义回退方案(fallbacks)。
然后是文档问题。对我来说显而易见的事情——“只需传递一个配置对象”——对于一个在半夜阅读 README 的人来说是非常模糊的。我用真实的配置对象、真实的文件路径重新编写了文档,并清晰地解释了调用函数时会发生什么,以及你的应用程序在调用后需要做什么。
这些小型个人项目充当了试验场。虽然它们风险很低,但它们暴露了那些仅靠孤立地盯着源代码是无法发现的真实缺陷。
真正的考验:在 Web Weavers World 的生产环境
个人项目就像沙盒。它们没有截止日期、利益相关者,也没有早于你这个包的遗留 CSS。真正的考验是我将 DTK 集成到我的商业网站 Web Weavers World 时。这是一个拥有现有样式、客户预期以及需要考虑的分析数据的在线站点。如果这个包弄坏了什么,我不能直接删除仓库重新开始。
我将 DTK 添加到构建流水线中,将其指向一个新的颜色配置,并让它生成一组全新的 CSS 变量。集成只花了一个下午,而不是一周。这就是信号。以前,添加新主题意味着编写新的 CSS,在二十个文件中寻找硬编码的十六进制值,并祈祷自己没有遗漏边缘情况。现在,我只需在配置文件中添加一个色板,DTK 就会生成变量,网站的其他部分直接使用它们。主题逻辑从一个脆弱的手动过程,转变为一个我足够信任、甚至可以交给协作人员处理的流程。
改变我构建方式的三个问题
通过这个过程,我被迫将一套心理清单正式化,现在我在进行任何抽象之前都会使用它:
- 这个变量是真的通用的吗? 如果名称或逻辑引用了原始项目中的领域概念,它就不应该被抽象出来。
- 这属于包还是属于应用程序? 业务规则、品牌身份和布局假设属于应用程序。负责生成标准化输出的底层逻辑属于包。
- 我是在解决一个可复用的问题,还是一个特定项目的问题? 这是最难诚实回答的问题。我们喜欢认为自己的解决方案是通用的,但通常它们只是局部的。
回答这些问题迫使我简化设计,通常是通过删除代码而不是增加代码。DTK 教会我,复用并不是你送给自己的礼物,而是一种通过拒绝便利来实践的自律。
关于重构的一种不同思考方式
我以前通过重构让代码缩短了多少来衡量重构。代码行数变少感觉像是进步。现在我通过重构开启了多少扇门来衡量。Dynamic Theme Kit 之所以优雅,并不是因为它简洁,而是因为它在经历了三个不相关的个人项目和一个生产环境的商业网站后,无需更改内部逻辑依然非常有用。
这才是重要的指标。运行一次的代码是开支。能反复运行的代码是资产。现在,在我开始任何功能之前,我会停下来。我会问自己是否正在构建一个以后会再次需要的东西。如果答案是肯定的,我会从第一行代码开始就以不同的方式进行构建。我隔离输入,定义输出,并消除假设。
最好的重构不会让你的代码变短,而是让你的代码能在你尚未想象到的地方发挥作用。
