隨着「發佈」進度條走到100%,重構的代碼終於上線了。我露出了老母親般的圍笑……前端
最近看了一篇文章,叫《史上最爛的開發項目長啥樣:苦撐12年,600多萬行代碼》,講的是法國的一個軟件項目,由於各類奇葩的緣由,致使代碼質量慘不忍睹,項目多年沒法交付,最終還有公司領導入獄。裏面有一些細節讓人啼笑皆非:一個右鍵響應事件須要花45分鐘;讀取700MB的數據,須要花7天時間。足見這個軟件的性能有多糟心。vue
若是讓筆者來接手這「坨」代碼,心裏早就飄過無數個敏感詞。其實,筆者本身也維護着一套陳釀了將近7年的代碼,隨着後輩的添油加醋……哦不,添磚加瓦,功能邏輯日益複雜,代碼也變得臃腫,維護起來寸步難行,性能也不盡如人意。終於有一天,我聽見了心裏的魔鬼在呼喚:「重構吧~~」webpack
重構是一件磨人的事情,輕易使不得。好在兄弟們齊心合力,各方資源也配合到位。咱們小步迭代了大半年,最後一氣呵成,終於完成了。今天跟你們分享一下此次重構的經驗和收益。web
這次重構的對象是一個大型單頁應用。它實現了雲端文件管理功能,共有10個路由頁面,涉及文件上傳、音視頻播放、圖片預覽、套餐購買等幾十個功能。前端使用QWrap、jQuery、RequireJS搭建,HTML使用PHP模板引擎Smarty編寫。vue-router
咱們選擇了Vue.js、vue-router、vuex來改造代碼,用webpack完成模塊打包的工做。彷彿一會兒從原始社會邁向了新世紀,是否是很完美?vuex
因爲項目比較龐大,爲了快速迭代,重構的過渡期容許新舊代碼並存,開發完一部分就測試上線一部分,直到最終徹底替代舊代碼。npm
然鵝,咱們很快就意識到一個問題:重構部分跟新增需求沒法保證一致。好比重構到一半,線上功能變了……產品不會等重構完再往前發展。難不成要在新老代碼中並行迭代相同的需求?編程
別慌,必定能想出更高效的解決辦法。稍微分析一下,發現咱們要處理三種狀況:性能優化
解決方法:新功能用vue組件實現,而後手動加載到頁面。好比:bash
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:這也是過渡期,不是最終狀態)。你們不要輕易模仿,若是條件容許,仍是新起一個頁面吧,心情會舒暢不少哦。
解決方法:若是這個組件不是特別複雜,咱們會以「夾帶私貨」的方式重構上線,這樣還能順便讓測試童鞋幫忙驗一下重構後有沒有bug。具體實現參考第一種狀況。
咱們的網站包含好幾個頁面,這次重構的單頁應用只是其中之一。它們共用了頂部導航欄。在這些頁面模板中經過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
}
})
複製代碼
完全重構後,咱們還作了進一步的性能優化。
<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的模板字面量來管理模板字符串。
首屏想要更快渲染,還要確保文檔加載的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
}]
...
複製代碼
import(/* webpackChunkName: "lazy_load" */ 'a.js')
import(/* webpackChunkName: "lazy_load" */ 'b.js')
複製代碼
async handler () {
await const {someFunc} = import('someFuncModule')
someFunc()
}
複製代碼
雖然代碼作了不少優化,可是動輒幾十到幾百KB的圖片瞬間碾壓了辛苦重構帶來的提高。因此圖片的優化也是相當重要滴~
因爲項目曾經支持IE6-8,大量使用了PNG,JPEG等格式的圖片。隨着歷史的車輪滾滾向前,IE6-8的用戶佔比已經大大下降,咱們在去年放棄了對IE8-的支持。這樣一來就能採起更優的解決方案啦~
咱們的頁面上有各類大小的圖標和不一樣種類的佔位圖。原先使用位圖並不能很好的適配retina顯示器。如今改爲SVG,只須要一套圖片便可。相比PNG,SVG有如下優勢:
壓縮後體積小 無限縮放,不失真 retina顯示器上清晰
2. 進一步「壓榨」SVG
雖然換成SVG,可是還遠遠不夠,使用webpack的loader能夠有效地壓縮SVG體積。
咱們使用svgo-loader對SVG作了一些優化,好比去掉無用屬性,去掉空格換行等。這裏就不細數它能提供的優化項目。你們能夠對照svgo-loader的選項配置。
<svg>
<use xlink:href="#icon-add"></use>
</svg>
複製代碼
便可在使用的位置展示該圖標。
以上是一些優化手段,下面給你們分享一下重構後的收益。
如下是重構帶來的收益: 收益項 重構前 重構後
組件化 無 100%
模塊化 50% 100%
規範化 無 ESLint 代碼規範檢查
語法 ES5 ES6+
首屏有效渲染時間 1.59s 1.28s(提高19%)
首次交互時間 2.56s 1.54s(提高39%)
老代碼沒有組件的概念,都是指令式的編程模式以及對DOM的直接操做。重構後,改成組件化之後,能夠充分利用組件的高複用性,以及虛擬DOM的性能優化,帶來更愉悅的開發體驗。
老代碼中也用RequireJS作了必定程度的模塊化,可是僅限於業務模塊,沒有解決第三方依賴的安裝和升級問題。重構後,藉助webpack和npm,只須要npm install安裝第三方依賴,而後用import的方式加載。極大地提升了開發效率。
老代碼幾乎沒有代碼規範,甚至連同一份文件裏都有不一樣的代碼縮進,強迫症根本沒法忍受。重構後,使用ESLint對代碼格式進行了統一,代碼看起來更加賞心悅目。
老代碼所使用的庫由於歷史悠久,加上沒有引入轉譯流程,只能使用ES5語法。重構後,可以盡情使用箭頭函數、解構、async/await等語言新特性來簡化代碼,從而提高開發體驗。
根據上線先後Lighthouse的性能檢測數據,首次有效渲染時間(First Meaningful Paint,FMP)提高 19% 。該指標表示用戶看到有用信息的時間(好比文件列表)。首次交互(First Interactive,FI)提高 39%。該指標表示用戶能夠開始跟網頁進行交互的時間 。
以上就是此次重構的總結。不要容忍代碼裏的壞味道,更不要容忍低效的開發模式。及時發現,勇敢改進吧~
Chrome 中的 First Meaningful Paint
Using SVG
Modern JavaScript Explained For Dinosaurs