在筆者所在的前端開發團隊中,採用前後端分離方案是在整個業務線穩定後進行的。業務前期主要採用後端套模板的方式,現階段是採用基於Vue
的單頁開發模式。
這會出現一種情形,產品在不斷迭代過程中,由於之前線上前端程式碼並非工程化專案,後面新需求多是另起Vue
專案來進行編碼上線,前端在整個業務線上沒有統一的工程,專案過多分佈散亂並且不易優化管理。(專案指根據新需求建立的專案程式碼,工程指一套程式碼下包含多個專案。後文以此約定。)針對這種情況下我們做出一些嘗試,將目前存在的多個專案整合成一個工程,統一入口。
當然我們更希望整合成統一工程後可以實現後續新需求接入無痛化。下文則主要圍繞專案整合過程中遇到的些許問題,分享一些可行解決方案。簡要從幾個方向,程式碼層級化分、元件處理、路由處理、資料狀態維護、其他優化等來簡述。
1. 程式碼層級劃分
如何合理劃分整個工程目錄?
在前端開發中我們首先會面對如何將程式碼及靜態資源劃分目錄層級放置問題。比如在前端洪荒時代通常會以img、css、js
命名不同目錄。那麼在結合Vue
相關技術棧以及多專案整合場景下,如何劃分目錄才能保證程式碼層次合理呢?
談到這個問題的時候,我們可以首先思考下整個工程具體需要哪些相關功能。在拋開具體原始碼內部結構情況下,主要有構建指令碼、構建配置、Mock資料、專案文件、專案原始碼等。在不結合具體技術棧的情況下,這也是前端在工程化方面大致目錄。
具體到專案原始碼內部目錄結構,按照不同功能模組,主要做了以下層次劃分:
- 靜態資源按專案拆分目錄
- 路由元件按專案拆分目錄
- 子元件按專案及所屬父路由元件拆分目錄
- 路由層按專案拆分不同檔案
- 資料層按專案拆分不同目錄
- mixins混合程式碼拆分不同檔案
- 配置層按專案拆分不同檔案
以專案src原始碼下元件相關目錄為例,如下圖所示:
├── pages // 路由元件目錄
│ ├── README.md
│ ├── base // 全域性基礎路由元件目錄
│ │ └── SuccessPage.vue
│ ├── period_process // 專案A路由級別元件目錄
│ │ ├── ChooseTime.vue // 專案A選擇時間路由元件
│ │ ├── xxx
│ └── period_suborder // 專案B路由級別元件目錄
│ └── xxx
|
├── components // 子元件目錄
│ ├── README.md
│ ├── base // 全域性基礎子元件目錄
│ │ ├── AddressInfo.vue
│ │ └── xxx
│ ├── period_process // 專案A子元件目錄
│ │ ├── base // 專案基礎A子元件目錄
│ │ │ ├── Picker.vue
│ │ │ └── xxx
│ │ ├── choose_time // 專案A選擇時間路由元件子元件目錄
│ │ │ ├── Cleaner.vue
│ │ │ └── xxx
│ │ ├── xxx
│ └── period_suborder // 專案B子元件目錄
│ └── xxx
複製程式碼
當然專案目錄結構不是一層不變的,可根據業務場景及技術棧靈活處理。但原則上避免一個檔案從頭寫到尾出現綿長程式碼情況,造成後續迭代可閱讀性差、不好維護問題。良好的程式碼層次可以簡述為將相同功能模組聚合同一目錄並拆分出獨立檔案。
2. 元件維護相關
如何控制元件拆分粒度?
談及元件部分,元件拆分粒度永遠是一個繞不開的話題。首先大的方面分為路由元件(頁面元件)和相應頁面子元件。路由元件為配置路由時元件,元件內部拆分不同子元件進行引用。路由元件及子元件分別拆分相應業務元件和基礎元件。
頁面元件拆分過程中,我們採用將相關功能模組程式碼拆分為子元件。將頁面劃分若干子元件(功能模組),每個子元件完成一個子功能。如下圖所示:
如何方便業務元件提升為基礎元件?
在元件劃分時,我們將子元件拆分業務元件和基礎元件。在專案整合的過程中,遇到業務子元件因被後續多個專案使用需要提升為基礎元件情況。但起初編碼過程中僅考慮作為業務子元件,內部資料來源多依賴Vuex
,在將其提升為基礎元件時需要做大量工作將資料來源改為props
物件,修改資料來源操作改為emit
觸發事件機制。
這裡更好的處理方式則是希望在編寫子元件時,內部資料來源儘可能依賴於父元件傳遞的props
物件。將需要修改資料來源行為通過emit
方式提升至父元件內操作。
3. 路由相關處理
如何處理多入口路由配置?
單頁web應用在處理不同view時提出前端router概念。對應單一專案需求時,我們可以很從容設定預設路由入口來解決。但是對於多入口的多專案工程,則需要一些思量。同時由於筆者所處公司APP在處理URL跳轉時預設不帶hash,那麼在URL訪問上則沒有辦法通過附加hash路由來跳轉相應檢視。
我們採用的解決方式是,在URl訪問時不攜帶hash,後端同學會在不同專案的入口html
檔案中放置PAGE_TYPE
變數,前端根據PAGE_TYPE
變數跳轉相應路由元件。PAGE_TYPE
變數對應於當前待訪問的路由。通過結合後端在頁面中渲染的路由標誌量,解決訪問時路徑問題。
在此基礎上,還需要考慮頁面重新整理以及跳轉外鏈後退情況。在非入口路由頁面重新整理會根據PAGE_TYPE
變數重置進入入口路由頁。這種情況需要判斷當前URL是否存在hash路由標誌,優先獲取當前連結hash值進行跳轉。具體虛擬碼如下所示:
let routeType = window.PAGE_TYPE // 獲取初始化路由變數
let routeName = getHashRouter() // 獲取當前路由名稱
if (routeName) {
router.push({path: `/${routeName}`})
} else {
switch (parseInt(routeType)) {
case 0:
router.push({path: '/index'})
break
case 1:
router.push({path: '/projectB'})
break
default:
router.push({path: '/index'}) // 預設路由入口
}
}
複製程式碼
如何優化非同步元件載入?
另外一個值得考慮的問題是,隨著專案不斷增加在打包構建應用時,js檔案會變得越來越大,影響頁面載入速度。將非入口路由元件非同步載入是一種比較高效的解決方案。結合Vue
的非同步元件和Webpack
的程式碼分割(code splitting
)特性可以輕鬆實現路由元件懶載入。具體語法可參考vue-router
官方文件。
非入口路由元件非同步載入,可減少首次載入JS檔案大小。但隨之而來的問題是,假如使用者選擇點選按鈕進行路由跳轉時,需要非同步獲取JS檔案,等待非同步元件載入完成後再跳轉。特別在跳轉之前還需要非同步呼叫介面校驗,使用者等待時間無疑增加。我們更希望可以在使用者空閒時間預載入後續跳轉的非同步元件。
具體預載入操作可以在元件生命週期mounted
中手動觸發後續非同步元件載入。還可將預載入操作聚合成mixin
檔案,註冊成全域性mixin
。埋點資料顯示後續路由元件跳轉時間約在300ms左右,屬於秒開範圍。示例如下:
mounted () {
// 預載入後續非同步路由元件
import(/* webpackChunkName: 'chooseTime' */ '@/pages/period_process/ChooseTime.vue')
// 或使用 webpack 特定的 require.ensure 語法
// require.ensure(['@/pages/period_process/ChooseTime.vue'], null, 'chooseTime')
}
複製程式碼
將非入口路由元件非同步載入,並在使用者空閒時間實現預載入接下來路由元件。降低使用者等待時間,使用者體驗自然也就更好。
如何優雅處理路由跳轉效果?
這裡說到的路由跳轉效果指在不同路由元件跳轉時所新增的過渡效果。Vue
預設的router-view
跳轉不存在動效,略感生硬。為router-view
新增transition
是不錯的選擇。
不過在處理transition
過程中,起初是將過渡效果新增至路由元件根節點上,但在某些安卓機型下跳轉會出現明顯的閃屏現象。解決方式是將transition
元件移至router-view
元件外統一處理。
4. 資料狀態維護
如何維護專案中的資料狀態?
可以預料到的資料互動行為主要有:
- 父子元件資料共享
- 兄弟元件資料共享
- 使用者行為資料儲存
- 後端介面資料快取
另外還希望所有資料層model和非同步介面可以抽取進行統一維護,為此我們引入了Vuex
。
Vuex
為Vue
應用程式的狀態管理模式,採用集中式儲存管理應用的所有元件狀態。未引入Vuex
下常見的問題多個檢視依賴與同一狀態,多個檢視需要變更同一狀態。兩種情形下多通過元件間引數傳遞或採用事件同步狀態。引入Vuex
後,將元件共享狀態抽取成單例模式管理。定義並約定遵守一定的狀態管理的規則,程式碼結構化更清晰更容易維護。當然如果不開發複雜單頁應用,使用Vuex
可能是繁瑣冗餘。
除此我們還需要考慮的是,由於Vuex
資料狀態是儲存在JS變數中的,當頁面重新整理時整個應用狀態會全部丟失。需要在state、mutations
讀取、儲存中新增本地持久化操作。封裝本地持久化儲存層Cache.js
,可選sessionStorage、localStorage、indexedDB
儲存方式。依據業務情形在mutations
檔案中選擇相應方式做本地持久化操作。
Vuex + Cache
方式做資料狀態維護,並將相應程式碼拆分獨立資料層,減少與業務程式碼耦合程度。業務流程中只需要通過mapState、mapActions
方式獲取相應資料狀態或更新相應資料狀態。
如何保證多專案中資料狀態不被汙染?
多專案整合在一起,各個專案中state
難免會出現變數名稱衝突,多個狀態變數相互汙染現象。同時actions、mutations
操作也都暴露在全域性狀態下。隨著專案不斷整合加入,這將會成為一個定時炸彈。
很開森的是Vuex
有相應的解決方案。Vuex
中提出模組(module)和名稱空間(namespace)概念。Vuex
允許我們將store
分割成模組,並且可通過新增namespaced: true
將其變為名稱空間模組。多個專案使用各自state
物件,資料耦合程度及被汙染的可能性降低。
結合Vuex
還可以做哪些好玩的事情?
按照Vuex
所約定資料狀態儲存以及修改的規則,很容易將資料層按照相應層次拆分出來。這時非同步請求則可以進行統一聚合,封裝成全域性狀態下的fetchData
action方法,在統一的非同步請求中我們做了以下工作:
- 防止重複提交
- 網路異常統一處理
- 快取介面資料(可選)
- 自動處理介面返回資料code不為0情況(可選)
- 介面請求時間過長顯示loading狀態(可選)
- 自動上報介面請求時間(可選)
如此下來,我們業務元件不再需要考慮非同步請求中的重複提交、網路異常等事情。單Vue
檔案元件中中僅聚焦業程邏輯的實現。
5. 其他優化
如何拆分元件中與業務無關邏輯程式碼?
解釋下,這裡無關邏輯是指與業務關聯性密切性不大的程式碼。比如,前端做router
跳轉不同檢視時,需要考慮跳轉到相應檢視下設定當前檢視的頁面title
或傳送當前檢視的pv
埋點。每個路由級別元件幾乎都會寫類似於這些與業務邏輯無關的程式碼。那麼如何提取拆分呢?
這裡提一個混合(mixin
)概念,mixin
是一種分發元件中可複用功能的靈活方式。通過mixins
或Vue.mixin()
語法接受一個混合物件或混合物件陣列。混合物件可以包含任意元件選項,另外同名鉤子函式將混合為陣列,混合物件的鉤子將在元件鉤子前呼叫。
結合Vue
中的mixin
語法,我們可以很容易做到將基礎與業務無關程式碼拆分成不同mixin
檔案,通過Vue.mixin()
註冊為全域性混合物件。在不同mixin
檔案編寫相應的元件生命週期函式做預載入元件、設定頁面title、傳送PV等操作。
如何將雪碧圖區分專案進行合併?
一言簡述之,在我們整合專案過程中發現之前的構建指令碼在處理雪碧圖合併的過程中將多個專案圖片合併成一張。這就會出現使用者訪問專案A過程中,會將整個工程的雪碧圖資源下載,佔用流量並造成資源浪費。那如何分專案合併雪碧圖呢?
這裡我們使用webpack-spritesmith
模組做雪碧圖合併。通過例項化webpack-spritesmith
物件,傳遞自定義模板,分別構建出css、scss
檔案(具體可參考官方文件)。其實分專案構建不同雪碧圖與webpack
構建多個html
頁面做法類似。在webpack
的plugins
配置陣列中push
多個webpack-spritesmith
例項,不同例項分別構建不同專案下的雪碧圖。這時不同專案業務元件便可分別引用相應的雪碧圖樣式檔案。
總結
將多個相關需求的專案整合到單一工程的過程中,從前期詳細設計到後面多個專案接入上線,一直在踩坑填坑。本文也是針對一些我們遇到的比較典型的問題拿出來分享。
在前端越來越追求工程化的今天,如何在工程化的基礎上將專案做到層次清晰、程式碼簡潔、耦合更低、效能更優則是我們要去思考的方向。
最後本文主要聚焦於基於Vue
的多個相關專案整合統一工程實踐解決方案。
倉促成文,如有錯誤,措辭不當,敬請斧正 :)