随着时间的流逝,我们在应用程序中添加了许多新功能,应用程序变得越来越大,而技术环境在不断变化,各种新的框架、组件、架构不断涌现。当开发人员回头审视多年前写的代码时,你可能很想把它们全都丢掉并重写。但很多时候由于重写的风险和复杂性,这么做是不可能的,你必须找到一种让旧代码和新代码共存的方法。 本文 介绍了 THRON(一个用于管理数字资产和产品信息的 SaaS 产品)的研发团队在将他们的 Web 应用程序从 KnockoutJS 迁移到 Vue 的过程中,如何让旧代码和新代码共存的技术思路和实践。
本文最初发表在 Medium 博客,经原作者授权,InfoQ 中文站翻译并分享。
我们从 2013 年开始编写公司的主打产品。这个产品是我们写的第二个单页应用程序(SPA),之前写的第一个 程序 是一个小项目。我们分析了前面的经验,避免重蹈覆辙。
当时,浏览器和 js 库的大环境和今天不 太 一样,我们的企业客户目标 需要覆盖 IE9 用户,并且我们不信任大型复杂框架,因此更愿意采用一组独立的库。我们建立了自己的框架:灵活、易用,还有我们想要的 很多 功能,如数据绑定、模板、路由、国际化……
至于那些缺少的功能,我们可以自由选择自己喜欢的库,自然优缺点都要照单全收。这就是一种“选择太多”的情况。
我们写过的其他一些文章也提到了我们拆分单体应用程序的旅程:代码库影响,前端代码架构。
随着时间的流逝,我们在应用程序中添加了许多功能;我们变得越来越熟练,技术环境也在不断发展;但最重要的是应用程序变得越来越大。我们尽了最大努力来保持最高的代码质量,但正如你所知,一份代码过了多年之后,再看到它的开发人员 总 会想扔掉它然后重写的。对于大型 SPA (单页应用程序) 来说,由于重写的风险和复杂性,这么做是不可能的。我们必须找到另一种方式来满足新需求:
我们希望能够在 不影响性能 的情况下向应用添加新的 “ 部分 ” (或称功能部分,section)。我们的 SPA 并未遵循“代码拆分”原则,因此每个新接口都被添加到了单个复杂代码块中。我们有太长时间只顾着添加功能了,当我们意识到应该做代码拆分时已经晚了:所有内容都紧密耦合在一起, 已经 无法拆分开 了 。
我们希望能够采用 现代化、速度更快的框架 ,但更重要的是,我们要能自由更新或改变它们。对于许多复杂的接口来说,使用 7 年之久的库所提供的性能是无法与 Virtual DOM 之类的现代库所能达到的性能相提并论的。
收集分析数据后,我们意识到市场已经足够成熟,所以可以将 支持的浏览器范围缩小到 IE11+ 。相比保留 IE9+支持的时候,这就带来了更多可能性。
我们希望能 将代码的开发和维护任务分散到并行工作的不同团队成员之间 。业务不会 变慢 或停 滞 ,因此我们仍然希望能快速推出新功能,但我们希望新功能是用新库做的,因此必须找到一种让旧代码和新代码共存的方法。
我们想要一个 更轻松、更敏捷的开发流程 。在当前架构中,需要添加某部分内容的开发人员可能必须了解整个架构才行,以免引入回 退 Bug 。我们 寻找 的解决方案必须允许组件之间更好 地 耦合,并让开发人员将学习范围缩小到他们的任务领域上。
但是我们如何在不停止产品发展的前提下实现所有这些目标呢?想要 在一个版本内重写整个应用程序是不可能的 。
我们使用的方法
我们将工作分为三个阶段:
第一步:从头开始,假装要重新设计一切
我们试着抛开现有的应用程序,从头开始想象一个新的架构,并参考流行的框架和著名的应用程序来获得灵感。
然后我们创建了概念验证(POC)解决方案来验证我们的需求。在开发它时我们添加了另一个要求:它必须提供应用 各部分管理层面的框架独立性 。也就是说我们 不能紧密绑定在特定框架上 。我们迟早会需要一个新框架来处理一些特殊的需求或提升可维护性,希望那一天到来的时候我们用不着再重构了。
POC 的想法是做一个“加载器”,它可以逐一导入我们需要的部分,同时尽量控制依赖项的数量,直到我们想要导入用不同框架编写的部分为止。在这种情况下,旧应用程序只不过是要加载的 其中 一个部分而已。
我们发现,既不想停下产品开发工作,又要对应用程序进行现代化改造,这就是唯一的选项:将旧的界面视为新应用程序中的一个特殊部分。
基于这些需求,POC 的核心就变成了一个“功能部分管理器”,它以 Vue Router 及其强大的导航保护为基础。当用户请求某个部分时,我们会检查其功能并加载正确的代码块。
旧 版 应用程序结构如下:
旧版 应用程序的加载过程
为了将旧的应用程序作为一个模块加载进来,我们需要做一些更改:
旧的应用程序引导器是一个轻量级模块,用于管理应用程序启动相关的基本行为,例如资源加载、移动应用程序重定向、品牌颜色、安全偏好等。这是我们的 SPA 中唯一被完全重写的部分。这部分的语句不是很多,因此我们也借此机会删除了过时的逻辑。
旧的应用程序使用了整个 HTML 正文来渲染接口。必须更改这个方法,让它在给定的 HTML 元素而不是整个正文部分上运行。
我们被迫更改了导航 API。旧版应用程序使用的是 window.location.hash,而不是功能部分加载器 Vue Router 所使用的现代化 window.history.pushState。在第一阶段我们试图让它们共存,但在添加了 IE11 支持后我们搞出了一个“怪物”,连一致性都无法保证。我们发现唯一的解决方案是将对主路由的引用注入到刚加载的部分,这样整个应用程序导航都会使用 Vue 路由。
然后我们遇到了一个问题:新的应用程序允许我们只创建新的部分,这是可以创建最小实体,但在某些地方我们觉得它还是太大了。为什么我们只停留在“功能部分级别”呢?
于是 ,我们找出了希望使用新架构管理的三种方案:
为应用程序创建新部分的情况不是很常见,添加新的子部分(subsection)的频率则要高得多:
在神奇的 CSS 的帮助下,我们可以仅在新接口中使用嵌套路由视图,在旧的部分使用现代化技术编写子部分。
我们还规划了一种用新技术编写组件的方法,这些组件可以嵌入到旧版和新版的部分。我们使用了类似嵌入的方法,每个组件都有一个 init 方法,通过传递包含该组件的 DOM 元素来调用。
事后看来,这是一个非常好的主意,因为我们确实找到了可以让我们走向现代化开发的场景:
搜索区域是组件被用在不同部分或不同应用程序中的一个例子
第二步:更改代码库结构
开始开发 SPA 时我们有一个很好的想法,就是在组织源代码时将视图和控制器分离开来。随着时间的推移我们意识到,只要按各个部分简单分组,就可以帮助新加入的开发人员更方便地查看源代码和熟悉项目。
因此,我们先来整理存储库:
托管标准的 Vue 项目结构,让刚开始接触代码库的开发人员或公司新人更容易理解代码(为什么选择Vue);
按功能部分区分资源。旧版代码被降级到了一个文件夹中,我们的目标 是 逐渐减少那里的代码(当我们重构时),最终把文件夹挖空:所有旧代码 最终 都会转换成新的。
第三步:切换技术
实践证明,通过 POC 收集的经验非常宝贵。我们利用这些经验编写了真正的加载器,并在新加载器中将旧应用程序作为一个简单模块来运行。
但总有不顺心的事情。如果我们有机会重写整个遗留部分,就可以极大地提高性能,相反,在加载遗留部分时,我们的性能还是以前那个水平。最终用户并没有从此次升级中受益。
我们对整体重写感到不放心的原因之一是,自动化测试套件并未涵盖所有部分。而没有自动化测试套件就会错过很多边缘场景或用户模式,这可能导致代码中出现新的回 退 Bug 。这是我们日积月累总结出的教训:测试可以极大提升你对重构的信心,因为人类的记忆可能会出问题,但测试结果可以永存。
结论
到最后,事实证明采用现代 JS 框架是一项了不起的进步:开发人员可以专注于应用程序逻辑,而不是底层细节。对于每个新功能,我们现在需要维护的代码更少,并能为客户提供更好的性能。旧代码和新代码共存在独立于框架的组件中,这些组件共同构成了我们的 SPA。
新架构还为我们带来了其他改进:
更简单的架构简化了优化工作。现在,我们更容易将一个较大的部分分割成一些较小的块,并且维护起来也更简单。
开发人员不会感到无助:现代框架意味着充满活力和乐于助人的社区。
更少的遗留代码意味着“黑匣子”也变少了,更多代码可以从更广泛的开发社区获得参考,从而提高了质量和可维护性。
我们的下一步将是改进测试套件。
你是否也有过类似的经历需要让新旧代码共存呢?你是如何应对的?请在评论区留言告诉我们 。
原文链接:
https://medium.com/thron-tech/keep-legacy-code-or-rewrite-a-middle-way-dc77c2f76e5f
评论 1 条评论