祖传代码迁移到Vue的另一种解题思路

2020 年 7 月 24 日

祖传代码迁移到Vue的另一种解题思路

随着时间的流逝,我们在应用程序中添加了许多新功能,应用程序变得越来越大,而技术环境在不断变化,各种新的框架、组件、架构不断涌现。当开发人员回头审视多年前写的代码时,你可能很想把它们全都丢掉并重写。但很多时候由于重写的风险和复杂性,这么做是不可能的,你必须找到一种让旧代码和新代码共存的方法。 本文 介绍了 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

2020 年 7 月 24 日 14:30 3206
用户头像
蔡芳芳 InfoQ高级编辑

发布了 454 篇内容, 共 210.2 次阅读, 收获喜欢 1150 次。

关注

评论 1 条评论

发布
用户头像
能不能放个demo出了
2020 年 08 月 25 日 17:17
回复
没有更多评论了
发现更多内容

架构师训练营第一期-第二周课后-作业一

架构师训练营第一期

C++的匿名函数(lambda表达式)

良知犹存

c++ 编程开发

HashMap源码解析

彭阿三

hashmap HashMap底层原理

第13周作业

大作业二:总结

zcj

为什么选择敏捷软件开发-考虑敏捷开发的主要优势

小隐乐乐

敏捷开发

第二周-框架设计-学习总结

刘希文

Flink 源码 | 自定义 Format 消费 Maxwell CDC 数据

Apache Flink

flink

不一样的面向对象(一)

书旅

php 面向对象

华为,与山河共舞这支芭蕾

脑极体

【MySQL】面试官:如何添加新数据库到MySQL主从复制环境?

冰河

MySQL 高可用 主从复制

多端消息推送的设计思考

Nil

Java spring 设计模式 消息推送

TensorFlow 篇 | TensorFlow 2.x 基于 Keras 的模型构建

Alex

tensorflow keras model

面试官,ThreadLocal 你要这么问,我就挂了!

小傅哥

Java 小傅哥 面试题 ThreadLocal 开放寻址

腾讯看点基于 Flink 的实时数仓及多维实时数据分析实践

Apache Flink

flink

Initialization failed for 'https://start.spring.io' Please check URL, network and proxy settings解决办法

Geek_416be1

甲方日常 21

大橘子

生活 工作 随笔杂谈 日常

Spring 5 中文解析数据存储篇-DAO支持

青年IT男

Spring5

Java8 之 Lambda 表达式

hepingfly

Lambda java8 新特性

极客大学 - 架构师训练营 第二周

9527

中国消费者独享长达三个月的年终跨境网购狂欢季

爱极客侠

week13--课后总结

Geek_165f3d

阿里架构师不慎泄露内部互联网架构面试题库。你确定不看一下吗?

小Q

Java 学习 架构 面试 阿里

阿里3轮面试都问了RecyclerView

Geek_211aa0

android 面试题 阿里 移动开发 RecyclerView

软件开发的 5 条核心原则,让工作事半功倍

沉默王二

程序员 软件开发

提高网站的吞吐量

架构师修行之路

你觉得Android又凉了?那带你看下2020年Android开发的前景如何?

Geek_211aa0

android 程序员 中年危机 移动开发 前景

week13---课后作业

Geek_165f3d

第二周 框架设计学习总结

钟杰

架构师训练营第 1 期

从 LRU Cache 带你看面试的本质

码农田小齐

算法

动图演示:手撸堆栈的两种实现方法!

王磊

Java 数据结构 算法

祖传代码迁移到Vue的另一种解题思路-InfoQ