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

隨着“發佈”進度條走到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大多有冗餘的內容,比如一些無用的defstitle等,刪除後並不會降低圖片質量,還能減小圖片體積。

我們使用svgo-loader對SVG做了一些優化,比如去掉無用屬性,去掉空格換行等。這裏就不細數它能提供的優化項目。大家可以對照svgo-loader的選項配置。

  • 用svg-sprite-loader合併多個SVG

另外,SVG有多種用法,比如:img,background,inline,inline + <use>。如果某些圖反覆出現並且對頁面渲染很關鍵,可以使用svg-sprite-loader將多個圖合併成一個大的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

Using SVG

Modern JavaScript Explained For Dinosaurs

本文鏈接:https://75team.com/post/yunpan_refactoring
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章