2020 Google开发者大会重磅开幕 了解详情

祖传代码迁移到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 3244
用户头像
蔡芳芳 InfoQ高级编辑

发布了 474 篇内容, 共 217.4 次阅读, 收获喜欢 1266 次。

关注

评论 1 条评论

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

redis系列之——Redis为什么这么快?

诸葛小猿

Java redis 程序员

redis里的数据结构

流沙

redis

架构师训练营第六周作业

张明森

不会有人还不知道全文检索工具Lucene怎么用吧?文字长文教程

给你买橘子

Java 搜索引擎 lucene 程序员 开发工具

计算机的时钟(一):NTP协议

ElvinYang

基于Kubernetes实现的大数据采集与存储实践总结

岿然独存5

Docker Kubernetes S3 EFK Fluentd

啃碎并发(九):内存模型之基础概述

猿灯塔

Java 猿灯塔

图说前端-内存管理(1/3)

梦见君笑

前端 内存 前端进阶训练营

分布式系统的一些基础理论

俊俊哥

分布式事务 CAP Base

如果你想写自己的Benchmark框架

程序那些事

JVM 性能调优 GC benchmark

给 Spring Boot 项目减减肥!18.18M 到 0.18M 是如何做到的?

给你买橘子

Java 程序员 Spring Cloud 编码 SpringBoot 2

图说前端-ArrayBuffers 和 SharedArrayBuffers(2/3)

梦见君笑

前端 内存管理 前端进阶训练营

图说前端-使用Atomics避免SharedArrayBuffers中的race conditions(3/3)

梦见君笑

前端 内存管理 前端进阶训练营

RESTful 架构及实践

pingan8787

JavaScript 前端 RESTf

Java 线程的生老病死

武培轩

Java 线程 多线程 并发 线程状态

玩转Redis高可用 - 哨兵(Sentinel)模式

Man

高可用 redis高可用 中间件

DOM 树的构建

法正

html DOM 前端进阶训练营

游戏夜读 | 如何分析游戏体验?

game1night

java 后端博客系统文章系统——No3

猿灯塔

基础篇:JAVA基本类型

csc

java classpath Java 25 周年

使用 Dockerfile 创建镜像 | Docker 系列

AlwaysBeta

Docker 容器 镜像 Dockerfile 容器技术

《精益思想》读后感分享

zhongzhq

精益 精益思想 精益生产方式 高效工作方式

ARTS 打卡 第2周

Scotty

那些让程序员目瞪口呆的Bug

Java小咖秀

程序员 程序员人生 bug

猿灯塔:spring Boot Starter开发及源码刨析(三)

猿灯塔

Java 猿灯塔

如何基于 BitMap 进行海量数据分析

GrowingIO技术专栏

互联网 数据分析 科技互联网 数据化

【计算机网络】网络层——路由器与路由选择协议

烫烫烫个喵啊

计算机网络 网络层

无价值人生记录.0:浪费1000%时间去做一个用来节省1%时间的“轮子玩具”(上:因缘)

八苦-瞿昙

C# 程序员人生 随笔 随笔杂谈 aop

架构师必须知道的架构知识

Chank

架构 架构师 Architecture Architect

如何搭建一个HBase集群

Rayjun

HBase

基础篇:Object对象

csc

java string pool Java 25 周年

微服务治理平台化探索

微服务治理平台化探索

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