得物複雜 C 端專案的重構實踐

得物技術 發表於 2022-06-23

1. 背景

1.1 重構

Q:什麼是重構?

重構是在不改變軟體可觀察行為的前提下,改善其內部結構。--《重構 - 改善既有程式碼的設計》

 

Q:為什麼要重構?

重構可以提高理解性和降低修改成本 。--《重構 - 改善既有程式碼的設計》

 

Q:什麼時候重構?

 

(1)何時不應該重構?

沒有價值,沒有意義或者投入產出比很低時。團隊資源是有限的,有限的資源應該儘可能投入到有意義的事情上去。從團隊的角度考慮投入產出比,對於已經只是維護狀態,如無需求、無調整的程式碼,不要去動它,如果對於新手而言,不僅不會帶來好處反而可能挖坑,要知道既有程式碼可能有不少坑。

 

(2)何時應該重構?

  1. 專案維護成本很高
  2. 影響專案調優,如效能優化時
  3. 程式碼長得醜,不優雅時
  4. 既有設計和實現不利於擴充套件新功能時
  5. 重複性工作,既有的程式碼無法幫助你輕鬆新增新特性時
  6. 修補 bug 時,排查邏輯困難
  7. code review 可以讓他人來複審程式碼檢查是否具備可讀性,可理解性
  8. 太多的程式碼無註釋,已然連自己都無法快速理清程式碼邏輯

 

1.2 如何重構

(1)準備(基本功)

推薦值得一讀再讀經典書籍,重構聖經《重構 - 改善既有程式碼的設計》 。本人從畢業第一年開始,幾年下來讀了 4 遍 +,受益匪淺,每次複習都能有所收穫,讓我經常折騰經手的專案卻沒出過問題。

6c4091cb-9a9c-4c5a-bb9c-db4072a2cc44.png
 

(2)重構實踐要點

  • 思考清楚(整體有設計,不一定要文件化但需要想清楚)。
  • 協同規劃(開發團隊內部的配合及重構分支與其他分支的整合、外部資源提前申請如產品、測試、運維等)、整體規劃。
  • 分層分步展開,抓大放小從粗到細。善用 “批處理”。
  • 一次只做一件事。
  • 不要重複造輪子。
  • 當你覺得一件事很難的時候,停下來思考是不是方法用錯了,它應該是怎樣的。保持監控及覆盤自己的思考方式。
  • 做好對內和對外溝通,尤其在當專案不是隻有一個人在開發和維護的情況下。注意提前和相關方(測試、運維)溝通好(方案、主要時間節點、需要投入的資源、需要其配合的事項)。

 

2. 社群 C 端的重構實踐

 

本次重構具有一定的複雜度,除了技術遷移改造的成本外,涉及的幾個倉庫是不同技術選型(框架 & 上層元件等)、專案快速的敏捷迭代、需求高併發及多人協同開發維護狀態。

 

2.1 現狀分析

 

技術棧:

 

倉庫名技術棧社群 C 端頁面數
repo AReact + umi3目標倉庫無需統計
repo BReact + umi35
repo Cvue2 + vuex27

 

專案側

 

三個倉庫 A / B / C 更新活躍,每個倉庫均涉及多業務線的開發,並行維護。分別按照 2 週一個 sprint 的迭代節奏展開,1 周開發 1 周測試,間或穿插著 hotfix。

 

從 V1 主版本釋出後開始重構,各個倉庫涉及的程式碼如下:

 

  1. repo A:A1 + A1. + A2 + A2.
  2. repo B:B1 + B1. + B2 + B2.
  3. repo C:C1 + C1. + C2 + C2.

.* 表示 hotfix

 

2.2 重構計劃

前端側的整體思路:

  • repo A 較新,是社群的主要倉庫,集中了大部分 C 端頁面,作為目標 C 端程式碼的目標倉庫。
  • repo B 到 repo A:repo B 與 目標倉庫的技術棧很接近,涉及 5 個頁面,通過人肉方式遷移,過程中注意依賴的一併遷移。
  • repo C 到 repo A:repo C 與目標倉庫差異較大,且語言異構,上層框架、元件庫等都有較大差異,涉及頁面較多。

    • 首先確定有效的頁面,將已下線頁面的 dead code 排除在遷移範圍之外;具體細節下文會說到,取出待遷移倉庫中的前端路由配置,知道頁面總範圍,檢視阿里雲 sls 日誌中近期的 PV(兩種查詢方式校對),排除無流量的頁面。
    • 分層分級重構,前期抓大放小,耗時耗力還容易出問題的框架語法轉換(vue to react)應採用指令碼工具化實現,實現檔案級和各個類中整體結構及引用關係的維護的轉換。
    • 細節語法通過自定義指令碼批處理(比如 vue 中用的 class 的 key 和字串形式的 value 轉換成 react 中的 className 及變數形式的 value)。
    • 為保證遷移後高效自測需要將對應的 *.vue 檔案保留,將其看成 doc 檔案,待整個遷移完畢再刪除,以提升遷移及測試的效率。注意改造 lint 規則忽視對這類檔案的檢測。
    • 過程中依賴檔案一同遷入,有 “名稱空間隔離”,注意保持整體目錄結構的相對關係,做整體遷移,且不去汙染目標倉庫中的既有檔案,防止同名檔案覆蓋的情況。
  • 通過上述三步將各個倉庫程式碼遷移到 repo A 後,同步 三個倉庫中的最新更新。repo C 到 repo A 的過程中(從 V1 切出的分支),repo C 還在持續更新程式碼,repo A 還需要將 repo C 中的 V1.、V2、V2. 程式碼合入(repo B 亦然)。由於程式碼都在不同的倉庫中,需要手工合併。Tips:可以在 repo C 中將 V1.、V2、V2. 的多個 commits 合成一個 commit,將所有變更項彙總到一處做批量更新。
  • repo A 中 SSR 方案調研和應用也在並行。重構中新遷入的頁面要和 SSR 做整合。

 

2.3 重構與整合實踐

2.3.1 倉庫 B 頁面梳理及遷入

這部分遷移在同構語言中進行,且涉及頁面數不多,主要通過人為遷移。

 

2.3.2 倉庫 C 頁面梳理及遷入

  • 線上流量查詢,排除無用頁面

    • 三個程式碼倉庫中路由申明確定總範圍
    • 根據阿里雲日誌確定過去 3 個月、2 個月、1 個月中的 PV,將無 PV 的頁面從待遷移頁面池中剔除。

      • 注意 1: 阿里雲 SLS 日誌是基於上報的資料,上報和統計過程可能有丟資料的情況,所以綜合兩個查詢入口確定和排查。
      • 注意 2: 對於有 1-2 個 PV 的頁面,可能是團隊內部開發前期做調研時產生的,確定訪問者後排出 “測試” 產生 PV 的頁面。

 

  • 確定最終重構範圍(27 個過濾 13 個)。將步驟 1 中獲取的總範圍中在步驟 2 中無使用者 PV 的頁面剔除。
  • 異構語言轉換和處理

    • 倉庫 C 中 Vue2 轉換為倉庫 A 中的 react

      • 工具轉換
這裡主要用到了 vue-to-react,然而該工具有不少約束和限制,大概成功轉換了一半的程式碼,轉化失敗的情況需要自己寫指令碼實現。原想對該庫的原始碼進行二次封裝和改造,看了其實現發現定製的成本高於自己寫指令碼的成本所以棄了(本人 vue 的經驗一個月不到),時間太緊不容仔細去研究。Tips:避免重複造輪子,當執行很繁瑣且很多重複的動作時,可以考慮擁抱團隊內部的輪子、社群和開源,沒有的話就自己去倒騰一個。

 

  • 指令碼轉換
  • 轉換

    • 專案目錄結構設計及檔案的對映過程
// step1:保持整體目錄結構的相對性不變

.

├── apis

│   ├── community.ts

│   ├── h5community

│   ├── ...

├── components

├── pages

│   ├── h5community

│   │   ├── App

│   │   ├── api

│   │   ├── asset

│   │   ├── components

│   │   ├── config

│   │   ├── filter

│   │   ├── live.js

│   │   ├── main.js

│   │   ├── mixins.js

│   │   ├── router

│   │   ├── style

│   │   ├── utils

│   │   └── views

│   ├── community

├── utils

└── ...




// step2: foo.vue檔案轉為 foo/ 目錄,模板分別對映為jsx及less檔案

.

├── apis

│   ├── community.ts

│   ├── h5community

│   └── ...

├── components

│   ├── h5community

│   └── ...

├── config

│   ├── h5community.js

│   └── ...

├── pages

│   ├── community

│   └── h5community

│       ├── column  // 原 column.vue 轉為目錄,分拆成index.tsx及index.scss

│       │   ├── index.local_js // index.local_js作為註釋保留,用於測試迴歸的參考

│       │   ├── index.scss

│       │   └── index.tsx // 首行自動插入對 index.scss 的引用

│       └── ...

└── utils

    ├── h5community

    └── ...
  • 分步轉換 1: 檔案級

對於 vue-to-react 處理失敗的頁面,通過指令碼生成頁面模版檔案。

 

// 轉換前檔案為 foo.vue


// 轉換後:
.
└── foo
    ├── index.jsx
    ├── index.local_js
    └── index.scss

 

自定義指令碼轉換生成的檔案內容結構如下:

得物複雜C端專案的重構實踐

 

  • 分步轉換 2: 語法級 - html lang

Vue 檔案轉換過程中有很多 lang="pug" 類的模版,通過工具 https://pughtml.com/ 轉換成 “類 jsx” 的模版(但凡雞肋人肉的事,首先應該想到工具,如果找不到,不妨 Google 中嘗試用不同的關鍵詞,而不要去人工)。

 

// 轉換前 foor.vue 中

<template lang="pug">

    article.modal-wrap(@touchmove.stop.prevent @click.stop='close')

        section.modal

            p.more 更多精彩內容, 就在得物App

            p.slogan 有毒的運動 x 潮流 x 好物

            .enter-btn(@click.stop='enter') 進入得物App

            aside.close(@click.stop='close')

</template>







// 轉換後 foo/index.jsx  中

<article class="modal-wrap" @touchmove.stop.prevent="@touchmove.stop.prevent" @click.stop="close">

    <section class="modal">

        <p class="more">更多精彩內容, 就在得物App</p>

        <p class="slogan">有毒的運動 x 潮流 x 好物</p>

        <div class="enter-btn" @click.stop="enter">進入得物App</div>

        <aside class="close" @click.stop="close"></aside>

    </section>

</article>
  • 分步轉換 3: 語法級 - className 等

上面指令碼生成的檔案在於檔案級的轉換,語法差異需要指令碼解決。比如 class 的替換和解析。這裡 html 屬性的規則解析正則比較繁瑣,實現時會思考哪裡會有,很自然就想到了 vue 的原始碼中一定會有該正則(框架是要解析做原生對映的),查了下果不其然,稍作修改就可以了,然後再做些定製(業務程式碼中的模版程式碼,如 import style 這些用指令碼自動生成按需插入)。

// foo.vue 檔案中的寫法 

<div class="var1">demo1</div>

<div class="var1 var2">demo1</div>







// foo/index.jsx (react中)的寫法

import style from './index.scss'

import classNames from 'classnames'

...

<div className={style["var1"]}>demo1</div>

<div className={classNames(style["var1"], style["var2"])}>demo1</div>
  • 逐頁面除錯與校對
  • 倉庫技術選型間的差異問題

    • umi 的路由規則與定製
    • 第三方元件庫
      如 Swiper、postcss-px-to-viewport 等,vue 版與 react 版有些差異,文件不全,擁抱原始碼和社群。其中 postcss-px-to-viewport 在不同倉庫中使用不同的 viewportWidth 設定,轉換過程中通過對不同的外掛例項處理不同的路徑範圍實現
  • 基本功:敏感度(這個跟經驗有關)。庫定位是什麼?成熟度怎麼樣?應該有什麼不應該支援什麼?如果自己來設計大概會怎麼設計(有時候即使文件不全情況下,不看原始碼也可以倒推出很多內容)?可以去哪裡找解決方案?怎麼找到?
  • 遷移 home 頁配置

    • 過程中縮小 home 頁的路徑範圍,隱藏 repo A 中的訪問路徑,僅透出待遷移的路徑,提高查詢效率
  • 遷移過程記錄(測試資料及路徑等,方便交叉測試和 QA 迴歸)
  • 覆蓋度自測。一個頁面中多業務邏輯的情況,後續需要對各路徑進行足夠自測
  • 遷移過程中目錄和檔案結構的設計與變化路徑(重要)

 

2.3.3 整合 repo A、repo B、repo C 重構分支程式碼

  • repo B 中的頁面遷移到 repo A 中,如用 chore-repoB 分支
  • repo C 中的頁面遷移到 repo A 中,如用 chore-repoC 分支
  • 將 repo A master 分支 和 chore-repoB、chore-repoC 合併並解決衝突,合併分支記為 chore-repoA-repoB-repoC,此時該分支僅有 V1 的程式碼,各個倉庫當前版本的迭代功能和及上個版本的 hotfix 還未被合併入該分支。

 

2.3.4 整合 repo A、repo B、repo C 中迭代分支程式碼

主版本日前一天下午各個倉庫中的迭代功能基本穩定,bug 已經收斂。此時可以將該各個倉庫的各個開發本地的分支 feat-foo、feat-bar 等彙總成一個 pre-release-temp 分支(已含有了 master 上的 hotfix),即 pre-release-temp 分支 是 V1.、V2 的彙總,將該分支的 增量 commits 合成一個 commit 獲取 V1.、V2 影響到的檔案變更。人為將這些變更同步到 repo A chore-repoA-repoB-repoC 分支上。

 

2.3.5 整合三個倉庫業務程式碼與 SSR 程式碼

社群 C 端 SSR 改造方案確定後,新啟了一個 A-SSR 倉庫。使用 SSR POC 的框架內容對 A-SSR 倉庫進行初始化,再將 repo A 中 chore-repoA-repoB-repoC 中的程式碼遷移到該倉庫中。遇到的問題:POC 中已對原 repo A 中的部分模組做了 SSR 轉換,遷移新程式碼到該倉庫中注意檔案覆蓋程式碼丟失,用 cp 然後 git diff 及人為 check 多變更源的檔案後再提交。

 

待版本日中再將近 1 天 + 各倉庫產生的 bugfix 同步到 A-SSR 倉庫,確保程式碼無丟失

 

3. 專案推進之外部協同

 

3.1 測試

較大範圍的重構需要保證充分測試,考慮到佔用的測試資源情況,儘可能提前和測試 leader 溝通資源需求。另外,移測前前端內部儘量充分自測。

 

3.2 運維

 

提前計劃好 頁面重定向方案(將最終的跨倉庫 / 應用遷移的頁面重定向),注意運維側變更的影響,一旦做了變更,相關的在對應的測試環境就不可用了(QA 迴歸需要時間,該過程中如果重定向啟用了會影響該環境上相應頁面的使用)。

 

3.3 遇到的問題

在開始規劃及啟動重構時,團隊沒有人對涉及的所有三個 C 端倉庫足夠熟悉。遷移到第二個頁時,發現有頁面是沒有線上流量的 dead code 時,重新溝通客戶端及運維等同學,最終通過查詢阿里雲 sls 日誌縮小遷移範圍,減少了近一半的工作量。過程中遇到的各種技術問題,還是需要平時多做積累。

 

4. 總結

 

複雜專案的重構對研發的基礎、經驗、規範和各方協同有一定要求。開始時可以多讀幾遍《重構》基礎的打好了,逐漸著手程式碼模組、簡單專案、複雜專案、跨團隊複雜專案等的重構,累計經驗。事前做好規劃(技術側整體方案、技術方面的疑難病症提前預估、整體推進計劃、相關方參與等),過程中思考全面足夠細心並持續覆盤調整,過程後做好總結沉澱。

 

事前做好設計、定期 Code Review、過程中和後續持續進行重構可以讓專案程式碼具有更好的可維護性,團隊保持重構的習慣的同時不斷積累重構經驗,能從整體上提升專案的健康度與可維護性。重構看得見改善是關鍵,在重構中成長,在重構中受益,從重構中收益。

 

相關連結:

 

* 文 / SHI FEI

關注得物技術,做最潮技術人!