做爲一家創新驅動的科技公司,袋鼠雲每一年研發投入達數千萬,公司80%員工都是技術人員,袋鼠雲產品家族包括企業級一站式數據中臺PaaS數棧、交互式數據可視化大屏開發平臺Easy[V]等產品也在迅速迭代。在進行產品研發的過程當中,技術小哥哥們能文能武,不斷提高產品性能和體驗的同時,也把這些提高和優化過程記錄下來,現錄入「袋鼠雲研發手記」專欄中,以和業內童鞋們分享交流。css
下爲「袋鼠雲研發手記」專欄第一期,本期做者爲袋鼠雲前端團隊。前端
袋鼠雲前端團隊-知乎專欄@DTUXreact
袋鼠雲UX團隊擁有十多名專家級別,經驗豐富的前端開發工程師,分別支撐公司大數棧產品線的不一樣子項目的開發需求,具體包括數據中臺產品「數棧」與數據可視化產品Easy[V]兩大塊。webpack
在長期的項目實踐與產品迭代過程當中,團隊成員在 React 技術棧、數據可視化技術、前端工程化等細分領域上不斷深耕探索,積累了豐富的經驗與最佳實踐,並分享在知乎專欄@DTUX。web
**第一期
袋鼠雲 EasyManager 的 TypeScript 重構紀要**json
前言redux
在 2018 年 Stack overflow 的開發者調查結果中,開發者們最愛的語言一欄中TypeScript 超越了 JavaScript 位居第四。後端
相較於 JavaScript,開發者們更喜歡 TypeScript 將類型系統帶入了 JavaScript 中,如此一來,開發者可以在運行程序以前發現更多的潛在的問題;得益於 TypeScript 編譯器的良好支持,結合VSCode, 它還能夠提示咱們該如何修復這些問題;將類型系統添加進 JavaScript, 同時容許編輯器給開發者提供更多的便利,好比代碼補全、更方便的進行項目重構以及自動的模塊導入等等。前端工程化
2018 Stack overflow調查結果api
在 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 步:
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函數提升開發效率
使用暴露的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 版本相對於 二、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 接口們,咱們會將它們分開存放便於查找與修改。