淺談使用 Vue 構建前端 10W+ 代碼的單頁面應用(二)
69 人贊了文章
開始之前
由於字數限制,上一篇中我們已經說完了前四個小節,分別是:
- ① 單頁面,多頁面
- ② 目錄結構
- ③ 通用組件
- ④ 全局配置,插件與攔截器
這篇文章繼續:
- ⑤ 路由配置與懶載入
- ⑥ Service 服務層
- ⑦ 狀態管理與視圖拆分
上篇文章地址:
Zero:淺談使用 Vue 構建前端 10W+ 代碼的單頁面應用(一)如果想一次性看完,可以點下方鏈接。
淺談使用 Vue 構建前端 10w+ 代碼量的單頁面應用開發底層
我們繼續討論。
⑤ 路由配置與懶載入
directives
裡面沒什麼可說的,不過很多難題都可以通過他來解決,要時刻記住,我們可以再指令裡面操作虛擬 DOM。
路由配置
而我們根據自己的業務性質,最終根據業務流程來拆分配置:
routes├── index.js // 入口文件├── common.js // 公共路由,登錄,提示頁等├── account.js // 賬戶流程├── register.js // 挂號流程└── ...
最終通過 index.js 暴露出去給 plugins/router
實例使用,這裡的拆分配置有兩個注意的地方:
- 需要根據自己業務性質來決定,有的項目可能適合
業務線
劃分,有的項目更適合以功能
劃分。 - 在多人協作過程中,儘可能避免衝突,或者減少衝突。
懶載入
文章開頭說到單頁面靜態資源過大,首次打開/每次版本升級
後都會較慢,可以用懶載入
來拆分靜態資源,減少白屏時間,但開頭也說到懶載入
也有待商榷的地方:
- 如果非同步載入較多的組件,會給靜態資源伺服器/ CDN 帶來更大的訪問壓力的同時,如果當多個非同步組件都被修改,造成版本號的變動,發布的時候會大大增加 CDN 被擊穿的風險。
- 懶載入首次載入未被緩存的非同步組件白屏的問題,造成用戶體驗不好。
- 非同步載入通用組件,會在頁面可能會在網路延時的情況下參差不齊的展示出來等。
這就需要我們根據項目情況在空間和時間
上做一些權衡。
以下幾點可以作為簡單的參考:
- 對於訪問量可控的項目,如公司後台管理系統
中,可以以操作 view 為單位進行非同步載入,通用組件全部同步載入的方式。- 對於一些複雜度較高,實時度較高的應用類型,可採用按功能模塊拆分
進行非同步組件載入。- 如果項目想保證比較高的完整性和體驗,迭代頻率可控,不太關心首次載入時間的話,可按需使用非同步載入或者直接不使用。
打包出來的 main.js 的大小,絕大部分都是在路由中引入的並註冊的視圖組件。
⑥ Service 服務層
服務層作為項目中的另一個核心之一,「自古以來」都是大家比較關心的地方。
不知道你是否看到過如下組織代碼方式:
views/ pay/ index.vue service.js components/ a.vue b.vue
在 service.js
中寫入編寫數據來源
export const CONFIAG = { apple: 蘋果, banana: 香蕉}// ...// ① 處理業務邏輯,還彈窗export function getBInfo ({name = , id = }) { return this.$ajax.get(/api/info, { name, id }).then({age} => { this.$modal.show({ content: age }) })}// ② 不處理業務,僅僅寫請求方法export function getAInfo ({name = , id = }) { return this.$ajax.get(/api/info, { name, id })}...
簡單分析:
- ① 就不多說了,拆分的不夠單純,當做二次開發的時候,你還得去找這彈窗到底哪裡出來的。- ② 看起來很美好,不摻雜業務邏輯,但不知道你與沒遇到過這樣情況,經常會有其他業務需要用到一樣的枚舉,請求一樣的介面,而開發其他業務的同學並不知道你在這裡有一份數據源,最終造成的結果就是數據源的代碼到處冗餘。我相信②在絕大多數項目中都能看到。
那麼我們的目的就很明顯了,解決冗餘,方便使用,我們把枚舉和請求介面的方法,通過插件,掛載到一個大對象上,注入 Vue 原型,方面業務使用即可。
目錄層級(僅供參考)
service├── api ├── index.js // 入口文件 ├── order.js // 訂單相關介面配置 └── ...├── const ├── index.js // 入口文件 ├── order.js // 訂單常量介面配置 └── ...├── store // vuex 狀態管理├── expands // 拓展 ├── monitor.js // 監控 ├── beacon.js // 打點 ├── localstorage.js // 本地存儲 └── ... // 按需拓展└── ...
抽離模型
首先抽離請求介面模型,可按照領域模型抽離
(service/api/index.js
):
{ user: [{ name: info, method: GET, desc: 測試介面1, path: /api/info, mockPath: /api/info, params: { a: 1, b: 2 } }, { name: info2, method: GET, desc: 測試介面2, path: /api/info2, mockPath: /api/info2, params: { a: 1, b: 2, b: 3 } }], order: [{ name: change, method: POST, desc: 訂單變更, path: /api/order/change, mockPath: /api/order/change, params: { type: SUCCESS } }] ...}
定製下需要的幾個功能:
- 請求參數自動截取。
- 請求參數不傳,則發送默認配置參數。
- 得需要命名空間。
- 通過全局配置開啟調試模式。
- 通過全局配置來控制走本地 mock 還是線上介面等。
插件編寫
定製好功能,開始編寫簡單的 plugins/api.js
插件:
import axios from ./axiosimport _pick from lodash/pickimport _assign from lodash/assignimport _isEmpty from lodash/isEmptyimport { assert } from Utils/toolsimport { API_DEFAULT_CONFIG } from Configimport API_CONFIG from Service/apiclass MakeApi { constructor(options) { this.api = {} this.apiBuilder(options) } apiBuilder({ sep = |, config = {}, mock = false, debug = false, mockBaseURL = }) { Object.keys(config).map(namespace => { this._apiSingleBuilder({ namespace, mock, mockBaseURL, sep, debug, config: config[namespace] }) }) } _apiSingleBuilder({ namespace, sep = |, config = {}, mock = false, debug = false, mockBaseURL = }) { config.forEach( api => { const {name, desc, params, method, path, mockPath } = api let apiname = `${namespace}${sep}${name}`,// 命名空間 url = mock ? mockPath : path,//控制走 mock 還是線上 baseURL = mock && mockBaseURL // 通過全局配置開啟調試模式。 debug && console.info(`調用服務層介面${apiname},介面描述為${desc}`) debug && assert(name, `${apiUrl} :介面name屬性不能為空`) debug && assert(apiUrl.indexOf(/) === 0, `${apiUrl} :介面路徑path,首字元應為/`) Object.defineProperty(this.api, `${namespace}${sep}${name}`, { value(outerParams, outerOptions) { // 請求參數自動截取。 // 請求參數不穿則發送默認配置參數。 let _data = _isEmpty(outerParams) ? params : _pick(_assign({}, params, outerParams), Object.keys(params)) return axios(_normoalize(_assign({ url, desc, baseURL, method }, outerOptions), _data)) } }) }) } }function _normoalize(options, data) { // 這裡可以做大小寫轉換,也可以做其他類型 RESTFUl 的兼容 if (options.method === POST) { options.data = data } else if (options.method === GET) { options.params = data } return options} // 注入模型和全局配置,並暴露出去export default new MakeApi({ config: API_CONFIG, ...API_DEFAULT_CONFIG})[api]
掛載到 Vue 原型
上,上文有說到,通過 plugins/inject.js
import api from ./apiexport default { install: (Vue, options) => { Vue.prototype.$api = api // 需要掛載的都放在這裡 }}
使用
這樣我們可以在業務中
愉快的使用業務層代碼:
// .vue 中export default { methods: { test() { this.$api[order/info]({ a: 1, b: 2 }) } }}
即使在業務之外
也可以使用:
import api from Plugins/apiapi[order/info]({ a: 1, b: 2})
當然對於
運行效率要求高
的項目中還請慎用此方式。
一般來說,多人協作時候大家都可以先看 api
是否有對應介面,當業務量上來的時候,也肯定會有人出現找不到,或者找起來比較費勁,這時候我們完全可以在 請求攔截器中,把當前請求的 url
和 api
中的請求做下判斷,如果有重複介面請求路徑,則提醒開發者已經配置相關請求,根據情況是否進行二次配置即可。
最終我們可以拓展 Service 層的各個功能:
基礎- api:非同步與後端交互
- const:常量枚舉
- store:Vuex
狀態管理
拓展- localStorage:本地數據,稍微封裝下,支持存取對象即可
- monitor:監控
功能,自定義搜集策略,調用 api
中的介面發送- beacon:打點
功能,自定義搜集策略,調用 api
中的介面發送
- ...
const
,localStorage
,monitor
和 beacon
根據業務自行拓展暴露給業務使用即可,思想也是一樣的,下面著重說下 store(Vuex)
。
插一句:如果看到這裡沒感覺不妥的話,想想上面
plugins/api.js
有沒有用單例模式
?該不該用?
⑦ 狀態管理與視圖拆分
Vuex 源碼分析可以看我之前寫的文章。
我們是不是真的需要狀態管理?
答案是否定的,就算你的項目達到 10 萬行代碼,那也並不意味著你必須使用 Vuex,應該由業務場景決定。
業務場景
- 第一類項目:業務/視圖複雜度不高,不建議使用 Vuex,會帶來開發與維護的成本,使用簡單的
vbus
做好命名空間,來解耦即可。
let vbus = new Vue()vbus.$on(print.hello, () => { console.log(hello)})vbus.$emit(print.hello)
- 第二類項目:類似
多人協作項目管理
,有道雲筆記
,網易雲音樂
,微信網頁版/桌面版
等應用,功能集中,空間利用率高,實時交互的項目,無疑Vuex 是較好的選擇
。這類應用中我們可以直接抽離業務領域模型
:
store├── index.js ├── actions.js // 根級別 action├── mutations.js // 根級別 mutation└── modules ├── user.js // 用戶模塊 ├── products.js // 產品模塊 ├── order.js // 訂單模塊 └── ...
當然對於這類項目,vuex
或許不是最好的選擇,有興趣的同學可以學習下 rxjs
。
- 第三類項目:
後台系統
或者頁面之間業務耦合不高的項目
,這類項目是佔比應該是很大的,我們思考下這類項目:
全局共享狀態不多,但是難免在某個模塊中會有複雜度較高的功能(客服系統,實時聊天,多人協作功能等),這時候如果為了項目的可管理性,我們也在 store
中進行管理,隨著項目的迭代我們不難遇到這樣的情況:
store/ ... modules/ b.js ...views/ ... a/ b.js ...
- 試想下有幾十個 module,對應這邊上百個業務模塊,開發者在兩個平級目錄之間調試與開發的成本是巨大的。
- 這些 module 可以在項目中任一一個地方被訪問,但往往他們都是冗餘的,除了引用的功能模塊之外,基本不會再有其他模塊引用他。
- 項目的可維護程度會隨著項目增大而增大。
如何解決第三類項目的 store 使用問題?
先梳理我們的目標:
- 項目中模塊可以自定決定是否使用 Vuex。(漸進增強)- 從有狀態管理的模塊,跳轉沒有的模塊,我們不想把之前的狀態掛載到store
上,想提高運行效率。(冗餘)- 讓這類項目的狀態管理變的更加可維護。(開發成本/溝通成本)
實現
我們藉助 Vuex 提供的 registerModule
和 unregisterModule
一併解決這些問題,我們在 service/store
中放入全局共享的狀態:
service/ store/ index.js actions.js mutations.js getters.js state.js
一般這類項目全局狀態不多,如果多了拆分 module 即可。
編寫插件生成 store 實例
:
import Vue from vueimport Vuex from vueximport {VUEX_DEFAULT_CONFIG} from Configimport commonStore from Service/storeVue.use(Vuex)export default new Vuex.Store({ ...commonStore, ...VUEX_DEFAULT_CONFIG})
對一個需要狀態管理頁面或者模塊進行分層:
views/ pageA/ index.vue components/ a.vue b.vue ... children/ childrenA.vue childrenB.vue ... store/ index.js actions.js moduleA.js moduleB.js
module 中直接包含了 getters
,mutations
,state
,我們在 store/index.js
中做文章:
import Store from Plugins/storeimport actions from ./actions.jsimport moduleA from ./moduleA.jsimport moduleB from ./moduleB.jsexport default { install() { Store.registerModule([pageA], { actions, modules: { moduleA, moduleB }, namespaced: true }) }, uninstall() { Store.unregisterModule([pageA]) }}
最終在 index.vue
中引入使用, 在頁面跳轉之前註冊這些狀態和管理狀態的規則,在路由離開之前,先卸載這些狀態和管理狀態的規則:
import store from ./storeimport {mapGetters} from vuexexport default { computed: { ...mapGetters(pageA, [aaa, bbb, ccc]) }, beforeRouterEnter(to, from, next) { store.install() next() }, beforeRouterLeave(to, from, next) { store.uninstall() next() }}
當然如果你的狀態要共享到全局,就不執行 uninstall
。
這樣就解決了開頭的三個問題,不同開發者在開發頁面的時候,可以根據頁面特性,漸進增強的選擇某種開發形式。
其他
這裡簡單列舉下其他方面,需要自行根據項目深入和使用。
打包,構建
這裡網上已經有很多優化方法:dll
,happypack
,多線程打包
等,但隨著項目的代碼量級,每次 dev 保存的時候編譯的速度也是會愈來愈慢的,而一過慢的時候我們就不得不進行拆分,這是肯定的,而在拆分之前儘可能容納更多的可維護的代碼,有幾個可以嘗試和規避的點:
moment.js
這樣的庫)等。
樣式
- 儘可能抽離各個模塊,讓整個樣式底層更加靈活,同時也應該儘可能的減少冗餘。
- 如果使用的
sass
的話,善用%placeholder
減少無用代碼打包進來。
MPA 應用
中樣式冗餘過大,%placeholder
也會給你帶來幫助。
Mock
很多大公司都有自己的 mock 平台
,當前後端定好介面格式,放入生成對應 mock api
,如果沒有 mock 平台,那就找相對好用的工具如 json-server
等。
代碼規範
請強制使用 eslint
,掛在 git 的鉤子上。定期 diff 代碼,定期培訓等。
TypeScript
非常建議用 TS 編寫項目,可能寫 .vue 有些彆扭,這樣前端的大部分錯誤在編譯時解決,同時也能提高瀏覽器運行時效率,可能減少 re-optimize
階段時間等。
測試
這也是項目非常重要的一點,如果你的項目還未使用一些測試工具,請儘快接入,這裡不過多贅述。
拆分系統
當項目到達到一定業務量級時,由於項目中的模塊過多,新同學維護成本,開發成本都會直線上升,不得不拆分項目,後續會分享出來我們 ToB
項目在拆分系統中的簡單實踐。
最後
時下有各種成熟的方案,這裡只是一個簡單的構建分享,裡面依賴的版本都是我們穩定下來的版本,需要根據自己實際情況進行升級。
項目底層構建往往會成為前端忽略的地方,我們既要從一個大局觀來看待一個項目或者整條業務線,又要對每一行代碼精益求精,對開發體驗不斷優化,慢慢累積後才能更好的應對未知的變化。
- 關於我,可以叫我 Zero,附上 Git 地址
- 文章標題圖片地址
最後的最後,請允許我打一波小小的廣告
EROS
如果前端同學想嘗試使用 Vue
開發 App
,或者熟悉 weex
開發的同學,可以來嘗試使用我們的開源解決方案 eros
,雖然沒做過什麼廣告,但不完全統計,50 個在線 APP 還是有的
,期待你的加入。
EROS 相關 地址:
bmfe/erosEROS
如果想看有哪些項目在使用,我們有部分產品截圖:
https://github.com/bmfe/eros#awesome-eros混合應用相關文章:
淺談混合應用的演進 - 掘金深入了解 Weex - 掘金(逃~)
推薦閱讀: