1. 程式人生 > >AngularJS 遺留專案的升級改造之路(一)

AngularJS 遺留專案的升級改造之路(一)

### 目錄 - 序言 - 遺留專案概述 - 條件限制下的升級原則 - 升級改造的演進方向 - 遇到的主要難點 - 小結 - 參考
### 1. 序言 `Angular` 官方網站針對 [從 AngularJS 升級到 Angular](https://Angular.cn/guide/upgrade#using-Angular-components-from-AngularJS-code) 提供了比較詳細的文件,並給出了一個 [PhoneCat 升級教程](https://Angular.cn/guide/upgrade#phonecat-upgrade-tutorial) 的案例演示,指導一步步如何改造。但總的來說,這個案例還是太過簡單,並不能很好地還原一個最原始的、相對複雜的、版本更低的遺留專案該如何一步步升級,以及升級過程中可能需要考慮的一些額外因素。
本篇文章會以一個相對複雜的遺留專案為原型來探討該如何一步步進行漸進式地升級改造,以及針對不同情況可以採取哪些策略,算是一篇結合了實際專案改造後的經驗之談。
### 2. 遺留專案概述 遺留專案按照不同業務拆分成了多個業務模組和一個公共模組,即有多個程式碼倉庫,如下圖: ![程式碼倉庫](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bcfb58c8746d40c4a582ae79623f2415~tplv-k3u1fbpfcp-watermark.image) 從上圖可知,遺留專案中主要使用的是 `AngularJS 1.4`,只有一個模組`D`使用了高版本的 `Angular` 。其實如果正確的業務劃分,模組`D`是屬於模組`C`的一個子模組(或一部分)。
原本是考慮將模組`C`拆分為更多更小的模組,再加上想嘗試用較新的 `Angular` 技術棧來寫新功能,在種種原因的促使下最終有了 `Angular` 高版本的模組`D`。但是,在後續的開發實踐中發現這樣的拆分是存在一定問題的(比如維護兩套類似邏輯的程式碼、修改容易疏漏等等),不過由於已經用了高版本的 `Angular`,無法再簡簡單單地合併回去。
最終,除了需要考慮如何將 `AngularJS 1.4` 一步步升級到高版本的`Angular` ,也需要考慮在升級到一定程度之後將相同業務模組的程式碼倉庫進行合併。
### 3. 條件限制下的升級原則 *這部分主要包含實際改造中遇到的一些硬性限制以及相對應的升級改造原則。*
**(1)程式碼量與時間上的限制** 首先,作為遺留專案,各個倉庫的程式碼量不一(有多有少),但總體的量是非常龐大的。因此不可能在短期內或一次性全部改造完成。對此較好的策略是從較小的倉庫開始著手,這樣既能用較小的成本來做技術預研,判斷改造方案的可行性,也能較好地控制改造後的風險。改造成功一個之後,則可以依葫蘆畫瓢慢慢鋪開,改造剩餘的倉庫。
其次,現有遺留專案還在不斷地改舊增新,這也將佔據大部分的編碼時間,並且還存在定期的發版上線,在做技術升級改造的同時需要優先保證正常的功能開發與發版。簡而言之,升級改造和改舊增新是並行的,升級改造需要兼顧改舊增新。
不論是從程式碼量上考慮,還是從改造時間的不連續性上考慮,新舊程式碼必然是長期共存的,並且為了保證正常的發版,升級改造也必須是漸進式的增量升級。
**(2)公共模組便利帶來的升級限制** 所謂成也蕭何敗蕭何,在升級改造中,公共模組的存在是最為尷尬的。正是因為其複用得多,對其改造後的影響範圍也是最大的,比如改一個公共的元件就需要檢查並修改所有引用了公共模組的倉庫。
特別是如果要對公共模組中使用的某個庫進行升級,那麼所有引用公共模組的模組也都必須同時升級,並且還需要檢查 `break changes` 的影響,這很有可能需要較大的工作量才能完成。因此有時不能只從升級的可行性上考慮,還需要考慮升級的必要性和其後所能帶來的收益大小等等。(這個問題在 `angular-ui-boostrap` 的升級改造中遇到,後面詳談。)
總的來說,雖然不同的遺留專案可能面對的情況和限制或多或少會有所差異,但大體上升級改造無法做到一步到位,將會是一個長期的過程是比較常見的情況,對此所需要的則是一個漸進式、增量升級的過程與解決方案。
### 4. 升級改造的演進方向 *這部分所談的演進方向主要是一些概要描述,不包含具體的實踐細節。*
**(1)程式碼風格改造** 遺留專案中第一優先需要改造的就是程式碼風格。由於` AngularJS 1.4` 版本本身的特性限制,遺留專案中存在著大量的 `replace:true` 、變數繫結在 `$scope` 上、檔案目錄不清晰等與 `Angular` 規範不太匹配的程式碼。
而要改造好程式碼風格,與升級相比還是較為容易實現,只需要約定好相應的規範(可以參考 `Angular` 官網推薦的 [風格指南](https://github.com/johnpapa/Angular-styleguide/blob/master/a1/README.md)),之後則是花費工作量的事情。
**(2)AngularJS 1.4 升級到 1.5** 從以下三方面綜合考慮,有必要將遺留專案中使用的 `AngularJS 1.4` 至少升級到 `AngularJS 1.5` :
**第一:** 升級的代價相對較小。因為畢竟是小版本的升級,雖然存在一定的 `breaking changes`,但根據官方提供的遷移文件,所需更改相對較少,只需要針對性的檢查一番和做少量修改即可。[詳情可見此](https://docs.AngularJS.org/guide/migration#migrating-from-1-4-to-1-5) **第二:** `AngularJS 1.5` 新增的特性將有助於更方便地實現新功能(比如 `component`、單向繫結、新的生命週期等)。 **第三:** `AngularJS 1.5` 新增了[元件API](https://docs.AngularJS.org/api/ng/type/Angular.Module),有助於改造遺留程式碼的風格。不論是在程式碼風格上還是在元件的生命週期上,其都比較像 [Angular 中的等價物](https://Angular.cn/guide/lifecycle-hooks),在此基礎上將程式碼升級到 `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](https://github.com/vsternbach/angular-ts-decorators) 以 `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` 模組載入啟動即可。[詳細可見](https://angular.cn/guide/upgrade#bootstrapping-hybrid-applications)
**(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`。如下圖: ![升級改造的目標](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aea0bfb8b24a4dc08301f7ab6a5b4eb6~tplv-k3u1fbpfcp-watermark.image)
### 5. 遇到的主要難點
**(1)路由及路由元件的升級改造** `Angular ` 官方文件在路由改造這一塊考慮不是很周全或參考性不強,其升級改造方式並不是漸進式的。一般來說,大的遺留專案根本無法一次性將所有的路由元件替換完。因此需要考慮 `AngularJS Router` 和 `Angular Router` 兩種路由的長期共存的可能性,並在改造中逐步用 `Angular Router` 去替換 `AngularJS Router` 。而這方面相關的解決方案,官方的升級文件中並沒有提供,需要自己摸索或搜尋。
**Tips:** 如果考慮在混合應用中只用 `AngularJS Router` 路由,也是可行的。其中一種解決方案是將所有的 `Angular` 路由元件進行降級使用,或者如果 `AngularJS Router` 用的是 `ui-router`, `ui-router` 官方也提供了一套對混合應用進行支援的方案 [angular-hybrid](https://github.com/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](https://ui-router.github.io/guide/ng1/migrate-to-1_0) 以及專案中使用情況,梳理出 `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` 元件庫進行替代,只要保證樣式和基本互動能夠基本一致即可。
### 6. 小結 綜上,主要介紹了遺留專案的基本情況、專案中的限制與應當遵循的升級改造原則、大致的升級改造方向以及遇到的主要難點。後續系列文章準備將進一步討論升級方案中一些步驟具體如何實踐以及踩過的坑。
### 7. 參考 [從 AngularJS 升級到 Angular](https://Angular.cn/guide/upgrade#using-Angular-components-from-AngularJS-code) [AngularJS migrating from 1.4 to 1.5](https://docs.AngularJS.org/guide/migration#migrating-from-1-4-