寫在前面
馬上到了金三銀四的時間,很多公司開啟了今年第一輪招聘的熱潮,雖說今年是網際網路的寒冬,但是隻要對技術始終抱有熱情以及有過硬的實力,即使是寒冬也不會阻撓你前進的步伐。在面試的時候,往往在二面,三面的時面試官會結合你的簡歷問一些關於你簡歷上專案的問題,而以下這個問題在很多時候都會被問到
在這個專案中你有遇到什麼技術難點,你是怎麼解決的?
其實這個問題旨在瞭解你在遇到問題的時候的解決方法,畢竟現在前端技術領域廣,各種框架和元件庫層出不窮,而業務需求上有時紛繁複雜,觀察一個程式設計師在面對未知問題時是如何處理的,這個過程相對於只出一些面試題來考面試者更能瞭解面試者實際解決問題的能力
而很多人會說我的專案不大,並沒有什麼難點,或者說並不算難點,只能說是一些坑,只要google一下就能解決,實在不行請教我同事,這些問題並沒有困擾我很久。其實我也遇到過相同的情況,和麵試官說如何通過搜尋引擎解決這些坑的吧不太好,讓面試官認為你只是一個API Caller,但是又沒有什麼值得一談的專案難點
我的建議是,如果沒有什麼可以深聊的技術難點,不妨在日常開發過程中,試著封裝幾個常用的元件,同時嘗試分析專案的效能瓶頸,尋找一些優化的方案,同樣也能讓面試官對你有一個整體的瞭解
在這篇文章中,我會分享在我目前公司的專案裡,是如何在滿足業務需求的基礎上,讓整個系統煥然一新的過程
技術棧是Vue + Element的單頁面應用
起源
在我剛入職的那會,編碼能力不怎麼好,加上之前離職的前端技術棧是React,接手這個Vue專案的時候,程式碼高度的耦合,而那個時候因為能力有限,也只是在他的基礎上繼續開發,好在接手的時候開發進度也只是剛剛開始,因此在幾個月後的某一天,我做了一個決定:準備把整個專案重寫
得益於整個後臺管理系統都是我獨立開發的,專案的不足點我都深有體會,並且修改的時候能夠更加的自由,恰好在那段時間看了花褲衩的vue-element-admin,我決定新開一個工程把之前的程式碼全部重寫
專案結構
之前我有打算基於Webpack4自己寫個腳手架用來打包檔案,但是那段時間剛好Vue-cli3剛剛釋出正式版並且也是基於Webpack4封裝的,於是想了一下還決定使用新的Vue-cli3腳手架搭建,最後我將專案分為以下層級
├─api //api介面
├─assets //專案執行時使用到的圖片和靜態資源
├─components //元件
│ ├─BaseEllipsis //業務元件 (Base開頭都是全域性元件)
│ ├─BasePagination //分頁器元件
│ ├─BaseIcon //svg圖示元件
│ ├─BaseToggle //業務元件
│ ├─BaseTable //表格元件
│ ├─FormPanel //業務元件(Form開頭是圍繞表單相關的小元件)
│ ├─TableOptions //業務元件(Table開頭是圍繞表格相關的小元件)
│ ├─TheBreadcrumb //麵包屑元件(The開頭是每個頁面元件只會引入一次的無狀態元件)
| ├─TheSidebar //側邊欄元件
│ ├─TransitionSildeDown //業務元件(Transition開頭是動畫元件)
│ └─index.js //全域性元件自動註冊的指令碼
│
├─directives //自定義指令
├─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個方面分享一下我在專案中是如何改善系統的效能,讓系統"步履如飛"的
- 網路請求相關
- 構建相關
- 靜態資源優化
- 編碼相關
網路請求相關
這部分旨在實現需求的前提下儘量減少http請求的開銷,或者減少響應時間
CDN
將第三方的類庫放到CDN上,能夠大幅度減少生產環境中的專案體積,另外CDN能夠實時地根據網路流量和各節點的連線、負載狀況以及到使用者的距離和響應時間等綜合資訊將使用者的請求重新導向離使用者最近的服務節點上
另外因為CDN和伺服器的域名一般不是同一個,可以緩解同一域名併發http請求的數量限制,有效分流以及減少多餘的cookie的傳送(CDN上面的靜態資源請求時不需要攜帶任何cookie)
通俗的來說就是使用CDN會一定程度上提升專案中的靜態檔案的傳輸速度,在vue-cli3中可以通過externals配置項,將第三方的類庫的引用地址從本地指向你提供的CDN地址
externals只適用於ES Module的預設匯入
這裡通過環境變數來判斷生產環境才啟用CDN,除了需要開啟CDN外,你還需要在index.html注入CDN的域名,所以我這裡通過html-webpack-plugin根據cdn域名動態的注入script標籤,同時需要在index.html中通過模版的語法宣告迴圈的陣列和注入的元素
打包前的index.html:
打包後的index.html:
可以看到通過這個外掛可以將cdn域名動態的注入到打包後的index.html中
還有一點要注意的是,externals物件的屬性為你引入包的名字,而屬性值是對應的全域性變數名稱(CDN引入的類庫檔案會自動掛載到window物件下面,而掛載時的屬性名需要去對應的CDN在原始碼中尋找,一般在開頭行都會有宣告,除此之外匯入還有困難的還可以看下這篇部落格webpack externals 深入理解)
這裡還是建議儘量放到公司專用的CDN上,不推薦使用公共的CDN,因為容易掛,生產環境還是以穩定為主吧
合理的快取策略
將長時間不會改變的第三方類庫或者靜態資源設定為強快取,將max-age設定為一個非常長的時間,再將訪問路徑加上雜湊達到雜湊值變了以後保證獲取到最新資源(vue-cli3會自動構建,自己搭建的webpack腳手架需要自行配置contentHash,chunkHash)
CDN上的快取策略,可以看到max-age的值非常大,這樣下次訪問就只會讀取本地磁碟/記憶體中快取的資源:
對於index.html和一些圖片等多媒體資源,可以選擇協商快取(max-age<=0,Last-Modified,ETag),保證返回伺服器最新的資源
一個好的快取策略,有助於減輕伺服器的壓力,並且顯著的提升使用者的體驗
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
對於SPA應用來說,當瀏覽器解析完script指令碼才會生成DOM節點,如果你的專案中沒有使用服務端渲染的話且需要載入一個比較耗時的首屏圖片時,可以考慮將這個首屏圖片放在preload標籤中讓瀏覽器預先請求並載入執行,這樣當script指令碼執行完畢後就會瞬間載入圖片(否則需要等指令碼執行完畢後再向後臺請求圖片)
另外使用preload預載入首屏需要的css樣式也是一個不錯的選擇,類似的庫有critical
沒有使用preload
使用preload
通過Waterfall可以看到這個webp圖片需要等到指令碼載入完之後才回去請求,如果這個圖片比較大就會浪費不必要的時間
在工程中,利用一些preload的webpack外掛可以很方便的給打包後的index.html注入預載入的資源標籤,有興趣的朋友可以試著搜尋一下相關的外掛
prefetch
prefetch可以讓瀏覽器提前載入下個頁面可能會需要的資源,vue-cli3預設會給所有懶載入的路由新增prefetch屬性,這樣可以在你訪問使用到懶載入的路由頁面時能夠獲得更快的載入速度
preload和prefetch的區別在於,preload的資源會和頁面需要的靜態資源並行載入,而prefetch則會等到瀏覽器載入完必要的資源後,在空閒時間載入被標記為prefetch的資源
dns-prefetch
dns-prefetch可以讓瀏覽器提前對域名進行解析,減少DNS查詢的開銷,如果你的靜態資源和後端介面不是同一個伺服器的話,可以將考慮你後端的域名放入link標籤加入dns-prefetch屬性
京東首頁也使用到了dns-prefetch技術
http2
http2從2015年問世以來已經走過了4個年頭,如今在國內也有超過50%的覆蓋率,得益於http2的分幀傳輸,它能夠極大的減少http(s)請求開銷
如果系統首屏同一時間需要載入的靜態資源非常多,但是瀏覽器對同一域名的tcp連線數量是有限制的(chrome為6個)超過規定數量的tcp連線,則必須要等到之前的請求收到響應後才能繼續傳送,而http2則可以在一個tcp連線中併發多個請求沒有限制,在一些網路較差的環境開啟http2效能提升尤為明顯
這裡極力推薦在支援https協議的伺服器中使用http2協議,可以通過web伺服器Nginx配置,或是直接讓伺服器支援http2
nginx開啟http2非常簡單,在nginx.conf中只需在原來監聽的埠後面加上http2就可以了,前提是你的nginx版本不能低於1.95,並且已經開啟了https
listen 443 ssl http2;
複製程式碼
在network中通過protocol可以檢視到當前的資源是通過哪個版本的http協議傳輸的
h2代表http2
構建相關
構建方面通過合理的配置構建工具,達到減少生產環境的程式碼的體積,減少打包時間,縮短頁面載入時間
路由懶載入
傳統的路由元件是通過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搭建的
DllPlugin
當沒有一個穩定的CDN時,使用DllPlugin這個webpack外掛同樣可以將類庫從業務程式碼中分離出去,其原理是預先將類庫檔案打包,隨後建立一個關聯表,在業務程式碼依賴第三方類庫時,會直接去關聯表中獲取已經打包好的類庫檔案。這樣做的好處在於因為業務程式碼會常常需要打包上線,而第三方類庫基本不會改變,所以每次打包可以跳過這些類庫檔案,減少不必要的重複打包
DllPlugin是一個webpack內建的外掛,無需安裝,但是要讓打包後的index.html注入這些打包後第三方類庫,需要額外安裝add-asset-html-webpack-plugin
外掛
當你需要在index.html中注入多個類庫時,需要例項化多次add-asset-html-webpack-plugin
,這裡我們可以利用nodejs的fs模組,遍歷DllPlugin打包後的目錄,根據類庫的數量決定需要生成多少個例項,非常的靈活,具體的配置項可以檢視我底部的連結
合理使用第三方庫
如果專案中有一些日期操作的需求,不妨將目光從moment轉移到day,相對於笨重的moment,它只有2kb,day和moment的api完全一樣,並且中文文件也比較友好
另外對於lodash這類的庫如果只需要部分功能,則只要引入其中一部分,這樣webpack在treeshaking後在生產環境也只會引入這一部分的程式碼
對於UI庫(element-ui)打包後的體積也會非常大,儘量使用按需載入,官方文件上也有詳細教程
element-ui的壓縮後的體積竟然是Vue的十倍
常用的路徑建立檔案別名
給常用的模組路徑建立一個別名是一個不錯的選擇,可以減少模組查詢時耗費的時間,專案越大收益也就越明顯
vue-cli3中的配置和使用方法(webpack鏈式呼叫文件)
使用視覺化工具分析打包後的模組體積
我通過webpack-bundle-analyzer這個外掛在每次打包後能夠更加直觀的分析打包後模組的體積,再對其中比較大的模組進行優化
這是我在優化前的各模組體積:
因為業務需求,要求前端匯出pdf和excel檔案,我這裡引入了xlsx和pdf.js這2個包,但是打包後通過視覺化工具發現光著2個檔案就佔了一半的專案體積,另外elementui和moment也非常的大
這是優化後通過視覺化工具觀察到的各模組體積,通過將這些類庫放到CDN上或者使用dllPlugin將類庫和業務檔案分離,可以看到沒有明顯特別大的模組了
靜態資源優化
這部分旨在減少請求一些圖片資源所造成的影響
圖片懶載入
如果你的系統是一個偏展示的專案需要給使用者展示大量圖片,是否啟用圖片懶載入可能是你需要考慮的一個點,不在使用者視野中的圖片是沒有必要載入的,圖片懶載入通過讓圖片先載入成一張統一的圖片,再給進入使用者視野的圖片替換真正的圖片地址,可以同一時間減少http請求開銷,避免顯示圖片導致的畫面抖動,提高使用者體驗
下面我提供2種圖片懶載入的思路,這2個方案最終都是用將佔位的圖片替換成真正的圖片,然後給img標籤設定一個自定義屬性data-src存放真正的圖片地址,src存放佔點陣圖片的地址
getBoundingClientRect
DOM元素包含一個getBoundingClientRect方法,執行該方法返回當前DOM節點相關的CSS邊框集合
其中有一個top屬性代表當前DOM節點距離瀏覽器視窗頂部的高度,只需判斷top值是否小於當前瀏覽器視窗的高度(window.innerHeight),若小於說明已經進入使用者視野,然後替換為真正的圖片即可
另外使用getBoundingClientRect作圖片懶載入需要注意3點
- 因為需要監聽scroll事件,不停的判斷top的值和瀏覽器高度的關係,請對監聽事件進行函式節流
- 當螢幕首次渲染時,不會觸發scroll事件,請主動呼叫一次事件處理程式,否則若使用者不滾動則首屏的圖片會一直使用懶載入的預設圖片
- 當所有需要懶載入的圖片都被載入完,需要移除事件監聽,避免不必要的記憶體佔用
IntersectionObserver
IntersectionObserver作為一個建構函式,傳入一個回撥函式作為引數,生成一個例項observer,這個例項有一個observe方法用來觀察指定元素是否進入了使用者的可視範圍,隨即觸發傳入建構函式中的回撥函式
同時給回撥函式傳入一個entries的引數,記錄著這個例項觀察的所有元素的一些閾值資訊(物件),其中intersectionRatio屬性表示圖片進入可視範圍的百分比,大於0表示已經有部分進入了使用者視野
此時替換為真實的圖片,並且呼叫例項的unobserve將這個img元素從這個例項的觀察列表的去除
例項程式碼
對懶載入還有迷惑的同學我這裡寫了一個DEMO可以參考一下實現的方式原始碼
結論
這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圖片
這裡需要使用到響應式圖片,HTML提供了picture標籤讓我們可以在不同裝置中使用不同的圖片格式
MDN:
HTML 元素通過包含零或多個
在工程中我們可以這樣使用
picture標籤包裹2個source標籤,一個提供webp圖片,通過srcset屬性讓瀏覽器從上到下選擇可以支援的圖片格式,如果瀏覽器不支援webp圖片會只使用第二個source,會回退到png圖片,如果瀏覽器不支援picture標籤,會使用底部的img標籤,同樣也會生成一個png圖片
picture標籤的瀏覽器支援率,相對於webp要好很多(注意底部的img標籤無論如何都要有,否則就算支援webp圖片也無法渲染出圖片)
壓縮圖片
對於一些png圖片可能質量會非常的高,但是對於Web平臺來說,使用者可能並不care圖片的畫質問題,但是如果載入圖片導致頁面出現卡頓那就顯得得不償失了,我們可以考慮將一些畫質較高的圖片做壓縮處理,我這裡使用tinypng幫我壓縮圖片,同樣能夠保證在肉眼幾乎分辨不出區別的情況下,提供一個體積較小的圖片,如果有其他好的壓縮軟體也可以推薦給我
編碼相關
編碼這方面主要是減少對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:
這樣在建立一個新的模組時,不需要在index.js中用import引入,在modules中再宣告一遍了
全域性元件和svg圖示:
避免了每建立一個全域性元件都需要引入,在呼叫一次Vue.component的過程,而當載入到Svg元件會自動掃描icons資料夾,將所有svg圖示匯入進來
有興趣的朋友可以看看我另一篇介紹這個api的部落格
原始碼
部分優化的方案放在我的github上,有興趣可以看看
寫在後面
本人為18年畢業本科生,座標上海,1年的前端開發經驗,如果有比較好的網際網路企業內推機會的話,希望能在評論區能留下您的聯絡方式或者聯絡我的郵箱1996yeyan@gmail.com,非常感謝~
下篇在這裡: