淺談使用 Vue 構建前端 10W+ 代碼的單頁面應用(二)

淺談使用 Vue 構建前端 10W+ 代碼的單頁面應用(二)

69 人贊了文章

開始之前

由於字數限制,上一篇中我們已經說完了前四個小節,分別是:

  • ① 單頁面,多頁面
  • ② 目錄結構
  • ③ 通用組件
  • ④ 全局配置,插件與攔截器

這篇文章繼續:

  • ⑤ 路由配置與懶載入
  • ⑥ Service 服務層
  • ⑦ 狀態管理與視圖拆分

上篇文章地址:

Zero:淺談使用 Vue 構建前端 10W+ 代碼的單頁面應用(一)?

zhuanlan.zhihu.com圖標

如果想一次性看完,可以點下方鏈接。

淺談使用 Vue 構建前端 10w+ 代碼量的單頁面應用開發底層?

juejin.im

我們繼續討論。


⑤ 路由配置與懶載入

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 是否有對應介面,當業務量上來的時候,也肯定會有人出現找不到,或者找起來比較費勁,這時候我們完全可以在 請求攔截器中,把當前請求的 urlapi 中的請求做下判斷,如果有重複介面請求路徑,則提醒開發者已經配置相關請求,根據情況是否進行二次配置即可。

最終我們可以拓展 Service 層的各個功能:

基礎- api非同步與後端交互- const常量枚舉- storeVuex 狀態管理

拓展- localStorage:本地數據,稍微封裝下,支持存取對象即可

- monitor監控功能,自定義搜集策略,調用 api 中的介面發送

- beacon打點功能,自定義搜集策略,調用 api 中的介面發送

- ...

constlocalStoragemonitorbeacon 根據業務自行拓展暴露給業務使用即可,思想也是一樣的,下面著重說下 store(Vuex)

插一句:如果看到這裡沒感覺不妥的話,想想上面 plugins/api.js 有沒有用單例模式?該不該用?


⑦ 狀態管理與視圖拆分

Vuex 源碼分析可以看我之前寫的文章。

我們是不是真的需要狀態管理?

答案是否定的,就算你的項目達到 10 萬行代碼,那也並不意味著你必須使用 Vuex,應該由業務場景決定。

業務場景

  1. 第一類項目:業務/視圖複雜度不高,不建議使用 Vuex,會帶來開發與維護的成本,使用簡單的 vbus 做好命名空間,來解耦即可。

let vbus = new Vue()vbus.$on(print.hello, () => { console.log(hello)})vbus.$emit(print.hello)

  1. 第二類項目:類似多人協作項目管理有道雲筆記網易雲音樂微信網頁版/桌面版應用,功能集中,空間利用率高,實時交互的項目,無疑 Vuex 是較好的選擇。這類應用中我們可以直接抽離業務領域模型

store├── index.js ├── actions.js // 根級別 action├── mutations.js // 根級別 mutation└── modules ├── user.js // 用戶模塊 ├── products.js // 產品模塊 ├── order.js // 訂單模塊 └── ...

當然對於這類項目,vuex 或許不是最好的選擇,有興趣的同學可以學習下 rxjs

  1. 第三類項目:後台系統或者頁面之間業務耦合不高的項目,這類項目是佔比應該是很大的,我們思考下這類項目:

全局共享狀態不多,但是難免在某個模塊中會有複雜度較高的功能(客服系統,實時聊天,多人協作功能等),這時候如果為了項目的可管理性,我們也在 store 中進行管理,隨著項目的迭代我們不難遇到這樣的情況:

store/ ... modules/ b.js ...views/ ... a/ b.js ...

  • 試想下有幾十個 module,對應這邊上百個業務模塊,開發者在兩個平級目錄之間調試與開發的成本是巨大的。
  • 這些 module 可以在項目中任一一個地方被訪問,但往往他們都是冗餘的,除了引用的功能模塊之外,基本不會再有其他模塊引用他。
  • 項目的可維護程度會隨著項目增大而增大。

如何解決第三類項目的 store 使用問題?

先梳理我們的目標:

- 項目中模塊可以自定決定是否使用 Vuex。(漸進增強)

- 從有狀態管理的模塊,跳轉沒有的模塊,我們不想把之前的狀態掛載到 store 上,想提高運行效率。(冗餘)

- 讓這類項目的狀態管理變的更加可維護。(開發成本/溝通成本)

實現

我們藉助 Vuex 提供的 registerModuleunregisterModule 一併解決這些問題,我們在 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 中直接包含了 gettersmutationsstate,我們在 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

這樣就解決了開頭的三個問題,不同開發者在開發頁面的時候,可以根據頁面特性,漸進增強的選擇某種開發形式。


其他

這裡簡單列舉下其他方面,需要自行根據項目深入和使用。

打包,構建

這裡網上已經有很多優化方法:dllhappypack多線程打包等,但隨著項目的代碼量級,每次 dev 保存的時候編譯的速度也是會愈來愈慢的,而一過慢的時候我們就不得不進行拆分,這是肯定的,而在拆分之前儘可能容納更多的可維護的代碼,有幾個可以嘗試和規避的點:

1. 優化項目流程:這個點看起來好像沒什麼用,但改變卻是最直觀的,頁面/業務上的化簡為繁會直接體現到代碼上,同時也會增大項目的可維護,可拓展性等。

2. 減少項目文件層級縱向深度。

3. 減少無用業務代碼,避免使用無用或者過大依賴(類似 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/eros?

github.com圖標EROS?

bmfe.github.io

如果想看有哪些項目在使用,我們有部分產品截圖:

https://github.com/bmfe/eros#awesome-eros?

github.com

混合應用相關文章:

淺談混合應用的演進 - 掘金?

juejin.im

深入了解 Weex - 掘金?

juejin.im

(逃~)

推薦閱讀:

TAG:前端工程師 | 前端開發 | Vuejs |