袋鼠雲研發手記 | 袋鼠雲EasyManager的TypeScript重構紀要

圖片描述
作爲一家創新驅動的科技公司,袋鼠雲每年研發投入達數千萬,公司80%員工都是技術人員,袋鼠雲產品家族包括企業級一站式數據中臺PaaS數棧、交互式數據可視化大屏開發平臺Easy[V]等產品也在迅速迭代。在進行產品研發的過程中,技術小哥哥們能文能武,不斷提升產品性能和體驗的同時,也把這些提升和優化過程記錄下來,現錄入“袋鼠雲研發手記”專欄中,以和業內童鞋們分享交流。

下爲“袋鼠雲研發手記”專欄第一期,本期作者爲袋鼠雲前端團隊。

袋鼠雲前端團隊-知乎專欄@DTUX

袋鼠雲UX團隊擁有十多名專家級別,經驗豐富的前端開發工程師,分別支撐公司大數棧產品線的不同子項目的開發需求,具體包括數據中臺產品「數棧」與數據可視化產品Easy[V]兩大塊。

在長期的項目實踐與產品迭代過程中,團隊成員在 React 技術棧、數據可視化技術、前端工程化等細分領域上不斷深耕探索,積累了豐富的經驗與最佳實踐,並分享在知乎專欄@DTUX。

**第一期
袋鼠雲 EasyManager 的 TypeScript 重構紀要**

前言

在 2018 年 Stack overflow 的開發者調查結果中,開發者們最愛的語言一欄中TypeScript 超越了 JavaScript 位居第四。

相較於 JavaScript,開發者們更喜歡 TypeScript 將類型系統帶入了 JavaScript 中,如此一來,開發者能夠在運行程序之前發現更多的潛在的問題;得益於 TypeScript 編譯器的良好支持,結合VSCode, 它還可以提示我們該如何修復這些問題;將類型系統添加進 JavaScript, 同時允許編輯器給開發者提供更多的便利,比如代碼補全、更方便的進行項目重構以及自動的模塊導入等等。

圖片描述
2018 Stack overflow調查結果

在 TypeScript 官網中列出了很多已經使用 TypeScript 的機構,同樣的還有著名測試框架 Jest 也表示嘗試將 Jest 遷移到 TypeScript。

圖片描述
TypeScript官網展示的已使用TypeScript機構

TypeScript 的發展勢頭可見一斑。

Easy Manager

Easy Manager是一款產品安裝部署工具,是數棧大數據平臺的運維管家,使用Easy Manager可實現數棧產品的安裝部署。

在產品安裝部署完成後,Easy Manager支持完成大數據平臺全方位的運維,包含產品升級、擴縮節點、版本回滾、產品卸載、集羣監控、實時告警等功能,致力於幫助客戶最大化地節省運維成本,降低線上故障率與運維難度,提供安全穩定的產品部署與監控。

Easy Manager 目前已支持百至千個節點的集羣部署,可進行大集羣的部署、監控及日常運維。同時,除支持數棧大數據平臺部署外,也可支持第三方產品的安裝部署。

關於此次重構

本次 EasyManager 的重構主要目標是使用 TypeScript 替代 JavaScript,加上類型約束提高團隊協作效率,同時剔除已經不需要的冗餘代碼。

在本次 EasyManager (下文簡寫成:EM)的重構之前,已經進行了一部分的重構工作,之前是基於EM2.3 版本進行重構的,但是發現 EM2.4 版本改動較大,需要在之前重構基礎上繼續進行重構工作。計劃是一邊開發  EM2.5版本,一邊進行 EM2.4 版本的重構工作,在EM2.5基礎上,最後進行  EM2.6 版本的開發同時進行遺漏補缺。

對於已經部署使用了EM2.5 版本的客戶和未來即將部署使用EM2.6 版本的客戶情況,我們保留了 js_master 和 ts_master 兩個分支。前者主要進行EM2.5 版本的 bug 修復爲客戶做支持,後者則是主版本進行版本迭代開發。

整個過程還是比較順利的,沒有遇到阻塞工作流程的情況,下面詳細來爲大家分析一下本次重構詳細情況。

js VS ts 結構區別與利弊
js 版本的目錄結構並不屬於那種一眼就能看懂的,其類似數棧將每個頁面所需的 action、reducer、assets 以及模塊需要的其他資源都放在了一個以模塊名稱命名的文件夾裏,我的理解是這裏是以“模塊”爲思想進行的開發結構搭建,比較方便開發,對人協同開發對 changelog 亦能一目瞭然,管理 redux 也很容易定位。

如下圖:

圖片描述
舊版本基本目錄結構

ts 版本在這裏做了更改,不再是以“模塊”爲單位進行目錄區分,而是以文件“功能”爲單位進行搭建,具體表現是:js 版本的 service 模塊文件夾下有 actions、reducers、pages、assets 文件夾以及 action types 文件 constants.js 和頁面所需的 json 數據文件。而在 ts 版本,actions 現在以單個文件的形式存在於 src 目錄下的 actions 文件夾下面,同理,reducers 則已單個文件形式存在於 stores 文件夾下,action types 則與路由文件 routers 和 api 接口定義 api.js 文件同在 constants 文件夾下;這樣一來,ts 版本里 pages 目錄下各個頁面的文件夾下只有一個 index.tsx 入口文件、style.scss 樣式文件和**.tsx 的頁面主文件,可以說是很精簡的目錄組成。

如下圖:

圖片描述
新版本的目錄結構

兩個結構各有利弊,ts 版本的結構比較經典,開發者花很短的時間就能讀懂搭建者的心思與意圖,對新手參與開發很友好,但是對於“按功能區分”文件結構的方式來講,開發一個模塊需要遊離在 actions、pages 和 stores 文件夾裏,在模塊多了之後文件就會變得難以定位和區分。js 版本的目錄結構相較於 ts 版本的更深一些,主要是將頁面所有資源都存放在頁面自己目錄下,這種結構很大程度上方便了多人協作開發,也方便定位頁面所需的功能文件,唯一不足也許就是對新接觸的開發者來說要花點時間熟悉。

重構思路
ts 可以作爲類型檢查和編譯工具使用,對於EasyManager來講主要作用在於類型檢查,因此本次重構使用 webpack、babel 等插件進行項目編譯。本次重構以模塊爲單位進行重構,重構順序依次是 host、product、dashboard、service。

本次重構原則是儘量不改動業務邏輯代碼,對已有的重構基礎進行包容疊加新特性,儘量避免改動非常基礎的東西,比如目錄結構等。重構工作是開着 JavaScript 版本的EasyManager 和 TypeScript 版本的EasyManager, 比較模塊功能異同進行同步,同時方便梳理EasyManager功能(得益於目前EasyManager 功能不是很複雜), 在 2.6 版本測試期間發現很多功能與 2.5 版本的不同,甚至還是 2.3 版本,因此建議在項目重構工作開始前根據PRD 和產品進行一次完整的功能梳理避免遺漏。

重構內容
重構內容主要分爲代碼層面重構和結構層面重構:

代碼層面重構主要工作是提取頁面 props 和 state 創建 interface 再通過 React.Component 傳給組件,然後需要的把一些參數加上類型就可以。需要注意的是,頁面/組件需要的 prop 必須現在 interface 中聲明出來,一個組件可能包含 redux 的 state 和其父組件傳入的 props,此時要將 redux 的 state 設置成可選,這樣父組件纔可以在不指定其值的情況下使用。

代碼層面重構改變的東西並不多,樣式文件直接拷貝內容就可以。

結構重構主要是將文章開始講的那種以模塊爲單位進行劃分的目錄結構改成以 ts 文件類型/功能進行劃分。

TypeScript 版本的 EM 將各個 action 和各個 reducer 集中在了一個文件夾下,因此每個模塊都需要將其相應的 action 和 reducer 新建到 actions 和 stores 文件夾內。

頁面內容集中在 pages 文件夾下,其中的一個模塊的具體目錄長這樣:

圖片描述
重構之後的Dashboard模塊

style.scss 即是這個模塊的樣式文件,index.tsx 文件是此模塊的入口,dashboard.tsx 則是此模塊的邏輯文件,detail 則是此模塊的子頁面,也包括一個頁面文件和入口文件,其他文件則是頁面所需的數據、組件等文件。之前的模塊構成是這樣的:

圖片描述
重構之前的Dashboard模塊

跟上面的比起來多了 redux 使用的 actions 和 reducer 文件,TypeScript 版本的已經將 action 和 reducer 提取到 src 目錄下的 actions 文件夾和 stores 文件夾。

重構步驟
重構內容在項目跑起來之後就主要包括四個步驟:

Step 1 遷移業務邏輯代碼

遷移業務邏輯代碼是整個過程中最爲繁瑣的。

遷移步驟大概可分爲:頁面主文件代碼遷移、service 層遷移,redux 層遷移。

01 頁面主文件代碼遷移

將頁面代碼 copy 到新文件裏即可,得益於 vscode 對 TypeScript 的良好支持,頁面中的錯誤都會有提示,如下圖:

圖片描述
VsCode智能提示

我們對這種錯誤進行處理就可以了,常見的問題有總結,詳情見重構注意事項。

02 service 層遷移

service 層遷移分爲兩步:補充 api、補充 service

這層遷移還是很方便的,copy 頁面代碼之後,如果有頁面直接調用 service 請求但是當前卻沒有這個方法的話編譯器會報錯,我們在頁面文件里根據這個錯誤逐一添加即可。

03 redux 層遷移

redux 層遷移分爲 6 步:

  1. 定義 Reducer 的 model(數據模型)

modal 文件定義了所有 reducer 使用的數據結構模型,在具體的 reducer 中我們使用此模型約束 redux 數據結構: 暴露Store接口。

圖片描述
暴露Store接口

2. 遷移 reducer 文件
遷移 reducer 直接把原來的代碼 copy 即可,需要注意的是在這裏初始化 redux 數據的時候,需要指定此 state 的數據模型:

圖片描述
reducer裏使用約束

同時還需要暴露此數據模型供頁面使用,避免頁面開發出現 undefined 之類的錯誤,同時得益於 vscode 對 TypeScript 的良好支持能夠一定程度的提高我們的開發效率(在當前版本的 em 裏面沒有用到這個導出,目前用到的是下面的 index 文件統一包裝導出的 modal 接口)。

3. 補充 reducer 的 index 文件
reducer 經過 index 的統一導出給所有頁面提供了所有 modal 的數據結構接口:

圖片描述
暴露所有store接口供使用

在頁面中使用 AppStoreTypes 即可:

圖片描述
在pages裏使用暴露的store接口進行約束

4. 補充 actionType 定義

補充 actionType 即把 2.5 版本的 action 使用的標識補充到新版本的 actionTypes 定義文件即可。

5. 遷移 action 文件
遷移 action 在把之前版本的 action 遷移過來之後另外需要導出此 action 的函數列表接口,這跟 reducer 導出 modal 是一樣的意義,開發頁面時可以直接進行使用:

圖片描述
暴露action函數提高開發效率

  1. 使用:

圖片描述
使用暴露的action函數接口

這樣我們在使用 actions 的時候瀏覽器就能夠智能提示了,同時對函數的參數也會嚴格限制。

Step 2 定義路由

本次重構過程因將 React Router V3 版本升級到了 V4 版本,因此路由部分改動蠻大。關於 React Router V3 遷移 V4 的詳細內容大家可以在網上找到很多相關文章,本文在“重構障礙”中會詳細聊聊。

定義路由主要工作就是將頁面地址綁定到模塊,因爲使用了路由由父組件進行定義,所以總的路由文件很精簡:

圖片描述
根路由文件

Step 3 遷移 CSS 內容

遷移 CSS 內容是整個過程中最令人舒適的部分啦!直接將已有的 CSS 文件 copy 到新目錄下然後引用即可。

需要注意的是本次EasyManager重構加入了對 antd 組件微調的效果,我們除了全局樣式 base.scss 之外還有一個用於 antd 組件的樣式文件 dtemStyle.scss 文件:

圖片描述
用於全局規定antd組件微調的樣式文件

在開發過程中我們會注意組件樣式是否全局統一,設計是否有意差異化等問題,儘量統一對 antd 組件的微調標準。

Step 4 測試

每個模塊基本都是遵循此步驟進行重構。期間,複雜的地方主要在第一步和第二步,遷移業務邏輯代碼將需要遷移頁面 jsx 代碼、頁面 redux 的 action 以及 reducer 代碼。第二步的複雜體現在涉及到嵌套路由的應用上,這塊我們在“重構障礙”裏詳細解釋。

重構障礙
React Router V4 的嵌套路由

React Router V4 版本相對於 2、3 版本改動較大,思想上也更向React 思想靠近。現在 React Router 取消了在一處管理所有頁面路由的方式,取而代之的是在各個容器裏進行管理。Router 像一個字組件一樣放在了父容器裏。

下面放一個官方 Demo 截圖:

圖片描述
react-router4官方示例

本次重構過程中遇到了嵌入頁面無法正常顯示問題,在發現不可以像 route v3 那樣在一個文件裏統一進行配置路由之後,我修改了每個模塊的入口文件 index.js,把其改成了 react-router 4 風格的路由定義文件,如下圖:其中 props.match.path 指向父組件導航過來的當前路由,默認渲染第一個組件,比如從 A 頁面點擊/tob,那麼/tob 就是渲染的 AlertChannel 組件,/tob/addAlertChannel 則渲染 AddAertChannelPanel 組件。(路由從配置文件變成了組件)

browserRouter 造成子組件頁面刷新白屏問題,在 HTML 里加入根目錄即可:

圖片描述
在HTML中解決,也可通過webpack解決

react refs 問題

主要情景是用戶搜索特定 menu 之後我們需要將選中的那個 menu 滾動到可視區域,使用 react 的 refs 的話會這樣:

圖片描述
vs提示不存在屬性

提示 Element 上沒有我們需要的屬性,當然了我們可以直接使用(this.refs.container as any).current 來規避 TypeScript 的錯誤提醒:

圖片描述
萬物皆any解決

這樣雖好,但是卻違反了我們使用 TypeScript 的初衷:類型約束。如果開發者使用這個方法隨便寫了一個不存在的特性,那麼又出現了常見的 undefined 錯誤。因此這裏建議使用私有變量設定值爲 react 創建制定 DOM 類型的 Ref:

圖片描述
創建響應類型的REF

在 dom 上綁定這個 ref:

圖片描述
綁定至dom

然後直接使用即可:

圖片描述
正常使用

重構常見處理
導入 react 因爲 react 沒有默認導出報錯:

圖片描述

改成:

圖片描述

antd 的 Form.create 傳入了組件 form 屬性,但是 ts 不知道:

圖片描述

此時導入 antd 的 formProps 合併到組件即可:

圖片描述

圖片描述

使用 router 傳入的 location 等等 props 需要在組件 prop 數據模型接口裏聲明,否則 TypeScript 也會報錯的:

圖片描述

重構總結與下一步

關於 MVC

相較於 Vue 的 MVVM 思想,放佛 MVC 更能讓人找到實踐的入口點。結合 TypeScript,我們進一步的向 MVC 思想靠攏:

圖片描述
簡化版MVC流程圖

我們將類型約束加入到了 pages 使用 stores 的過程中,當然還包括 page 中自己的 state 或者父子組件的 prop 等也加入了類型約束。

按照 MVC 的思想,我們所有的操作都要由 actions 去完成,筆者的想法是不需要放到 redux 中的就不需要放,state 已經足夠好用,如果僅僅是爲了遵循 MVC 思想而把沒有必要放在全局數據的變量放到全局裏那對後來者理解項目代碼和模塊化開發來講將是難以接受的。

因此筆者的建議是能不放 redux 的儘量不放,提高項目的模塊化程度。

重構不足
本次重構過於倉促,重構前的準備工作也沒有做的很充分。本次最大的不足有以下幾點:

使用了過多的 any。包括但不侷限於:服務器返回的數據格式,service 層請求入參格式以及各個函數的出參格式。

model 體現過於薄弱。在 reducer 中或者是頁面 state 中都缺少了集中統一的 model 定義。

下一步

EM2.6 版本僅僅是完成了 JavaScript 到 TypeScript 的遷移,我們接下來的工作就是要充分發揮 TypeScript 的優勢,下一步我們會將 service 層的入參進行接口定義,然後對後端返回的數據進行接口定義。然後剝離現在在一起的 reducer 的 store 接口們,我們會將它們分開存放便於查找與修改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章