前端熵增是真实存在的。代码库不会在一夜之间崩溃,它是在不断累积的。某个周二,你添加了一个日期格式化库。六个月后,有人又添加了另一个,因为他们没找到第一个。为不再支持的浏览器准备的 Polyfill 堆积如山。构建工具层层叠加。最终,node_modules 文件夹变成了一个数字杂物抽屉,你不敢扔掉任何东西。你停止更新,然后停止关注。就在那时,每一次微小的改动都变成了一场豪赌。
在尝试升级一个旧项目中的 Material UI 时,我撞上了这堵墙。我打开 package.json,发现一半的条目都快认不出来了。几十个库躺在那里,有的已经过时多年,有的则如此晦涩,我不得不通过 git blame 来查找是谁添加了它们以及原因。我运行了新版 Material UI 的安装命令,终端瞬间被同级依赖(peer dependency)警告刷屏了。我想更新的包本身没问题,但它周围的生态系统出了问题。我意识到我不是在进行升级,而是在挖掘遗迹。
为什么混乱的代价远超面子
忽视依赖项不仅仅是美观问题,它会引发真实且昂贵的麻烦。
安全风险是显而易见的威胁。被遗弃的包带有已公开的漏洞,扫描器每周都会标记它们。更糟糕的是,你直接安装的库可能没问题,但它们引入的传递依赖(transitive dependencies)却有问题。你在不知不觉中继承了别人的技术债。
成本随时间复合增长。 等待的时间越长,版本差距就越大。跨越一个 React 大版本是工作量的问题;跨越三个大版本则是一个可能耗费数周的迁移项目。你不再获得错误修复、性能改进以及对现代工具的兼容性。团队最终不得不围绕着那些早已不存在的限制进行开发。
库会消亡。 一个没有活跃维护者的包,默认就成了你的私有分支(fork)。当它崩溃时,你就是那个在半夜阅读其压缩后的源代码的人。社区已经转向了更好的解决方案,而你的团队却被困在维护一个“幽灵”中。
开发速度骤降。 新开发者入职后的头几天,都在学习一些早已被 Web 标准或主流替代方案取代的工具所特有的 API。资深工程师不再是交付功能,而是变成了历史学家,解释为什么这个项目还在使用 2015 年的任务运行器。
在改动任何版本之前先进行审计
最严重的错误是进行一键式全量更新并寄希望于测试通过。从审计开始吧。拿起 package.json,审视每一个条目。
问四个问题:
- 它解决了什么问题?
- 我们具体在哪里使用了它?
- 它仍然是必需的吗?
- 现在是否有更好的替代方案?
你会发现冗余。也许 moment 和 date-fns 同时出现在列表中,是因为两名开发者在不同时间解决了同一个问题。也许即使你的分析数据显示来自旧版浏览器的流量为零,但仍携带着 Internet Explorer 的 Polyfill。也许可以删除对 fetch 的自定义封装,因为现代浏览器已经原生处理了这些边缘情况。
有时,替换优于更新。与其在被遗弃的图表库中挣扎于三年的破坏性变更,不如直接换成一个稳定的替代方案并重构几个组件。要敢于做减法。
隐藏层:传递依赖与 Semver
直接依赖只是冰山一角。真正的庞然大物隐藏在传递依赖(transitive dependencies)中,即你的包所依赖的包。你没有选择它们,但它们会在你的构建过程中运行。它们会让你的 Bundle 体积膨胀,扩大攻击面,并偶尔以产生晦涩构建错误的方式相互冲突。
你需要根据语义化版本(Semver)的实际含义来阅读它,而不是根据你希望它具备的含义。
- 大版本更新 (Major updates): 这些属于迁移。除非另有证明,否则请将其视为破坏性变更(breaking changes)。阅读变更日志(changelog),分配时间,并进行彻底测试。
- 次版本更新 (Minor updates): 这些会增加功能。它们也可能以微妙的方式改变行为。不要假设它们是无代价的。
- 修订版本更新 (Patch updates): 这些用于修复 Bug。它们通常是安全的,但如果你的代码依赖于该 Bug,或者补丁更改了你正在进行猴子补丁(monkey-patching)的内部实现,你仍然可能会导致程序崩溃。
了解这些规则有助于你在动手之前对风险进行分类。
像工匠一样使用你的工具
如果你使用 Yarn,有几个内置命令可以将猜测变成一个标准流程。
首先运行 yarn outdated。它会为你提供一个关于哪些内容已经发生 drift
