我是如何讓公司後臺管理系統煥然一新的(上) -專案重構,效能優化

weixin_34370347發表於2019-03-04

寫在前面

馬上到了金三銀四的時間,很多公司開啟了今年第一輪招聘的熱潮,雖說今年是網際網路的寒冬,但是隻要對技術始終抱有熱情以及有過硬的實力,即使是寒冬也不會阻撓你前進的步伐。在面試的時候,往往在二面,三面的時面試官會結合你的簡歷問一些關於你簡歷上專案的問題,而以下這個問題在很多時候都會被問到

在這個專案中你有遇到什麼技術難點,你是怎麼解決的?

其實這個問題旨在瞭解你在遇到問題的時候的解決方法,畢竟現在前端技術領域廣,各種框架和元件庫層出不窮,而業務需求上有時紛繁複雜,觀察一個程式設計師在面對未知問題時是如何處理的,這個過程相對於只出一些面試題來考面試者更能瞭解面試者實際解決問題的能力

而很多人會說我的專案不大,並沒有什麼難點,或者說並不算難點,只能說是一些坑,只要google一下就能解決,實在不行請教我同事,這些問題並沒有困擾我很久。其實我也遇到過相同的情況,和麵試官說如何通過搜尋引擎解決這些坑的吧不太好,讓面試官認為你只是一個API Caller,但是又沒有什麼值得一談的專案難點

我的建議是,如果沒有什麼可以深聊的技術難點,不妨在日常開發過程中,試著封裝幾個常用的元件,同時嘗試分析專案的效能瓶頸,尋找一些優化的方案,同樣也能讓面試官對你有一個整體的瞭解

在這篇文章中,我會分享在我目前公司的專案裡,是如何在滿足業務需求的基礎上,讓整個系統煥然一新的過程

技術棧是Vue + Element的單頁面應用

起源

在我剛入職的那會,編碼能力不怎麼好,加上之前離職的前端技術棧是React,接手這個Vue專案的時候,程式碼高度的耦合,而那個時候因為能力有限,也只是在他的基礎上繼續開發,好在接手的時候開發進度也只是剛剛開始,因此在幾個月後的某一天,我做了一個決定:準備把整個專案重寫

得益於整個後臺管理系統都是我獨立開發的,專案的不足點我都深有體會,並且修改的時候能夠更加的自由,恰好在那段時間看了花褲衩的vue-element-admin,我決定新開一個工程把之前的程式碼全部重寫

專案結構

之前我有打算基於Webpack4自己寫個腳手架用來打包檔案,但是那段時間剛好Vue-cli3剛剛釋出正式版並且也是基於Webpack4封裝的,於是想了一下還決定使用新的Vue-cli3腳手架搭建,最後我將專案分為以下層級

├─api                                 //api介面
├─assets                              //專案執行時使用到的圖片和靜態資源
├─components                          //元件
│  ├─Breadcrumb                       //麵包屑元件
│  ├─Ellipsis                         //業務元件
│  ├─FormPanel                        //業務元件
│  ├─Sidebar                          //側邊欄元件
│  └─globalComponents                 //全域性元件
│     ├─Pagination                    //分頁器元件   
│     ├─SildeDown                     //業務元件
│     ├─SvgIcon                       //svg圖示元件
│     ├─TableOptions                  //業務元件
│     ├─Toggle                        //業務元件
│     ├─ZTable                        //表格元件
│     └─index.js                      //全域性元件自動註冊的指令碼
│  
├─directive                           //自定義指令
├─element                             //elementui
├─errorLog                            //錯誤捕獲
├─filters                             //全域性過濾器
├─icons                               //svg圖示存放資料夾
├─interface                           //TypeScript介面
├─mixins                              //區域性混入
├─router                              //vue-router
│  ├─modules                      
│  └─index.js           
├─store                                //vuex
│  ├─modules                      
│  └─index.js                      
├─style                                //全域性樣式/區域性頁面可複用的樣式
├─util                                //公共的模組(axios,cookie封裝,工具函式)
├─vendor                              //類庫檔案
└─views                               //頁面元件
複製程式碼

一個良好的專案分層在業務迭代的時候能夠快速找到對應的模組進行修改,而不是在茫茫的程式碼海中找到其中的某一行程式碼

效能優化

在我重寫整個系統之前,每次打包都會花費好幾分鐘的時間,並且打包後的專案超過了17M

然而在我重寫系統之後,打包後的體積只有2M,縮小了8倍

這裡我從以下4個方面分享一下我在專案中是如何改善系統的效能,讓系統"步履如飛"的

  • 構建相關
  • 網路請求相關
  • 靜態資源優化
  • 編碼相關

構建相關

構建方面通過合理的配置構建工具,達到減少生產環境的程式碼的體積,減少打包時間,縮短頁面載入時間

路由懶載入

傳統的路由元件是通過import靜態的打包到專案中,這樣做的缺點是因為所有的頁面元件都打包在同一個指令碼檔案中,導致生產環境下首屏因為載入的程式碼量太多會有明顯的卡頓(白屏)

通過import()使得ES6的模組有了動態載入的能力,讓url匹配到相應的路徑時,會動態載入頁面元件,這樣首屏的程式碼量會大幅減少,webpack會把動態載入的頁面元件分離成單獨的一個chunk.js檔案

當然懶載入也有缺點,就是會額外的增加一個http請求,如果專案非常小的話可以考慮不使用路由懶載入

預渲染

由於瀏覽器在渲染出頁面之前,需要先載入和解析相應的html,css和js檔案,為此會有一段白屏的時間,如何儘可能的減少白屏對使用者的影響,目前我選擇的是在html模版中,注入一個loading動畫,這裡我拿D2-Admin中的loading動畫舉例

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>icon.ico">
    <title><%= VUE_APP_TITLE %></title>
    <style>
      html, body, #app { height: 100%; margin: 0px; padding: 0px; }
      .d2-home { background-color: #303133; height: 100%; display: flex; flex-direction: column; }
      .d2-home__main { user-select: none; width: 100%; flex-grow: 1; display: flex; justify-content: center; align-items: center; flex-direction: column; }
      .d2-home__footer { width: 100%; flex-grow: 0; text-align: center; padding: 1em 0; }
      .d2-home__footer > a { font-size: 12px; color: #ABABAB; text-decoration: none; }
      .d2-home__loading { height: 32px; width: 32px; margin-bottom: 20px; }
      .d2-home__title { color: #FFF; font-size: 14px; margin-bottom: 10px; }
      .d2-home__sub-title { color: #ABABAB; font-size: 12px; }
    </style>
  </head>
  <body>
    <noscript>
      <strong>
        很抱歉,如果沒有 JavaScript 支援,D2Admin 將不能正常工作。請啟用瀏覽器的 JavaScript 然後繼續。
      </strong>
    </noscript>
    <div id="app">
      <div class="d2-home">
        <div class="d2-home__main">
          <img
            class="d2-home__loading"
            src="./image/loading/loading-spin.svg"
            alt="loading">
          <div class="d2-home__title">
            正在載入資源
          </div>
          <div class="d2-home__sub-title">
            初次載入資源可能需要較多時間 請耐心等待
          </div>
        </div>
        <div class="d2-home__footer">
          <a
            href="https://github.com/d2-projects/d2-admin"
            target="_blank">
            https://github.com/d2-projects/d2-admin
          </a>
        </div>
      </div>
    </div>
  </body>
</html>

複製程式碼

在打包完成後,在這個index.html下方還會注入頁面的指令碼,當使用者訪問你的專案時,指令碼還沒有執行,但是可以顯示loading動畫,因為它是直接注入在html中的,等到指令碼執行完畢後,Vue會新生成一個app的節點然後將舊的同名節點刪除,這樣可以有效的過渡白屏的時間

loading動畫只是一個讓使用者感知到你程式正在啟動的效果,只是一個靜態頁面沒有任何的功能

另外預渲染還可以使用服務端渲染(SSR),通過後端輸出一個首頁的模版,或者使用骨架屏的方案,這裡本人沒有深入的瞭解過,有興趣的朋友可以去實踐一下

升級到最新的webpack版本

webpack4相對於webpack3來說在打包優化方面效能提升還是比較明顯的,如果覺得自己配置腳手架比較複雜的話,可以使用vue-cli3來構建你的專案,同樣是基於webpack4搭建的

合理使用第三方庫

如果專案中有一些日期操作的需求,不妨將目光從moment轉移到day,相對於笨重的moment,它只有2kb,day和moment的api完全一樣,並且中文文件也比較友好

另外對於lodash這類的庫如果只需要部分功能,則只要引入其中一部分,這樣webpack在treeshaking後在生產環境也只會引入這一部分的程式碼

常用的路徑建立檔案別名

給常用的模組路徑建立一個別名是一個不錯的選擇,可以減少模組查詢時耗費的時間,專案越大收益也就越明顯

vue-cli3中的配置和使用方法(webpack鏈式呼叫文件)

使用視覺化工具分析打包後的模組體積

我通過webpack-bundle-analyzer這個外掛在每次打包後能夠更加直觀的分析打包後模組的體積,再對其中比較大的模組進行優化

這是我在優化前的各模組體積:

因為業務需求,要求前端匯出pdf和excel檔案,我這裡引入了xlsx和pdf.js這2個包,但是打包後通過視覺化工具發現光著2個檔案就佔了一半的專案體積,另外elementui和moment也非常的大

這是我優化後通過視覺化工具觀察到的各模組體積,我將這些類庫放到CDN上從生產環境中抽離出去,可以看到沒有明顯特別大的模組了

網路請求相關

這部分旨在實現需求的前提下儘量減少http請求的開銷,或者減少響應時間

CDN

將第三方的類庫放到CDN上,能夠大幅度減少生產環境中的專案體積,另外CDN能夠實時地根據網路流量和各節點的連線、負載狀況以及到使用者的距離和響應時間等綜合資訊將使用者的請求重新導向離使用者最近的服務節點上

通俗的來說就是提升專案中的靜態檔案的傳輸速度,在vue-cli3中可以通過externals配置項,將第三方的類庫的引用地址從本地指向你提供的CDN地址

這裡通過環境變數來判斷生產環境才啟用CDN,除了需要開啟CDN外,你還需要在index.html注入CDN的域名,所以我這裡通過html-webpack-plugin根據cdn域名動態的注入script標籤,同時需要在index.html中通過模版的語法宣告迴圈的陣列和注入的元素

打包前的index.html:

打包後的index.html:

可以看到通過這個外掛可以將cdn域名動態的注入到打包後的index.html中

還有一點要注意的是,externals物件的屬性為你引入包的名字,而屬性值是對應的AMD模組名字(這個名字比較特殊,一般常用的我已經列出來了,其餘的第三方類庫名字可以到訪問對應的CDN在原始碼中尋找,一般在開頭行都會有宣告,匯入有困難的還可以看下這篇部落格webpack externals 深入理解

gzip

為你的檔案開啟gzip壓縮是一個不錯的選擇,通常開啟gzip壓縮能夠有效的縮小傳輸資源的大小,如果你的專案是用nginx作為web伺服器的話,只需在nginx的配置檔案中配置相應的gzip選項就可以讓你的靜態資源伺服器開啟gzip壓縮

    #開啟和關閉gzip模式
    gzip on;
    #gizp壓縮起點,檔案大於1k才進行壓縮
    gzip_min_length 1k;
    # gzip 壓縮級別,1-9,數字越大壓縮的越好,也越佔用CPU時間
    gzip_comp_level 6;
    # 進行壓縮的檔案型別。
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
    #nginx對於靜態檔案的處理模組,開啟後會尋找以.gz結尾的檔案,直接返回,不會佔用cpu進行壓縮,如果找不到則不進行壓縮
    gzip_static on
    # 是否在http header中新增Vary: Accept-Encoding,建議開啟
    gzip_vary on;
    # 設定gzip壓縮針對的HTTP協議版本
    gzip_http_version 1.1;
複製程式碼

但是我們這裡要說的是前端輸出gzip檔案,利用compression-webpack-plugin讓webpack在打包的時候輸出.gz字尾的壓縮檔案

這樣不需要伺服器主動壓縮我們就已經可以得到gzip檔案,在上面的nginx配置項中可以發現這一行

 #nginx對於靜態檔案的處理模組,開啟後會尋找以.gz結尾的檔案,直接返回,不會佔用cpu進行壓縮,如果找不到則不進行壓縮
    gzip_static on
複製程式碼

只要把.gz的檔案放到伺服器上,開始gzip_static就可以讓伺服器優先返回.gz檔案,在面對高流量時,也能一定程度減輕對伺服器的壓力,屬於用空間來換時間(.gz檔案會額外佔有伺服器的空間)

資源嗅探

對於現代瀏覽器來說,可以給link標籤新增preload,prefetch,dns-prefetch屬性

  • preload可以讓瀏覽器儘早發現提前載入資源,而不是等到解析到當前標籤才發http請求
  • prefetch可以讓瀏覽器提前載入下個頁面可能會需要的資源
  • dns-prefetch可以讓瀏覽器提前對域名進行解析,減少DNS查詢的開銷

vue-cli3預設會給所有懶載入的路由新增prefetch屬性,如果你的靜態資源和後端介面不是同一個伺服器的話,可以將你後端的域名放入link標籤加入dns-prefetch屬性

京東首頁也使用到了dns-prefetch技術

http2

http2從2015年問世以來已經走過了4個年頭,如今在國內也有超過50%的覆蓋率,得益於http2的分幀傳輸,它能夠極大的減少http(s)請求開銷

http2和http1.1的效能差異對比

如果系統首屏同一時間需要載入的靜態資源非常多,但是瀏覽器對同一域名的tcp連線數量是有限制的(chrome為6個)超過規定數量的tcp連線,則必須要等到之前的請求收到響應後才能繼續傳送,而http2則可以在一個tcp連線中併發多個請求沒有限制,在一些網路較差的環境開啟http2效能提升尤為明顯

這裡極力推薦在支援https協議的伺服器中使用http2協議,可以通過web伺服器Nginx配置,或是直接讓伺服器支援http2

靜態資源優化

這部分旨在減少請求一些圖片資源所造成的影響

圖片懶載入

如果你的系統是一個偏展示的專案需要給使用者展示大量圖片,是否啟用圖片懶載入可能是你需要考慮的一個點,不在使用者視野中的圖片是沒有必要載入的,圖片懶載入通過讓圖片先載入成一張統一的圖片,再給進入使用者視野的圖片替換真正的圖片地址,可以同一時間減少http請求開銷,避免顯示圖片導致的畫面抖動,提高使用者體驗

下面我提供2種圖片懶載入的思路,這2個方案最終都是用將佔位的圖片替換成真正的圖片,然後給img標籤設定一個自定義屬性data-src存放真正的圖片地址,src存放佔點陣圖片的地址

  1. 使用getBoundingClientRect該DOM節點相關的CSS邊框集合,它返回一個物件,其中有一個top屬性代表當前DOM節點距離瀏覽器視窗頂部的高度,判斷是否小於當前瀏覽器視窗的高度(window.innerHeight),若小於說明已經進入使用者視野,然後替換為真正的圖片即可,同時需要監聽scroll事件不停的執行上述操作(需要進行節流)
  2. 使用IntersectionObserver構造器傳入一個回撥函式,生成一個例項observer,這個例項有一個observe方法能夠監聽指定元素是否進入檢視,進入則會觸發之前的回撥函式。同時給回撥函式傳入一個entries的引數,記錄著這個例項觀察的所有元素的一些閾值資訊,其中intersectionRatio大於0表示進入了使用者視野

此時替換為真實的圖片,並且呼叫例項的unobserve將這個img元素從這個例項的觀察列表的去除

這2種的區別在於監聽的方式,我個人更推薦使用Intersection Observer,因為監聽scroll事件開銷比較大,而讓將這個工作交給另一個執行緒非同步的去監聽開銷會小很多,但是它的缺點是一些老版本的瀏覽器可能支援率不高,好在社群有polyfill的方案

或者可以直接使用第三方的元件庫vue-lazyload

使用svg圖示

相對於用一張圖片來表示圖示,svg擁有更好的圖片質量,體積更小,並且不需要開啟額外的http請求,svg是一個未來的趨勢,阿里的圖示庫iconfont支援匯出svg格式的圖示,但是在專案中需要封裝一個支援svg的元件,具體封裝的教程可以參考花褲衩的文章這裡就不多贅述了手摸手,帶你優雅的使用 icon,或者可以參考我的github

使用webp圖片

webp圖片最初在2010年釋出,目標是減少檔案大小,但達到和JPEG格式相同的圖片質量,希望能夠減少圖片檔在網路上的傳送時間。webp圖片無損比png圖片無損的平均體積要小 20%~40%,並且圖片質量用肉眼看幾乎沒什麼差別

webp圖片的缺點是相容性並不是那麼的好,在can l use 上查到webp圖片的支援率並不是那麼的理想。但是我們仍可以在支援webp圖片的瀏覽器中使用它,而在不支援的瀏覽器提供png圖片

編碼相關

編碼這方面主要是減少對DOM的訪問,減少瀏覽器的重排/重繪,訪問DOM是非常昂貴的操作,因為會涉及到2個不同的執行緒互動(JS執行緒和UI渲染執行緒)並且DOM本身又是一個非常笨重的物件,這裡給出幾個建議

  • 如果有需要動態建立DOM的需求,可以建立一個文件碎片(DocumentFragment),在文件碎片中操作因為不是在當前文件流不會引起重排/重繪,最後再一次性插入DOM節點

  • 避免頻繁獲取檢視資訊(getBoundingClientRect,clientWidth,offsetWidth),當發生重排/重繪操作時瀏覽器會維護一個佇列,等到超過了最大值或過了指定時間(1000ms/60 = 16.6ms)才會去清空佇列一次性執行操作,這樣可以節省效能,而獲取檢視資訊會立刻清空佇列執行重排/重繪

  • 高頻的監聽事件使用函式防抖/節流(可以使用lodash庫的throttle函式,但是推薦先搞懂原理)

  • 特效可以考慮單獨觸發渲染層(CSS3的transform會觸發渲染層),動畫可以使用絕對定位脫離文件流

開發過程中小技巧

使用require.context這個webpack的api可以避免每次引入一個檔案都需要顯式的用import匯入,它可以掃描你指定的檔案,然後全部匯入到指定檔案,可以用在

  • vue-router的路由自動匯入
  • vuex的模組自動匯入
  • svg圖示的自動匯入
  • 全域性元件的自動匯入

vuex:

全域性元件和svg圖示:

有興趣的朋友可以看看我另一篇介紹這個api的部落格

寫在後面

本人為18年畢業本科生,座標上海,1年的前端開發經驗,如果有比較好的網際網路企業內推機會的話,希望能在評論區能留下您的聯絡方式或者聯絡我的郵箱1996yeyan@gmail.com,非常感謝~

參考資料

vue-element-admin

D2 Admin

嗨,送你一張Web效能優化地圖

vue-cli3 專案從搭建優化到docker部署

前端效能優化不完全指北

相關文章