Angular
官方网站针对 从 AngularJS 升级到 Angular 提供了比较详细的文档,并给出了一个 PhoneCat 升级教程 的案例演示,指导一步步如何改造。但总的来说,这个案例还是太过简单,并不能很好地还原一个最原始的、相对复杂的、版本更低的遗留项目该如何一步步升级,以及升级过程中可能需要考虑的一些额外因素。
本篇文章会以一个相对复杂的遗留项目为原型来探讨该如何一步步进行渐进式地升级改造,以及针对不同情况可以采取哪些策略,算是一篇结合了实际项目改造后的经验之谈。
遗留项目按照不同业务拆分成了多个业务模块和一个公共模块,即有多个代码仓库,如下图:
从上图可知,遗留项目中主要使用的是 AngularJS 1.4
,只有一个模块D
使用了高版本的 Angular
。其实如果正确的业务划分,模块D
是属于模块C
的一个子模块(或一部分)。
原本是考虑将模块C
拆分为更多更小的模块,再加上想尝试用较新的 Angular
技术栈来写新功能,在种种原因的促使下最终有了 Angular
高版本的模块D
。但是,在后续的开发实践中发现这样的拆分是存在一定问题的(比如维护两套类似逻辑的代码、修改容易疏漏等等),不过由于已经用了高版本的 Angular
,无法再简简单单地合并回去。
最终,除了需要考虑如何将 AngularJS 1.4
一步步升级到高版本的Angular
,也需要考虑在升级到一定程度之后将相同业务模块的代码仓库进行合并。
这部分主要包含实际改造中遇到的一些硬性限制以及相对应的升级改造原则。
(1)代码量与时间上的限制
首先,作为遗留项目,各个仓库的代码量不一(有多有少),但总体的量是非常庞大的。因此不可能在短期内或一次性全部改造完成。对此较好的策略是从较小的仓库开始着手,这样既能用较小的成本来做技术预研,判断改造方案的可行性,也能较好地控制改造后的风险。改造成功一个之后,则可以依葫芦画瓢慢慢铺开,改造剩余的仓库。
其次,现有遗留项目还在不断地改旧增新,这也将占据大部分的编码时间,并且还存在定期的发版上线,在做技术升级改造的同时需要优先保证正常的功能开发与发版。简而言之,升级改造和改旧增新是并行的,升级改造需要兼顾改旧增新。
不论是从代码量上考虑,还是从改造时间的不连续性上考虑,新旧代码必然是长期共存的,并且为了保证正常的发版,升级改造也必须是渐进式的增量升级。
(2)公共模块便利带来的升级限制
所谓成也萧何败萧何,在升级改造中,公共模块的存在是最为尴尬的。正是因为其复用得多,对其改造后的影响范围也是最大的,比如改一个公共的组件就需要检查并修改所有引用了公共模块的仓库。
特别是如果要对公共模块中使用的某个库进行升级,那么所有引用公共模块的模块也都必须同时升级,并且还需要检查 break changes
的影响,这很有可能需要较大的工作量才能完成。因此有时不能只从升级的可行性上考虑,还需要考虑升级的必要性和其后所能带来的收益大小等等。(这个问题在 angular-ui-boostrap
的升级改造中遇到,后面详谈。)
总的来说,虽然不同的遗留项目可能面对的情况和限制或多或少会有所差异,但大体上升级改造无法做到一步到位,将会是一个长期的过程是比较常见的情况,对此所需要的则是一个渐进式、增量升级的过程与解决方案。
这部分所谈的演进方向主要是一些概要描述,不包含具体的实践细节。
(1)代码风格改造
遗留项目中第一优先需要改造的就是代码风格。由于 AngularJS 1.4
版本本身的特性限制,遗留项目中存在着大量的 replace:true
、变量绑定在 $scope
上、文件目录不清晰等与 Angular
规范不太匹配的代码。
而要改造好代码风格,与升级相比还是较为容易实现,只需要约定好相应的规范(可以参考 Angular
官网推荐的 风格指南),之后则是花费工作量的事情。
(2)AngularJS 1.4 升级到 1.5
从以下三方面综合考虑,有必要将遗留项目中使用的 AngularJS 1.4
至少升级到 AngularJS 1.5
:
第一: 升级的代价相对较小。因为毕竟是小版本的升级,虽然存在一定的 breaking changes
,但根据官方提供的迁移文档,所需更改相对较少,只需要针对性的检查一番和做少量修改即可。详情可见此
第二: AngularJS 1.5
新增的特性将有助于更方便地实现新功能(比如 component
、单向绑定、新的生命周期等)。
第三: AngularJS 1.5
新增了组件API,有助于改造遗留代码的风格。不论是在代码风格上还是在组件的生命周期上,其都比较像 Angular 中的等价物,在此基础上将代码升级到 Angular
时会更容易。
(3)引入 TypeScript
这里所说的引入 TypeScript
,并不会像官网案例中那样引入了 TypeScript
后就将所有文件直接改为 .ts
文件,而是依旧采用渐进式的升级改造方式。可以通过借助 webpack
打包工具,让项目同时支持 .js
和 .ts
两种文件格式,有针对性的使用相关插件,最终统一生成 js
的目标文件。这样就可以不用一次性将全部文件改为 .ts
文件,把改造的影响降到最低,只需要在后续改造中一步步将 .js
替换为 .ts
文件即可。
另外,在现有遗留项目中,针对 js
文件有用 eslint
进行代码风格检查与约束。现在添加了 TypeScript
,针对 ts
文件同样也可以使用 eslint
。从 eslint 6.0
之后可以根据不同的文件后缀使用不同的规则,这样就可以同时支持 js
和 ts
两种文件。
(4)引入 angular-ts-decorator(可选)
在将 AngularJS
升级到 1.5+
之后,可以通过引入 angular-ts-decorator 以 Angular 2
的代码风格对遗留代码进一步改造或直接编写新业务。angular-ts-decorator
的原理很简单,其实就是借助装饰器,将 AngularJS
模块声明、指令、控制器声明全部包装了一层,其内在实质没有变化。
简而言之,可以通过使用 angular-ts-decorator
将 AngularJS
的代码风格改为如同 Angular 2
代码风格,在享受 Angular
风格的代码带来的便利性的同时,也方便后续的升级改造。
到这一步,你或许会有所疑惑,因为按照官网的升级改造,似乎完全没有必要进行这一步。在必要的代码风格改造 +
引入 TypeScript
后,其实就可以直接进入到开启 AngularJS + Angular
的混合模式了。然后就可以快乐地用高版本的 Angular
的写组件,新功能完全用高版本写,至于涉及到 AngularJS
的部分,利用组件的升/降级方案,可以在 AngularJS
和 Angular
两边混用组件 ,一切看起来似乎很美好,但实际情况会有这么简单和容易吗?
一方面, 需要考虑“改旧增新”开发新功能会占据主要时间,技术升级改造的时间相对较少且不连续。而在项目中引入 angular-ts-decorator
库的工作量是极小的,基本上可以开箱即用,只需要写一两个样例,整个团队就可以按照新风格来写 AngularJS
。这将直接提升团队整体的开发体验,同时新写法与升级后的 Angular
组件很类似(除了 html
依旧是 AngularJS
写法),除了方便后续的升级改造,也更易于维护。
另一方面, 升/降级组件其实都没有想象中那么简单。这里的不简单主要受限于过滤器/管道、属性指令以及第三方 UI
组件库这三个方面(具体在后面遇到的难点中详谈)。如果能够较好的解决这三个问题,那么升级 AngularJS
的组件为 Angular
的组件相对来说就比较容易。
也因此,虽然这一步是可选的,但结合项目的具体情况,其也可能变成是必要的。
(5)启用 AngularJS + Angular 混合模式
开启混合模式本身很简单,只需要引入 Angular
相关的库,然后在 Angular
中引导 AngularJS
模块加载启动即可。详细可见
(6)逐步升级替换 AngularJS
第一: 引入 HttpClient
来处理 Http
请求,并配置好相关的 Http Intercepters
。这会与 AngularJS
中的 $resource
以及配置的 $httpProvider
相关的策略相对应。
第二: 引入 RouterModule
,使用相邻出口配置 Angular 的路由策略,让混合应用同时支持 AngularJS
和 Angular
的两种路由。
第三: 如果遗留项目中用了第三方的 AngularJS
的 UI
组件库(比如 angular-ui-bootstrap
),首先考虑是否能够升级到对应的 Angular
的版本。如果不能或工作量实在太大,那么则需要考虑是否有可替代的 Angular
版的 UI
组件库,当然这会使得项目中存在 AngularJS
和 Angular
两套第三方 UI
组件库,需要考虑的样式和交互上的一致性。
第四: 全新的功能和页面,可以完全采用 Angular
组件和路由来写,而涉及到 AngularJS
的部分,如果不能一次性升级改造完,则可以采用临时的升/降级组件和服务,来实现混用。(总体原则:优先用高版本 Angular
组件或服务实现相关业务功能)
第五: 合并相同业务模块。因为已经开启混合模式,配置好了 Http
请求和路由策略,所以可以考虑将高版本的 Angular
模块合并到开启混合模式的模块中。
…
(7)最终目标
不论准备工作和具体的升级实施方案如何,技术升级改造的最终目标是简单明确的——合并相同业务模块,并将所有仓库的代码升级到高版本 Angular
。如下图:
(1)路由及路由组件的升级改造
Angular
官方文档在路由改造这一块考虑不是很周全或参考性不强,其升级改造方式并不是渐进式的。一般来说,大的遗留项目根本无法一次性将所有的路由组件替换完。因此需要考虑 AngularJS Router
和 Angular Router
两种路由的长期共存的可能性,并在改造中逐步用 Angular Router
去替换 AngularJS Router
。而这方面相关的解决方案,官方的升级文档中并没有提供,需要自己摸索或搜寻。
Tips: 如果考虑在混合应用中只用 AngularJS Router
路由,也是可行的。其中一种解决方案是将所有的 Angular
路由组件进行降级使用,或者如果 AngularJS Router
用的是 ui-router
, ui-router
官方也提供了一套对混合应用进行支持的方案 angular-hybrid 。但如果考虑到升级改造本身就是要替换掉 AngularJS Router
路由,那么首选混合路由相对较好。
(2)升级改造中的 breaking changes
对所有第三方库的升级,即使是次版本的升级,有时也会有一些 breaking changes
(比如 Angular 1.4
到 1.5
),这是升级时所必须注意的。而对相应的 breaking changes
则必须结合实际项目作出评估,判断出影响范围有哪些或者是否很大。如果影响范围很大或修改工作量太大,就需要考虑是否有升级的必要性。
另外,在升级过程中还遇到了依赖升级的情况。在将 Angular 1.4
升级到 1.5
后,在使用 1.5
新增的 component
组件特性时,发现其作为路由组件在项目中使用的 ui-router 0.4.x
中不支持这一特性,而它是从 1.0
及其以后开始支持的。这种大版本的变更必然带来 breaking changes
,在结合了官方的 UI-Router 1.0 Migration 以及项目中使用情况,梳理出 breaking changes
带来的影响点后,判定为影响相对较小可以接受,因此也连带着将 ui-router
升级到了 1.0
。
(3)官方 Angular 升级方案本身的限制
无法对 AngularJS
的过滤器 filter
以及属性指令 attribute directive
进行升级在 Angular 中使用,同时 Angular 的管道 Pipe
以及属性指令 attribute directive
也无法降级在 AngularJS
中使用。
这个主要会带来两个问题:
第一: 无法复用,一定时间内可能会同时存在类似逻辑的两份代码。
第二: 有时要升级一个 AngularJS
组件,会发现里面大量使用了过滤器 filter
以及自定义的属性指令 attribute directive
,升级一个组件的工作量会比预想中的大得多(不能很顺滑的升级组件)。
(4)第三方 UI 组件库的升级改造
遗留项目中主要使用的 UI
组件库是 angular-ui-bootstrap
,一个纯 AngularJS
的组件库。
首先,由于其引入的版本比较低只有 0.14.x
,其组件指令 component directive
实现还是用了 replace: true
等这些无法进行升级的特性,所以无法直接通过升级在 Angular
中使用。
其次,在遗留项目中不仅大量使用 angular-ui-bootstrap
的组件指令 component directive
,也使用了很多它的属性指令 attribute directive
,这也导致了就算将 angular-ui-bootstrap
本身进行升级(至少升到 2.0
,存在大量 breaking changes
),以使得组件指令 component directive
可以通过暂时升级的方式在 Angular
中使用,但属性指令 attribute directive
无法使用的问题仍旧无法解决。
也因此,最终放弃了升级 angular-ui-bootstrap
本身,而是考虑直接用一个高版本的 Angualr
的 UI
组件库进行替代,只要保证样式和基本交互能够基本一致即可。
综上,主要介绍了遗留项目的基本情况、项目中的限制与应当遵循的升级改造原则、大致的升级改造方向以及遇到的主要难点。后续系列文章准备将进一步讨论升级方案中一些步骤具体如何实践以及踩过的坑。
从 AngularJS 升级到 Angular
AngularJS migrating from 1.4 to 1.5