你必定是閒得蛋疼才重構的吧

隨着「發佈」進度條走到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

然鵝,咱們很快就意識到一個問題:重構部分跟新增需求沒法保證一致。好比重構到一半,線上功能變了……產品不會等重構完再往前發展。難不成要在新老代碼中並行迭代相同的需求?編程

別慌,必定能想出更高效的解決辦法。稍微分析一下,發現咱們要處理三種狀況:性能優化

  1. 產品須要新增一個功能。好比一個活動彈窗或路由頁面。

解決方法:新功能用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:這也是過渡期,不是最終狀態)。你們不要輕易模仿,若是條件容許,仍是新起一個頁面吧,心情會舒暢不少哦。

  1. 產品須要修改老代碼裏的獨立組件。

解決方法:若是這個組件不是特別複雜,咱們會以「夾帶私貨」的方式重構上線,這樣還能順便讓測試童鞋幫忙驗一下重構後有沒有bug。具體實現參考第一種狀況。

  1. 產品須要修改整站的公共部分。

咱們的網站包含好幾個頁面,這次重構的單頁應用只是其中之一。它們共用了頂部導航欄。在這些頁面模板中經過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 + 。若是某些圖反覆出現而且對頁面渲染很關鍵,可使用svg-sprite-loader將多個圖合併成一個大的SVG,避免逐個發起圖片請求。而後使用內聯或者JS加載的方式將這個SVG引入頁面,而後在須要的地方使用SVG的標籤引用該圖標。合併後的大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

Using SVG

Modern JavaScript Explained For Dinosaurs

本文連接:75team.com/post/yunpan…

相關文章
相關標籤/搜索