隨着“發佈”進度條走到100%,重構的代碼終於上線了。我露出了老母親般的微笑……
最近看了一篇文章,叫《史上最爛的開發項目長啥樣:苦撐12年,600多萬行代碼》,講的是法國的一個軟件項目,因爲各種奇葩的原因,導致代碼質量慘不忍睹,項目多年無法交付,最終還有公司領導入獄。裏面有一些細節讓人哭笑不得:一個右鍵響應事件需要花45分鐘;讀取700MB的數據,需要花7天時間。足見這個軟件的性能有多糟心。
如果讓筆者來接手這“坨”代碼,內心早就飄過無數個敏感詞。其實,筆者自己也維護着一套陳釀了將近7年的代碼,隨着後輩的添油加醋……哦不,添磚加瓦,功能邏輯日益複雜,代碼也變得臃腫,維護起來步履維艱,性能也不盡如人意。終於有一天,我聽見了內心的魔鬼在呼喚:“重構吧~~”
重構是一件磨人的事情,輕易使不得。好在兄弟們齊心協力,各方資源也配合到位。我們小步迭代了大半年,最後一鼓作氣,終於完成了。今天跟大家分享一下這次重構的經驗和收益。
挑戰
此次重構的對象是一個大型單頁應用。它實現了雲端文件管理功能,共有10個路由頁面,涉及文件上傳、音視頻播放、圖片預覽、套餐購買等幾十個功能。前端使用QWrap、jQuery、RequireJS搭建,HTML使用PHP模板引擎Smarty編寫。
我們選擇了Vue.js、vue-router、vuex來改造代碼,用webpack完成模塊打包的工作。彷彿一下子從原始社會邁向了新世紀,是不是很完美?
(圖片來自網絡)
由於項目比較龐大,爲了快速迭代,重構的過渡期允許新舊代碼並存,開發完一部分就測試上線一部分,直到最終完全替代舊代碼。
然鵝,我們很快就意識到一個問題:重構部分跟新增需求無法保證一致。比如重構到一半,線上功能變了……產品不會等重構完再往前發展。難不成要在新老代碼中並行迭代相同的需求?
別慌,一定能想出更高效的解決辦法。稍微分析一下,發現我們要處理三種情況:
1. 產品需要新增一個功能。比如一個活動彈窗或路由頁面。
解決方法:新功能用vue組件實現,然後手動加載到頁面。比如:
const wrap = document.createElement('div')
document.body.appendChild(wrap)
new Vue({
el: wrap,
template: '<App />',
components: { App }
})
如果這個組件必須跟老代碼交互,就將組件暴露給全局變量,然後由老代碼調用全局變量的方法。比如:
// someApp.js
window.someApp = new Vue({
...
methods: {
funcA() {
// do somthing
}
}
})
// 老代碼.js
...
window.someApp.funcA()
注意:全局變量名需要人工協調,避免命名衝突。PS:這是過渡期的妥協,不是最終狀態。
新增一個路由頁面時更棘手。聰明的讀者一定會想到讓新增的路由頁面獨立於已有的單頁應用,單獨分配一個URL,這樣代碼會更乾淨。
假如新增的路由頁面需要實現十幾個功能,而這些功能已經存在於舊代碼中呢?權衡了需求的緊急性和對代碼整潔度的追求,我們再次妥協(PS:這也是過渡期,不是最終狀態)。大家不要輕易模仿,如果條件允許,還是新起一個頁面吧,心情會舒暢很多哦。
2. 產品需要修改老代碼裏的獨立組件。
解決方法:如果這個組件不是特別複雜,我們會以“夾帶私貨”的方式重構上線,這樣還能順便讓測試童鞋幫忙驗一下重構後有沒有bug。具體實現參考第一種情況。
3. 產品需要修改整站的公共部分。
我們的網站包含好幾個頁面,此次重構的單頁應用只是其中之一。它們共用了頂部導航欄。在這些頁面模板中通過Smarty的include
語法加載:
{%include file=“topPanel.inc”%}
產品在一次界面改版中提出要給導航欄加上一些功能的快捷入口,比如導入文件,購買套餐等。而這些功能在單頁應用中已經用vue實現了。所以還得將導航欄實現爲vue組件。
爲了更快渲染導航欄,需要保留它原有的標籤,而不是在JS裏以組件的形式渲染。所以需要用到特殊手段:
- 在topPanel.inc裏寫上自定義標籤,對應到vue組件,比如下面代碼裏的
<import-button>
。當JS未加載時,會立即渲染導航欄的常規標籤以及自定義標籤。
<div id="topPanelMountee">
<div id="topPanel">
<div>一些頁面直出的內容</div>
...
<import-button>
<button class="btn-import">
導入
</button>
</import-button>
...
</div>
</div>
- 導航欄組件:topPanel.js,它包含了
ImportButton
等子組件(對應上面的<import-button>
)。等JS加載後,ImportButton
組件就會掛載到<import-button>
上併爲這個按鈕綁定行爲。另外,注意下面代碼中的template
並不是<App />
,而是一個ID選擇器,這樣topPanel組件就會以#topPanelMountee
裏的內容作爲模板掛載到#topPanelMountee
元素中,是不是很機智~
// topPanel.js
new Vue({
el: '#topPanelMountee',
template: '#topPanelMountee',
components: {
...
ImportButton
}
})
徹底重構後,我們還做了進一步的性能優化。
進一步優化
1. HTML瘦身
在採用組件化開發之前,HTML中預置了許多標籤元素,比如:
<button data-cn="del" class="del">刪除</button>
<button data-cn="rename" class="rename">重命名</button>
...
當狀態改變時,通過JS操作DOM來控制預置標籤的內容或顯示隱藏狀態。這種做法不僅讓HTML很臃腫,JS跟DOM的緊耦合也讓人頭大。改成組件化開發後,將這些元素統統刪掉。
之前還使用了很多全局變量存放服務端輸出的數據。比如:
<script>
var SYS_CONF = {
userName: {%$userInfo.name%}
...
}
</script>
隨着時間的推移,這些全局變量越來越多,管理起來很費勁。還有一些已經廢棄的變量,對HTML的體積做出了“貢獻”。所以重構時只保留了必需的變量。更多數據則在運行時加載。
另外,在沒有模板字面量的年代,HTML裏大量使用了<script>
標籤存放運行時所需的模板元素。比如:
<script type="text/template" id="sharePanel">
<div class="share">
...
</div>
</script>
雖然上線時會把這些標籤內的字符串提取成JS變量,以減小HTML的體積,但在開發時,這些<script>
標籤會增加代碼閱讀的難度,因爲要不停地切換HTML和JS目錄查找。所以重構後刪掉了大量的<script>
標籤,使用vue的<template>
以及ES6的模板字面量來管理模板字符串。
2. 漸進渲染
首屏想要更快渲染,還要確保文檔加載的CSS和JS儘量少,因爲它們會阻塞文檔加載。所以我們儘可能延遲加載非關鍵組件。比如:
單頁應用有很多路由組件。所以除了默認跳轉的路由組件,將非默認路由組件打包成單獨的chunk。使用import()
的方式動態加載。只有命中該路由時,才加載組件。比如:
const AsyncComp = () => import(/* webpackChunkName: "AsyncCompName" */ 'AsyncComp.vue')
const routes = [{
path: '/some/path',
meta: {
type: 'sharelink',
isValid: true,
listKey: 'sharelink'
},
component: AsyncComp
}]
...
這些組件其實可以延遲到主要內容渲染完畢再加載。將這些組件單獨打包爲一個chunk。比如:
import(/* webpackChunkName: "lazy_load" */ 'a.js')
import(/* webpackChunkName: "lazy_load" */ 'b.js')
如果某些功能屬於低頻操作,或者不是所有用戶都需要。則可以選擇延遲到需要的時候再加載。比如:
async handler () {
await const {someFunc} = import('someFuncModule')
someFunc()
}
3. 優化圖片
雖然代碼做了很多優化,但是動輒幾十到幾百KB的圖片瞬間碾壓了辛苦重構帶來的提升。所以圖片的優化也是至關重要滴~
1. PNG改成SVG
由於項目曾經支持IE6-8,大量使用了PNG,JPEG等格式的圖片。隨着歷史的車輪滾滾向前,IE6-8的用戶佔比已經大大降低,我們在去年放棄了對IE8-的支持。這樣一來就能採取更優的解決方案啦~
我們的頁面上有各種大小的圖標和不同種類的佔位圖。原先使用位圖並不能很好的適配retina顯示器。現在改成SVG,只需要一套圖片即可。相比PNG,SVG有以下優點:
- 壓縮後體積小
- 無限縮放,
- 不失真 retina顯示器上清晰
2. 進一步“壓榨”SVG
雖然換成SVG,但是還遠遠不夠,使用webpack的loader可以有效地壓縮SVG體積。
- 用svgo-loader去除無用屬性
SVG本身既是文本也是圖片。設計師提供的SVG大多有冗餘的內容,比如一些無用的defs
,title
等,刪除後並不會降低圖片質量,還能減小圖片體積。
我們使用svgo-loader對SVG做了一些優化,比如去掉無用屬性,去掉空格換行等。這裏就不細數它能提供的優化項目。大家可以對照svgo-loader的選項配置。
- 用svg-sprite-loader合併多個SVG
另外,SVG有多種用法,比如:img,background,inline,inline + <use>
。如果某些圖反覆出現並且對頁面渲染很關鍵,可以使用svg-sprite-loade
r將多個圖合併成一個大的SVG,避免逐個發起圖片請求。然後使用內聯或者JS加載的方式將這個SVG引入頁面,然後在需要的地方使用SVG的<use>
標籤引用該圖標。合併後的大SVG如下圖:
使用時:
<svg>
<use xlink:href="#icon-add"></use>
</svg>
即可在使用的位置展現該圖標。
以上是一些優化手段,下面給大家分享一下重構後的收益。
重構的收益
以下是重構帶來的收益:
收益項 | 重構前 | 重構後 |
---|---|---|
組件化 | 無 | 100% |
模塊化 | 50% | 100% |
規範化 | 無 | ESLint 代碼規範檢查 |
語法 | ES5 | ES6+ |
首屏有效渲染時間 | 1.59S | 1.28s(提升19%) |
首次交互時間 | 2.56S | 1.54s(提升39%) |
-
組件化:從0到100%
老代碼沒有組件的概念,都是指令式的編程模式以及對DOM的直接操作。重構後,改爲組件化以後,可以充分利用組件的高複用性,以及虛擬DOM的性能優化,帶來更愉悅的開發體驗。 -
模塊化:從50%到100%
老代碼中也用RequireJS做了一定程度的模塊化,但是僅限於業務模塊,沒有解決第三方依賴的安裝和升級問題。重構後,藉助webpack和npm,只需要npm install
安裝第三方依賴,然後用import
的方式加載。極大地提高了開發效率。 -
規範化:從0到1
老代碼幾乎沒有代碼規範,甚至連同一份文件裏都有不同的代碼縮進,強迫症根本無法忍受。重構後,使用ESLint對代碼格式進行了統一,代碼看起來更加賞心悅目。 -
ES6+語法:從0到大量使用
老代碼所使用的庫因爲歷史悠久,加上沒有引入轉譯流程,只能使用ES5語法。重構後,能夠盡情使用箭頭函數、解構、async/await等語言新特性來簡化代碼,從而提升開發體驗。 -
性能提升
根據上線前後Lighthouse的性能檢測數據,首次有效渲染時間(First Meaningful Paint,FMP)提升 19% 。該指標表示用戶看到有用信息的時間(比如文件列表)。首次交互(First Interactive,FI)提升 39%。該指標表示用戶可以開始跟網頁進行交互的時間 。
以上就是這次重構的總結。不要容忍代碼裏的壞味道,更不要容忍低效的開發模式。及時發現,勇敢改進吧~
參考
Chrome 中的 First Meaningful Paint