
作為一家創新驅動的科技公司,袋鼠雲每年研發投入達數千萬,公司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 步:
- 定義 Reducer 的 model(資料模型)
- 檔案定義了所有 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函數提高開發效率
- 使用:
使用暴露的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 接口們,我們會将它們分開存放便于查找與修改。