1. 背景
1.1 項目背景
公司近兩年快速發(fā)展,社區(qū)線C端代碼分散在不同倉庫中,每個倉庫中采用不同的前端框架和選型,且均含有幾條業(yè)務線的代碼,團隊整體采用敏捷模式快速迭代,導致開發(fā)管理成本較高,升級改造麻煩。比如,所關聯(lián)的三個倉庫中的代碼均引了一個內部基礎組件庫,該組件有非必現(xiàn)bug,導致三個倉庫的不同頁面均出現(xiàn)了不同表現(xiàn)的異常,由具體負責的不同測試分別報到前端開發(fā),分別溝通、排查、解決并走獨立的發(fā)布上線流程,耗時耗力。當同一倉庫中活躍著不同業(yè)務線的開發(fā),一個公共的地方需要修改,開發(fā)沒有溝通清楚導致沖突線上bug。
此外,公司C端體驗分析的統(tǒng)計和報表是應用粒度的,先前代碼耦合了其他業(yè)務的內容,導致我所在業(yè)務線的統(tǒng)計數(shù)據不置信。
近期團隊對C端項目進行重構,將不同倉庫中的代碼匯總到一個倉庫中管理。以期減少管理成本及方便后續(xù)對組內項目做優(yōu)化和升級改造。
1.2 重構經驗
之前我有獨立負責過幾次較大的重構,也曾2周獨立完成近20萬行C端代碼(不含node_modules)從JS到TS遷移在并行業(yè)務需求迭代的情況下實現(xiàn)上線0bug。
1.3 重構基礎
Q:什么是重構?
重構是在不改變軟件可觀察行為的前提下,改善其內部結構。–《重構 – 改善既有代碼的設計》
Q:為什么要重構?
重構可以提高理解性和降低修改成本 。–《重構 – 改善既有代碼的設計》
Q:什么時候重構?
(1)何時不應該重構?
沒有價值,沒有意義或者投入產出比很低時。團隊資源是有限的,有限的資源應該盡可能投入到有意義的事情上去。從團隊的角度考慮投入產出比,對于已經只是維護狀態(tài),如無需求、無調整的代碼,不要去動它,如果對于新手而言,不僅不會帶來好處反而可能挖坑,要知道既有代碼可能有不少坑。
(2)何時應該重構?
- 項目維護成本很高
- 影響項目調優(yōu),如性能優(yōu)化時
- 代碼長得丑,不優(yōu)雅時
- 既有設計和實現(xiàn)不利于擴展新功能時
- 重復性工作,既有的代碼無法幫助你輕松添加新特性時
- 修補bug時,排查邏輯困難
- Code Review 可以讓他人來復審代碼檢查是否具備可讀性,可理解性
- 太多的代碼無注釋,已然連自己都無法快速理清代碼邏輯
1.4 如何重構
(1)準備(基本功)
推薦值得一讀再讀經典書籍,重構圣經《重構 – 改善既有代碼的設計》。本人從畢業(yè)第一年開始,幾年下來讀了4遍 ,受益匪淺,每次復習都能有所收獲,讓我經常折騰經手的項目卻沒出過問題。
(2)重構實踐要點
- 思考清楚(整體有設計,不一定要文檔化但需要想清楚)。
- 協(xié)同規(guī)劃(開發(fā)團隊內部的配合及重構分支與其他分支的集成、外部資源提前申請如產品、測試、運維等)、整體規(guī)劃。
- 分層分步展開,抓大放小從粗到細。善用“批處理”。
- 一次只做一件事。
- 不要重復造輪子。
- 當你覺得一件事很難的時候,停下來思考是不是方法用錯了,它應該是怎樣的。保持監(jiān)控及復盤自己的思考方式。
- 做好對內和對外溝通,尤其在當項目不是只有一個人在開發(fā)和維護的情況下。注意提前和相關方(測試、運維)溝通好(方案、主要時間節(jié)點、需要投入的資源、需要其配合的事項)。
2. 社區(qū)C端的重構實踐
本次重構具有一定的復雜度,除了技術遷移改造的成本外,涉及的幾個倉庫是不同技術選型(框架&上層組件等)、項目快速的敏捷迭代、需求高并發(fā)及多人協(xié)同開發(fā)維護狀態(tài)。
2.1 現(xiàn)狀分析
技術棧:
倉庫名 | 技術棧 | 社區(qū)C端頁面數(shù) |
repo A | React umi3 | 目標倉庫無需統(tǒng)計 |
repo B | react umi3 | 5 |
repo C | vue2 vuex | 27 |
項目側
三個倉庫 A / B / C 更新活躍,每個倉庫均涉及多業(yè)務線的開發(fā),并行維護。分別按照2周一個sprint的迭代節(jié)奏展開,1周開發(fā)1周測試,間或穿插著hotfix。
從 V1主版本發(fā)布后開始重構,各個倉庫涉及的代碼如下:
- repo A:A1 A1.* A2 A2.*
- repo B:B1 B1.* B2 B2.*
- repo C:C1 C1.* C2 C2.*
.*表示hotfix
2.2 重構計劃
前端側的整體思路:
- repo A 較新,是社區(qū)的主要倉庫,集中了大部分C端頁面,作為目標C端代碼的目標倉庫。
- repo B 到 repo A:repo B 與 目標倉庫的技術棧很接近,涉及5個頁面,通過人肉方式遷移,過程中注意依賴的一并遷移。
- repo C 到 repo A:repo C 與目標倉庫差異較大,且語言異構,上層框架、組件庫等都有較大差異,涉及頁面較多。
- 首先確定有效的頁面,將已下線頁面的dead code排除在遷移范圍之外;具體細節(jié)下文會說到,取出待遷移倉庫中的前端路由配置,知道頁面總范圍,查看阿里云sls日志中近期的PV(兩種查詢方式校對),排除無流量的頁面。
- 分層分級重構,前期抓大放小,耗時耗力還容易出問題的框架語法轉換(vue to react)應采用腳本工具化實現(xiàn),實現(xiàn)文件級和各個類中整體結構及引用關系的維護的轉換。
- 細節(jié)語法通過自定義腳本批處理(比如 vue中用的 class的key和字符串形式的value轉換成react中的className及變量形式的value)。
- 為保證遷移后高效自測需要將對應的 *.vue 文件保留,將其看成doc文件,待整個遷移完畢再刪除,以提升遷移及測試的效率。注意改造lint規(guī)則忽視對這類文件的檢測。
- 過程中依賴文件一同遷入,有“名稱空間隔離”,注意保持整體目錄結構的相對關系,做整體遷移,且不去污染目標倉庫中的既有文件,防止同名文件覆蓋的情況。
通過上述三步將各個倉庫代碼遷移到 repo A 后,同步 三個倉庫中的最新更新。repo C 到 repo A 的過程中(從V1 切出的分支),repo C 還在持續(xù)更新代碼,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頁面梳理及遷入
這部分遷移在同構語言中進行,且涉及頁面數(shù)不多,主要通過人為遷移。
2.3.2 倉庫C頁面梳理及遷入
- 線上流量查詢,排除無用頁面
- 三個代碼倉庫中路由申明確定總范圍
- 根據阿里云日志確定過去3個月、2個月、1個月中的PV,將無PV的頁面從待遷移頁面池中剔除。
- 注意1: 阿里云SLS日志是基于上報的數(shù)據,上報和統(tǒng)計過程可能有丟數(shù)據的情況,所以綜合兩個查詢入口確定和排查。
- 注意2: 對于有1-2個PV的頁面,可能是團隊內部開發(fā)前期做調研時產生的,確定訪問者后排出“測試”產生PV的頁面。
- 確定最終重構范圍(27個過濾13個)。將步驟1中獲取的總范圍中在步驟2中無用戶PV的頁面剔除。
- 異構語言轉換和處理
- 工具轉換
- 倉庫C中Vue2 轉換為倉庫A中的react
這里主要用到了 vue-to-react,然而該工具有不少約束和限制,大概成功轉換了一半的代碼,轉化失敗的情況需要自己寫腳本實現(xiàn)。原想對該庫的源碼進行二次封裝和改造,看了其實現(xiàn)發(fā)現(xiàn)定制的成本高于自己寫腳本的成本所以棄了(本人vue的經驗一個月不到),時間太緊不容仔細去研究。Tips:避免重復造輪子,當執(zhí)行很繁瑣且很多重復的動作時,可以考慮擁抱團隊內部的輪子、社區(qū)和開源,沒有的話就自己去倒騰一個。
- 腳本轉換
- 轉換
- 項目目錄結構設計及文件的映射過程
// 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及l(fā)ess文件.├── 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
自定義腳本轉換生成的文件內容結構如下:
- 分步轉換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 屬性的規(guī)則解析正則比較繁瑣,實現(xiàn)時會思考哪里會有,很自然就想到了vue的源碼中一定會有該正則(框架是要解析做原生映射的),查了下果不其然,稍作修改就可以了,然后再做些定制(業(yè)務代碼中的模版代碼,如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的路由規(guī)則與定制
- 第三方組件庫
- 如Swiper、postcss-px-to-viewport等,vue版與react版有些差異,文檔不全,擁抱源碼和社區(qū)。其中postcss-px-to-viewport在不同倉庫中使用不同的viewportWidth設置,轉換過程中通過對不同的插件實例處理不同的路徑范圍實現(xiàn)
- 基本功:敏感度(這個跟經驗有關)。庫定位是什么?成熟度怎么樣?應該有什么不應該支持什么?如果自己來設計大概會怎么設計(有時候即使文檔不全情況下,不看源碼也可以倒推出很多內容)?可以去哪里找解決方案?怎么找到?
- 遷移home頁配置
- 過程中縮小home頁的路徑范圍,隱藏repo A中的訪問路徑,僅透出待遷移的路徑,提高查找效率
- 遷移過程記錄(測試數(shù)據及路徑等,方便交叉測試和QA回歸)
- 覆蓋度自測。一個頁面中多業(yè)務邏輯的情況,后續(xù)需要對各路徑進行足夠自測
- 遷移過程中目錄和文件結構的設計與變化路徑(重要)
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中迭代分支代碼
主版本日前一天下午各個倉庫中的迭代功能基本穩(wěn)定,bug已經收斂。此時可以將該各個倉庫的各個開發(fā)本地的分支 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 集成三個倉庫業(yè)務代碼與SSR代碼
社區(qū)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. 項目推進之外部協(xié)同
3.1 測試
較大范圍的重構需要保證充分測試,考慮到占用的測試資源情況,盡可能提前和測試leader溝通資源需求。另外,移測前前端內部盡量充分自測。
3.2 運維
提前計劃好 頁面重定向方案(將最終的跨倉庫/應用遷移的頁面重定向),注意運維側變更的影響,一旦做了變更,相關的在對應的測試環(huán)境就不可用了(QA回歸需要時間,該過程中如果重定向啟用了會影響該環(huán)境上相應頁面的使用)。
3.3 遇到的問題
在開始規(guī)劃及啟動重構時,團隊沒有人對涉及的所有三個C端倉庫足夠熟悉。遷移到第二個頁時,發(fā)現(xiàn)有頁面是沒有線上流量的 dead code時,重新溝通客戶端及運維等同學,最終通過查詢阿里云sls日志縮小遷移范圍,減少了近一半的工作量。過程中遇到的各種技術問題,還是需要平時多做積累。
4. 總結
復雜項目的重構對研發(fā)的基礎、經驗、規(guī)范和各方協(xié)同有一定要求。開始時可以多讀幾遍《重構》基礎的打好了,逐漸著手代碼模塊、簡單項目、復雜項目、跨團隊復雜項目等的重構,累計經驗。事前做好規(guī)劃(技術側整體方案、技術方面的疑難病癥提前預估、整體推進計劃、相關方參與等),過程中思考全面足夠細心并持續(xù)復盤調整,過程后做好總結沉淀。
事前做好設計、定期Code Review、過程中和后續(xù)持續(xù)進行重構可以讓項目代碼具有更好的可維護性,團隊保持重構的習慣的同時不斷積累重構經驗,能從整體上提升項目的健康度與可維護性。重構看得見改善是關鍵,在重構中成長,在重構中受益,從重構中收益。
相關鏈接:
- https://pughtml.com/
*文/石菲
關注得物技術公眾號~每周一三五晚18:30更新技術干貨
要是覺得文章對你有幫助的話,歡迎評論轉發(fā)點贊~
版權聲明:本文內容由互聯(lián)網用戶自發(fā)貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權/違法違規(guī)的內容, 請發(fā)送郵件至 舉報,一經查實,本站將立刻刪除。