來源: 趣談前端公衆号
作者: 徐小夕
hi, 大家好, 我是徐小夕, 今天和大家分享一下前端項目重構的一些思考和複盤, 同時也是對自己多年項目研發經驗的一個總結.
一. 背景介紹
1. 我們為什麼要做項目重構
項目重構是每一家穩定發展的互聯企業的必經之路, 就像一個産品的誕生, 會經曆産品試錯和産品疊代 一樣, 随着業務或新技術的不斷發展, 已有架構已無法滿足更多業務擴充的需求, 是以隻有通過重構來讓産品“進化”, 才能跟上飛速發展的時代浪潮.
這裡我結合自己的實際經驗總結一下項目重構的幾個原因:
1. 技術因素
技術因素主要有如下幾個方面:
- 早期技術團隊在技術選型上的誤判(常發生于MVP類型的産品快速上線導緻的技術調研不夠充分)
- 新老技術架構的更替(比如從 jquery 遷移到 vue/react)
- 技術團隊交接出現的斷層(老技術團隊的架構設計更不上新技術團隊的發展, 出現架構上的“平替”)
- 技術架構更新(比如随着業務發展, 由傳統的MPA應用轉為基于微前端模式的SPA應用)
- 安全,性能,代碼品質等原因導緻的技術重構
2. 産品因素
- 産品形态調整(比如由純PC應用轉為響應式應用, 或者從H5到支援跨端)
- 産品業務調整(非常常見的重構理由之一)
- 産品名額調整(如相容性, 性能名額等導緻的代碼重構)
上面是我列出來的比較典型的重構場景, 也是我們未來在設計産品技術架構之前需要考慮的方面. 為了提高自己設計的架構穩定性, 我們需要提前和産品溝通明确, 以降低後期重構和維護成本.
最後總結幾條架構設計的經驗:
- 能做規範的一定要嚴格做好規範
- 在設計架構之前, 一定要充分了解業務場景, 明确産品的技術傳遞名額
- 架構設計以可溯源 為基本要求
- 不要盲目追求最好的方案, 以局部最優解為工程設計理念
2. 做項目重構之前, 需要有哪些準備
當然做項目重構也是有技術門檻的,不是所有程式員都能做好重構工作, 建議大家具備如下幾種技術能力:
- 對項目所使用的架構語言有相對深入的了解和掌握
- 有一定的前端工程化經驗(如webpack, vite, gulp, nodejs, babel, AST等有一定的研究)
- 熟悉常用的web性能優化方案
- 熟悉常見的設計模式和前端編碼規範
- 熟悉前端主流的技術架構的設計原理和工程設計思想
接下來我們一起看看常見的幾種項目重構場景及其重構方向.
二. 不同類型項目重構的方法論
1. 業務系統自身的重構
業務系統自身的重構一般可以包含如下幾個方面:
- 業務代碼優化
業務代碼優化主要是針對一些核心業務代碼, 進行流程上, 邏輯上的重構, 讓它更具可讀性和維護性, 同時保證業務操作的相容性, 具體方案如下:
- 複雜業務邏輯需要編寫注釋
- 代碼中通路性屬性提供相容邏輯(常見的比如通路對象屬性, a.b.c, 如果a,b為非對象則整段代碼将報錯)
- 代碼結構優化(比如冗長的if else 或者“回調地獄” 可以采用擴充卡模式或者es6+文法來優化)
- 方法參數調優(一個函數有多個參數, 可以使用參數對象來提高可讀性,降低使用偏差)
- 業務代碼性能優化(複雜背景系統比如低代碼類産品, 前端需要處理很多資料和邏輯, 此時可以用合适的資料結構和算法優化js計算)
- 函數式程式設計思想優化業務函數(可選)
- 業務代碼進行單元測試, 提高代碼品質(可選)
- 代碼規範
早期可能由某名研發單獨負責的項目, 對代碼規範和格式要求不是很高, 但是需要考慮後期團隊擴容帶來的協作開發問題, 這個時候如果沒有統一的規範, 不同研發小夥伴可能寫出的代碼千奇百怪, 導緻後期維護成本巨大, 尤其是涉及到需要維護他人代碼時. 是以我們重構的另一個目标就是降低代碼了解成本, 保證項目代碼在閱讀時就像同一個寫出來的, 這樣對後期邏輯複用, 元件解耦, 問題定位以及業務代碼維護将非常有幫助.
常用的措施有:
- 代碼格式規範(如eslint)
- 邏輯文法類型限制(如typescript)
- 代碼規範(如css命名規範OOCSS, BEM等, 檔案命名規範, js變量命名複規範等)
- git 送出規範(常見的是在git hooks的送出階段對送出格式等進行校驗)
當然, 這些都是需要結合自身團隊和項目來定的, 這裡隻做參考.
- 工程化優化
工程化優化主要有以下幾個場景:
- 由于業務不斷增加, 系統的複雜性加大導緻的本地運作和打包速度越來越慢
- 由于項目代碼量的增加導緻頁面臃腫, 需要進行合理的拆分
- 基于已有的工程經驗沉澱, 需要對工程化配置做進一步更新, 優化
- 老舊腳手架無法适應目前的項目生産效率
接下來我會針對以上場景, 進行一些解決方案的分享.
- 由于業務不斷增加, 系統的複雜性加大導緻的本地運作和打包速度越來越慢
針對這種情況, 我們可以借助 speed-measure-webpack-plugin 插件,它可以分析 webpack 的總打包耗時以及每個 plugin 和 loader 的打包耗時,進而讓我們對打包時間較長的部分進行針對性優化。
同時預設情況react, react-dom, react-router 等公共子產品在每次建構都會參與打包, 這些實際上是沒有必要的, 我們可以将其傳到 cdn上, 進而減少webpack 的打包”工作量“.
我們可以安裝 html-webpack-externals-plugin 來實作将指定子產品從打包清單中排除, 具體用法如下:
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
module.exports = {
// ...其他配置代碼
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://cdn.dooring.cn/umd/react.production.min.js',
global: 'React',
},
{
module: 'react-dom',
entry:
'https://cdn.dooring.cn/umd/react-dom.production.min.js',
global: 'ReactDOM',
},
],
}),
],
};
為了追求更驚一步的打包效率, 我們可以使用并行的方式建構, 同樣 webpack 生态也提供了對應的子產品 parallel-webpack. 具體用法大家可以看文檔, 非常簡單友善.
其他還有很多優化的方案, 這裡我列一下, 大家可以根據實際情況使用:
- 配置并行壓縮(terser-webpack-plugin, css-minimizer-webpack-plugin, html-minimizer-webpack-plugin 等都支援parallel參數)
- 預編譯資源子產品(可以利用webpack.DllPlugin來提前将公共子產品打包以便後續直接複用)
- 使用建構緩存(webpack5 内置的 cache 子產品, 或者cache-loader)
- 對打包體積進行分析, 以便有針對性的優化(如webpack-bundle-analyzer)
當然除了對已有建構工具的優化, 我們可以評估一下重構成本, 将建構核心替換vite等更高效的建構工具.
- 由于項目代碼量的增加導緻頁面臃腫, 需要進行合理的拆分
針對項目代碼量的增加導緻頁面臃腫, 我們可以從項目本身的角度, 對項目進行拆解, 将公共子產品抽離為公用業務類庫或者元件庫:
除了對項目進行可複用性拆分之外, 我們還需要根據系統複雜量級, 近一步拆分項目, 比如将一個巨石工程拆分為多個子工程, 單獨運作維護, 或者采用之前熱點讨論的微前端的模式, 比如使用 qainkun, single-spa, Micro App, EMP, Garfish, Bit 這些優秀的微前端架構.
綜上, 我們可以根據項目複雜度, 做如下優化:
- 子產品 & 元件化
- 拆分子系統(已有架構不變的MPA模式)
- 拆分子系統(微前端)
當然我們始終需要保持一個理念: 局部最優, 誤增繁複.
- 基于已有的工程經驗沉澱, 需要對工程化配置做進一步更新, 優化
這種情況主要是在項目發展穩定之後, 需要思考的重構方向, 比如早期由于業務場景單一, 很多公共配置都寫在業務代碼裡的, 随着業務複雜之後, 很多子產品都需要使用改配置或者變量, 比如:
// a.js
const publicDomain = 'https://dooring.vip';
const serverUrl = 'https://xxx.cn';
// b.js
const publicDomain = 'https://dooring.vip';
// c.js
const appid = 'xxxxxxxx';
const website_Logo = 'http://h5.dooring.cn/logo.png';
對于這種零散且固定的變量, 未來可能會被多個頁面或者子產品複用, 是以為了降低成本, 我們可以把這些通用配置提取到外層, 作為公共配置檔案, 這樣後期新項目也能享受開箱即用的配置體驗.
拿我的親身經驗, 比如幾年前我開發的低代碼項目H5-Dooring, 有一些零散的配置資訊分散在項目的各個角落, 後面經過幾次重構優化之後, 整個項目隻需要在配置檔案中輕松配置内容, 即可一鍵控制頁面的走向, 這裡分享一下優化過後的配置檔案:
// h5-dooring全局配置檔案
define: {
START_ENV,
lang,
// 配置h5端通路的域名
h5Domain: 'h5.dooring.cn',
// 設定目前版本号
curVersion: dooringVersion,
// 備案資訊
copyright: 'xxxxx-3',
// 是否顯示更新彈窗
showUpdateModal: true,
// 更新日志
updateList: [
"1. 新增表格元件",
"2. 國際化優化",
"3. 表單詳情頁支援内部滾動",
"4. 個人圖檔庫性能優化",
"5. 下載下傳代碼功能優化"
],
// 網站logo位址
logo: 'http://cdn.dooring.cn/dr/logo.ff7fc6bb.png',
// 入口頁面是否展示贊助品牌和版權提示
showAdsAndTip: true,
// 登入時擷取登入碼的二維碼
qrcode: 'http://cdn.dooring.cn/dr%2Fcode1.png',
// 友情連結展示
friendLinks: [
{
name: 'V6',
link: 'http://v6.dooring.cn/beta',
title: '可視化大屏編輯器'
},
{
name: 'Power',
link: '/powernice',
title: '文檔編輯器'
}
],
// 預設語言
defaultLocale: 'zh-CN',
langMap: langMap
},
這樣, 我們的工程化結果就可以讓不同的技術小夥伴輕松的享受, 讓項目建立的成本和自由度得到極大的提升.
- 老舊腳手架無法适應目前的項目生産效率
對于這種場景, 我們就需要對腳手架自身有更多的研究和了解, 比如熟悉webpack設計思想, 熟悉babel的工作流程, 熟悉 nodejs 開發工具鍊的一些模式等, 這裡分享幾個比較成熟的先進腳手架, 大家如果覺得老項目工程比較老舊, 可以往這幾個方向重構:
- 基于webpack5.0的項目腳手架
- vite
- umi4.0
如果大家對以上三種之一比較熟悉, 也可以基于他們二次封裝成符合自身業務場景的DIY項目工具.
- 渲染層優化
渲染層優化主要表現在産品的體驗上, 比如:
- 提高首屏加載速度
- 白屏體驗優化
- 大資料清單渲染優化
- api請求優化
- 動畫性能優化
- dom過載導緻的頁面卡頓優化
以上是我之前遇到的一些渲染優化的次元, 接下來和大家一一介紹解決思路.
1. 提高首屏加載速度
有很多方法可以幫助我們提高首屏加載速度, 比如:
- 靜态資源放cdn, 提高不同地域使用者通路資源的速度
- 頁面或路由懶加載, 降低首次加載單一頁面的代碼體積
- 靜态資源(如圖檔, 視訊等)懶加載
- 資源壓縮(比如開啟gzip模式)
當然還存在很多客觀因素, 比如使用者所在區域為弱網環境, 我們可以根據網速提供一個最小化弱網可替代頁面, 來保證我們網站的可用性和可通路性.
2. 白屏體驗優化
對于白屏優化, 也有很多成熟的例子, 比如采用骨架屏:
如果我們的項目是基于 vue-cli 建構的,那麼可以使用比較成熟的的 page-skeleton-webpack-plugin 方案,否則我們仍然可以選擇 vue-router 提供的 vue-server-renderer.
當然你的項目是使用react的, 也可以輕松使用如 react-content-loader 這樣的svg方案來定制自己的骨架屏.
除了骨架屏之外, 我們還可以提供一個模版頁面或者加載動畫, 以便在頁面加載完成之前給使用者一個優雅的過渡提示. 比如:
3. 大資料清單渲染優化
對于一些中背景複雜的系統子產品, 可能會涉及到一次渲染大量清單項或者多級組織樹的情況:
尤其是在大公司或者大集團中出現的頻率非常高, 這種情況我們就需要用到虛拟清單或者節點懶加載的方式了. 虛拟清單應用非常廣泛, 目前也有幾個成熟的方案大家可以直接使用:
- vue virtual scroll list
- react-virtualized
- rc-virtual-list
如果你的項目目前還好沒有使用這種方案, 不妨評估一下, 是否可以用這些方案為自己項目保駕護航.
4. api請求優化
api 請求優化主要正對這種場景: 頁面的渲染依賴于某個或者某些請求的完成, 或者由于某個頁面請求量過大導緻每次重新進入頁面都需要造成一定的性能開銷.
對于這兩種情況, 其實不僅僅是對浏覽器渲染有影響, 還會極大的增加伺服器的壓力, 是以我們需要對請求或者頁面進行一定範圍的緩存.
比如說我們可以把不長變動的請求資料存在 indexedDB 中, 當第二次通路直接可以從 indexedDB 中拿到請求資料, 這樣既降低了伺服器壓力, 也提高了二次渲染的效率.
其次我們可以對部分頁面做路由緩存, 避免每次切換都重新渲染, 當然這隻針對于不需要實時更新資料的頁面而言. 我之前也分享了一篇浏覽器緩存接口實戰的文章, 大家感興趣的可以學習參考, 對于 indexedDB, 我封裝了一個開箱即用的庫, 大家可以直接使用:
github位址: https://github.com/MrXujiang/xdb
5. 動畫性能優化
這也是個老生常談的問題, 這裡直接分享幾個方案:
- 優化精簡 DOM 結構,合理布局
- 使用 transform 代替 left,top 減少使用引起頁面重排的屬性
- 開啟硬體加速(比如設定transform: translateZ(0) 或者transform: translate3d(0, 0, 0))
- 盡量避免浏覽器建立不必要的圖形層
- 盡量減少 js 動畫,如需要,使用性能更友好的requestAnimationFrame
- 使用 chrome performance 工具調試動畫性能
由于dom動畫有上限很低, 是以對于一些更複雜的動畫渲染, 我們可以采用 svg 或者 canvas 來代替, 以降低 dom 對浏覽器記憶體的占用.
6. dom過載導緻的頁面卡頓優化
一個頁面如果dom數量過多, 會産生很多問題, 一方面會使得浏覽器記憶體占用過高, 導緻其他不相關的js邏輯操作進行阻塞或者失效, 表現就是頁面卡頓或者無響應.
為了解決這個問題我們仍然可以使用虛拟滾動的方案或者懶加載的方案, 保證使用者目前螢幕下的dom在一個合理的範圍内, 如果是無法避免必須要展示大段dom元素, 我們可以用一個單獨的頁面來承載或者嵌入, 避免頁面其他部分當機. 也可以對複雜dom 進行局部“冷凍”(在非激活狀态将其轉化為圖檔, 激活時在逐漸渲染)
- 産品需求層優化
産品需求層面導緻的重構主要場景比如:
- 項目國際化支援
- 項目埋點
- 整體項目UI更新
當然還有很到場景這裡不一一介紹了. 以上列的場景都是比較常見的, 而且也有很多解決方案, 後期我會一一複盤. 我們在項目重構之前或者立項之前, 這幾種情況也是需要重點考慮的, 畢竟都是大工作量的任務.
2. 技術更新帶來的重構
技術更新帶來的重構主要有前端架構的更新, 前端設計模式的更新, 腳手架的更新. 後面兩個主要是圍繞前端技術的不斷演進, 我們采取的程式性更新, 比如從傳統的 gulp 更新到 webpack, 或者從 webpack 更新到vite 等. 前者比較常見的場景是企業中有很多老的系統, 采用的是比較傳統的技術方案如 CMD 模式 + jquery, 但是新項目采用的是 webpack + vue 或者 react, 此時我們需要更新項目情況來有選擇的做重構:
- 老項目隻需要少量維護的情況
這種情況我們就不需要大刀闊斧的重新用新架構再寫一套了, 我們隻需要在重構時, 對老項目代碼做好足夠的注釋, 類庫的封裝即可:
其次我們需要做好js變量隔離, 因為傳統模式我們會在 window 頂層定義大量 var 全局變量, 作為優化的一部分, 我們可以采用閉包自執行和變量約定來規範我的js變量定義, 以防止全局的變量污染.
- 老項目仍然需要不斷疊代, 并且後期會有新的子產品
這種情況我們需要做評估和拆分, 如果是小子產品, 我們可以用 jquery插件 的方式快速爹疊代, 如果是頁面級别的疊代, 并且互動比較複雜, 我們可以将老系統的新頁面拆離一個子工程, 采用最新的架構(如vue)來開發疊代, 再通過 MPA 的方式和老系統做內建:
- 老項目和新項目需要互相通信, 嵌套
這種場景下最好的方式就是用iframe + postmessage, 或者我們可以參考類似微前端的方式來管理組織不同子系統.
3. 元件庫重構
對于一個包含很多子系統的複雜的項目系統來說,要想設計一個好的架構,第一步就是合理劃分元件,元件的粒度拆成的足夠細,這樣才能最大限度的複用元件。
對于任何一個複雜系統來說,最重要的就是實作錯綜複雜的業務功能,但是不同子產品或者子系統之間很多業務往往是相通的或者相似的,如果這個時候我們每個頁面對于實作類似的業務場景都去重複去寫一遍業務代碼,那完全是沒必要的,對于可維護性來說也是一種打擊,是以基于這種場景我們的 業務元件 就很有必要出場了。我們可以把功能或者需求類似的有機體封裝成一個業務元件,并對外暴露接口來實作靈活的可定制性,這樣的話我們就可以再不同頁面不同子系統中複用同樣的邏輯和功能了。
同理,不同頁面中往往有可能出現視覺或者互動完全相同或者類似的區塊,為了提高可複用性和提高開發效率,我們往往會基于基礎元件和業務元件再進行一次封裝,讓其成為一個獨立的區塊以便直接複用。
通過這樣一層層封裝,我們就逐漸搭建了一套完整的元件化系統,基于這種模式的開發往往也是一個好的前端架構的開始。但要注意一點就是高層次的元件一定會依賴低層次的元件,但是低層次的元件不可以包含高層次的元件。(聽起來有點像rudex的單向資料流法則),他們的關系就好像下圖:
是以對元件庫的重構需要對我們的項目有一個本質的認知, 并對頁面進行有效的拆分, 進而達到局部的最優, 降低後續的維護成本, 并能提高整個系統甚至跨系統的複用.
有關如何從0到1教你搭建前端團隊的元件系統 我之前也寫過詳細的文章, 大家可以參考學習一下.
總結
系統重構是一個持續的過程, 我們不僅要有持續學習的态度, 還需要不斷的實踐和積累優秀的最佳實踐, 這樣才能在不斷重構中讓我們的系統不斷适應複雜多變的“社會環境”.
更多推薦
分享12款我常用的開源免費軟體
分享6款yyds的可視化搭建開源項目
《前端圖形學實戰》幾何學在前端邊界計算中的應用和原理分析