摘要:其實就是我們開始掛載上去的我們在這里出去,我們就可以在回調里面只處理我們的業務邏輯,而其他如斷網超時服務器出錯等均通過攔截器進行統一處理。
開始之前
隨著業務的不斷累積,目前我們 ToC 端主要項目,除去 node_modules, build 配置文件,dist 靜態資源文件的代碼量為 137521 行,后臺管理系統下各個子應用代碼,除去依賴等文件的總行數也達到 100萬 多一點。
代碼量意味不了什么,只能證明模塊很多,但相同兩個項目,在運行時性能相同情況下,你的 10 萬行代碼能容納并維護 150 個模塊,并且開發順暢,我的項目中 10 萬行代碼卻只能容納 100 個模塊,添加功能也好,維護起來也較為繁瑣,這就很值得思考。
本文會在主要描述以 Vue 技術棧為技術主體,ToC 端項目業務主體,在構建過程中,遇到或者總結的點(也會提及一些 ToB 項目的場景),可能并不適合你的業務場景(僅供參考),我會盡可能多的描述問題與其中的思考,最大可能的幫助到需要的同學,也辛苦開發者發現問題或者不合理/不正確的地方及時向我反饋,會盡快修改,歡迎有更好的實現方式來 pr。
vue-develop-template 完善中,可以運行
可以參考螞蟻金服數據體驗技術團隊編寫的文章:
如何管理好10萬行代碼的前端單頁面應用
本文并不是基于上面文章寫的,不過當時在看到他們文章之后覺得有相似的地方,相較于這篇文章,本文可能會枯燥些,會有大量代碼,同學可以直接用上倉庫看。
① 單頁面,多頁面首先要思考我們的項目最終的構建主體是單頁面,還是多頁面,還是單頁 + 多頁,通過他們的優缺點來分析:
單頁面(SPA)
優點:體驗好,路由之間跳轉流程,可定制轉場動畫,使用了懶加載可有效減少首頁白屏時間,相較于多頁面減少了用戶訪問靜態資源服務器的次數等。
缺點:初始會加載較大的靜態資源,并且隨著業務增長會越來越大,懶加載也有他的弊端,不做特殊處理不利于 SEO 等。
多頁面(MPA):
優點:對搜索引擎友好,開發難度較低。
缺點:資源請求較多,整頁刷新體驗較差,頁面間傳遞數據只能依賴 URL,cookie,storage 等方式,較為局限。
SPA + MPA
這種方式常見于較老 MPA 項目遷移至 SPA 的情況,缺點結合兩者,兩種主體通信方式也只能以兼容MPA 為準
不過這種方式也有他的好處,假如你的 SPA 中,有類似文章分享這樣(沒有后端直出,后端返 HTML 串的情況下),想保證用戶體驗在 SPA 中開發一個頁面,在 MPA 中也開發一個頁面,去掉沒用的依賴,或者直接用原生 JS 來開發,分享出去是 MPA 的文章頁面,這樣可以加快分享出去的打開速度,同時也能減少靜態資源服務器的壓力,因為如果分享出去的是 SPA 的文章頁面,那 SPA 所需的靜態資源至少都需要去進行協商請求,當然如果服務配置了強緩存就忽略以上所說。
我們首先根據業務所需,來最終確定構建主體,而我們選擇了體驗至上的 SPA,并選用 Vue 技術棧。
② 目錄結構其實我們看開源的絕大部分項目中,目錄結構都會差不太多,我們可以綜合一下來個通用的 src 目錄:
src ├── assets // 資源目錄 圖片,樣式,iconfont ├── components // 全局通用組件目錄 ├── config // 項目配置,攔截器,開關 ├── plugins // 插件相關,生成路由、請求、store 等實例,并掛載 Vue 實例 ├── directives // 拓展指令集合 ├── routes // 路由配置 ├── service // 服務層 ├── utils // 工具類 └── views // 視圖層③ 通用組件
components 中我們會存放 UI 組件庫中的那些常見通用組件了,在項目中直接通過設置別名來使用,如果其他項目需要使用,就發到 npm 上。
結構// components 簡易結構 components ├── dist ├── build ├── src ├── modal ├── toast └── ... ├── index.js └── package.json項目中使用
如果想最終編譯成 es5,直接在 html 中使用或者部署 CDN 上,在 build 配置簡單的打包邏輯,搭配著 package.json 構建 UI組件 的自動化打包發布,最終部署 dist 下的內容,并發布到 npm 上即可。
而我們也可直接使用 es6 的代碼:
import "Components/src/modal"其他項目使用
假設我們發布的 npm 包叫 bm-ui,并且下載到了本地 npm i bm-ui -S:
修改項目的最外層打包配置,在 rules 里 babel-loader 或 happypack 中添加 include,node_modules/bm-ui:
// webpack.base.conf ... rules: [{ test: /.vue$/, loader: "vue-loader", options: vueLoaderConfig }, { test: /.js$/, loader: "babel-loader", // 這里添加 include: [resolve("src"), resolve("test"), resolve("node_modules/bm-ui")] },{ ... }] ...
然后搭配著 babel-plugin-import 直接在項目中使用即可:
import { modal } from "bm-ui"多個組件庫
同時有多個組件庫的話,又或者有同學專門進行組件開發的話,把 `components
內部細分`一下,多一個文件分層。
components ├── bm-ui-1 ├── bm-ui-2 └── ...
你的打包配置文件可以放在 components 下,進行統一打包,當然如果要開源出去還是放在對應庫下。
④ 全局配置,插件與攔截器這個點其實會是項目中經常被忽略的,或者說很少聚合到一起,但同時我認為是整個項目中的重要之一,后續會有例子說道。
全局配置,攔截器目錄結構config ├── index.js // 全局配置/開關 ├── interceptors // 攔截器 ├── index.js // 入口文件 ├── axios.js // 請求/響應攔截 ├── router.js // 路由攔截 └── ... └── ...全局配置
我們在 config/index.js 可能會有如下配置:
// config/index.js // 當前宿主平臺 兼容多平臺應該通過一些特定函數來取得 export const HOST_PLATFORM = "WEB" // 這個就不多說了 export const NODE_ENV = process.env.NODE_ENV || "prod" // 是否強制所有請求訪問本地 MOCK,看到這里同學不難猜到,每個請求也可以多帶帶控制是否請求 MOCK export const AJAX_LOCALLY_ENABLE = false // 是否開啟監控 export const MONITOR_ENABLE = true // 路由默認配置,路由表并不從此注入 export const ROUTER_DEFAULT_CONFIG = { waitForData: true, transitionOnLoad: true } // axios 默認配置 export const AXIOS_DEFAULT_CONFIG = { timeout: 20000, maxContentLength: 2000, headers: {} } // vuex 默認配置 export const VUEX_DEFAULT_CONFIG = { strict: process.env.NODE_ENV !== "production" } // API 默認配置 export const API_DEFAULT_CONFIG = { mockBaseURL: "", mock: true, debug: false, sep: "/" } // CONST 默認配置 export const CONST_DEFAULT_CONFIG = { sep: "/" } // 還有一些業務相關的配置 // ... // 還有一些方便開發的配置 export const CONSOLE_REQUEST_ENABLE = true // 開啟請求參數打印 export const CONSOLE_RESPONSE_ENABLE = true // 開啟響應參數打印 export const CONSOLE_MONITOR_ENABLE = true // 監控記錄打印
可以看出這里匯集了項目中所有用到的配置,下面我們在 plugins 中實例化插件,注入對應配置,目錄如下:
插件目錄結構plugins ├── api.js // 服務層 api 插件 ├── axios.js // 請求實例插件 ├── const.js // 服務層 const 插件 ├── store.js // vuex 實例插件 ├── inject.js // 注入 Vue 原型插件 └── router.js // 路由實例插件實例化插件并注入配置
這里先舉出兩個例子,看我們是如何注入配置,攔截器并實例化的
實例化 router:
import Vue from "vue" import Router from "vue-router" import ROUTES from "Routes" import {ROUTER_DEFAULT_CONFIG} from "Config/index" import {routerBeforeEachFunc} from "Config/interceptors/router" Vue.use(Router) // 注入默認配置和路由表 let routerInstance = new Router({ ...ROUTER_DEFAULT_CONFIG, routes: ROUTES }) // 注入攔截器 routerInstance.beforeEach(routerBeforeEachFunc) export default routerInstance
實例化 axios:
import axios from "axios" import {AXIOS_DEFAULT_CONFIG} from "Config/index" import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from "Config/interceptors/axios" let axiosInstance = {} axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG) // 注入請求攔截 axiosInstance .interceptors.request.use(requestSuccessFunc, requestFailFunc) // 注入響應攔截 axiosInstance .interceptors.response.use(responseSuccessFunc, responseFailFunc) export default axiosInstance
我們在 main.js 注入插件:
// main.js import Vue from "vue" GLOBAL.vbus = new Vue() // import "Components"http:// 全局組件注冊 import "Directives" // 指令 // 引入插件 import router from "Plugins/router" import inject from "Plugins/inject" import store from "Plugins/store" // 引入組件庫及其組件庫樣式 // 不需要配置的庫就在這里引入 // 如果需要配置都放入 plugin 即可 import VueOnsen from "vue-onsenui" import "onsenui/css/onsenui.css" import "onsenui/css/onsen-css-components.css" // 引入根組件 import App from "./App" Vue.use(inject) Vue.use(VueOnsen) // render new Vue({ el: "#app", router, store, template: "", components: { App } })
axios 實例我們并沒有直接引用,相信你也猜到他是通過 inject 插件引用的,我們看下 inject:
import axios from "./axios" import api from "./api" import consts from "./const" GLOBAL.ajax = axios export default { install: (Vue, options) => { Vue.prototype.$api = api Vue.prototype.$ajax = axios Vue.prototype.$const = consts // 需要掛載的都放在這里 } }
這里可以掛載你想在業務中( vue 實例中)便捷訪問的 api,除了 $ajax 之外,api 和 const 兩個插件是我們服務層中主要的功能,后續會介紹,這樣我們插件流程大致運轉起來,下面寫對應攔截器的方法。
請求,路由攔截器在ajax 攔截器中(config/interceptors/axios.js):
// config/interceptors/axios.js import {CONSOLE_REQUEST_ENABLE, CONSOLE_RESPONSE_ENABLE} from "../index.js" export function requestSuccessFunc (requestObj) { CONSOLE_REQUEST_ENABLE && console.info("requestInterceptorFunc", `url: ${requestObj.url}`, requestObj) // 自定義請求攔截邏輯,可以處理權限,請求發送監控等 // ... return requestObj } export function requestFailFunc (requestError) { // 自定義發送請求失敗邏輯,斷網,請求發送監控等 // ... return Promise.reject(requestError); } export function responseSuccessFunc (responseObj) { // 自定義響應成功邏輯,全局攔截接口,根據不同業務做不同處理,響應成功監控等 // ... // 假設我們請求體為 // { // code: 1010, // msg: "this is a msg", // data: null // } let resData = responseObj.data let {code} = resData switch(code) { case 0: // 如果業務成功,直接進成功回調 return resData.data; case 1111: // 如果業務失敗,根據不同 code 做不同處理 // 比如最常見的授權過期跳登錄 // 特定彈窗 // 跳轉特定頁面等 location.href = xxx // 這里的路徑也可以放到全局配置里 return; default: // 業務中還會有一些特殊 code 邏輯,我們可以在這里做統一處理,也可以下方它們到業務層 !responseObj.config.noShowDefaultError && GLOBAL.vbus.$emit("global.$dialog.show", resData.msg); return Promise.reject(resData); } } export function responseFailFunc (responseError) { // 響應失敗,可根據 responseError.message 和 responseError.response.status 來做監控處理 // ... return Promise.reject(responseError); }
定義路由攔截器(config/interceptors/router.js):
// config/interceptors/router.js export function routerBeforeFunc (to, from, next) { // 這里可以做頁面攔截,很多后臺系統中也非常喜歡在這里面做權限處理 // next(...) }
最后在入口文件(config/interceptors/index.js)中引入并暴露出來即可:
import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from "./ajax" import {routerBeforeEachFunc} from "./router" let interceptors = { requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc, routerBeforeEachFunc } export default interceptors
請求攔截這里代碼都很簡單,對于 responseSuccessFunc 中 switch default 邏輯做下簡單說明:
responseObj.config.noShowDefaultError 這里可能不太好理解
我們在請求的時候,可以傳入一個 axios 中并沒有意義的 noShowDefaultError 參數為我們業務所用,當值為 false 或者不存在時,我們會觸發全局事件 global.dialog.show,global.dialog.show我們會注冊在 app.vue 中:
// app.vue export default { ... created() { this.bindEvents }, methods: { bindEvents() { GLOBAL.vbus.$on("global.dialog.show", (msg) => { if(msg) return // 我們會在這里注冊全局需要操控試圖層的事件,方便在非業務代碼中通過發布訂閱調用 this.$dialog.popup({ content: msg }); }) } ... } }
這里也可以把彈窗狀態放入 Store 中,按團隊喜好,我們習慣把公共的涉及視圖邏輯的公共狀態在這里注冊,和業務區分開來。
GLOBAL 是我們掛載 window 上的全局對象,我們把需要掛載的東西都放在 window.GLOBAL 里,減少命名空間沖突的可能。
vbus 其實就是我們開始 new Vue() 掛載上去的
GLOBAL.vbus = new Vue()
我們在這里 Promise.reject 出去,我們就可以在 error 回調里面只處理我們的業務邏輯,而其他如斷網、超時、服務器出錯等均通過攔截器進行統一處理。
攔截器處理前后對比對比下處理前后在業務中的發送請求的代碼:
攔截器處理前:
this.$axios.get("test_url").then(({code, data}) => { if( code === 0 ) { // 業務成功 } else if () {} // em... 各種業務不成功處理,如果遇到通用的處理,還需要抽離出來 }, error => { // 需要根據 error 做各種抽離好的處理邏輯,斷網,超時等等... })
攔截器處理后:
// 業務失敗走默認彈窗邏輯的情況 this.$axios.get("test_url").then(({data}) => { // 業務成功,直接操作 data 即可 }) // 業務失敗自定義 this.$axios.get("test_url", { noShowDefaultError: true // 可選 }).then(({data}) => { // 業務成功,直接操作 data 即可 }, (code, msg) => { // 當有特定 code 需要特殊處理,傳入 noShowDefaultError:true,在這個回調處理就行 })為什么要如此配置與攔截器?
在應對項目開發過程中需求的不可預見性時,讓我們能處理的更快更好
到這里很多同學會覺得,就這么簡單的引入判斷,可有可無,
就如我們最近做的一個需求來說,我們 ToC 端項目之前一直是在微信公眾號中打開的,而我們需要在小程序中通過 webview 打開大部分流程,而我們也沒有時間,沒有空間在小程序中重寫近 100 + 的頁面流程,這是我們開發之初并沒有想到的。這時候必須把項目兼容到小程序端,在兼容過程中可能需要解決以下問題:
請求路徑完全不同。
需要兼容兩套不同的權限系統。
有些流程在小程序端需要做改動,跳轉到特定頁面。
有些公眾號的 api ,在小程序中無用,需要調用小程序的邏輯,需要做兼容。
很多也頁面上的元素,小程序端不做展示等。
可以看出,稍微不慎,會影響公眾號現有邏輯。
添加請求攔截 interceptors/minaAjax.js, interceptors/minaRouter.js,原有的換更為 interceptors/officalAjax.js,interceptors/officalRouter.js,在入口文件interceptors/index.js,根據當前宿主平臺,也就是全局配置 HOST_PLATFORM,通過代理模式和策略模式,注入對應平臺的攔截器,在minaAjax.js中重寫請求路徑和權限處理,在 minaRouter.js 中添加頁面攔截配置,跳轉到特定頁面,這樣一并解決了上面的問題 1,2,3。
問題 4 其實也比較好處理了,拷貝需要兼容 api 的頁面,重寫里面的邏輯,通過路由攔截器一并做跳轉處理。
問題 5 也很簡單,拓展兩個自定義指令 v-mina-show 和 v-mina-hide ,在展示不同步的地方可以直接使用指令。
最終用最少的代碼,最快的時間完美上線,絲毫沒影響到現有 toC 端業務,而且這樣把所有兼容邏輯絕大部分聚合到了一起,方便二次拓展和修改。
雖然這只是根據自身業務結合來說明,可能沒什么說服力,不過不難看出全局配置/攔截器 雖然代碼不多,但卻是整個項目的核心之一,我們可以在里面做更多 awesome 的事情。
⑤ 路由配置與懶加載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 "./axios" import _pick from "lodash/pick" import _assign from "lodash/assign" import _isEmpty from "lodash/isEmpty" import { assert } from "Utils/tools" import { API_DEFAULT_CONFIG } from "Config" import API_CONFIG from "Service/api" class 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 "./api" export 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/api" api["order/info"]({ a: 1, b: 2 })
當然對于運行效率要求高的項目中,避免內存使用率過大,我們需要改造 API,用解構的方式引入使用,最終利用 webpack 的 tree-shaking 減少打包體積。幾個簡單的思路
一般來說,多人協作時候大家都可以先看 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 "vue" import Vuex from "vuex" import {VUEX_DEFAULT_CONFIG} from "Config" import commonStore from "Service/store" Vue.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/store" import actions from "./actions.js" import moduleA from "./moduleA.js" import moduleB from "./moduleB.js" export default { install() { Store.registerModule(["pageA"], { actions, modules: { moduleA, moduleB }, namespaced: true }) }, uninstall() { Store.unregisterModule(["pageA"]) } }
最終在 index.vue 中引入使用, 在頁面跳轉之前注冊這些狀態和管理狀態的規則,在路由離開之前,先卸載這些狀態和管理狀態的規則:
import store from "./store" import {mapGetters} from "vuex" export 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 還是有的,期待你的加入。
[[文章] 淺談混合應用演進](https://juejin.im/post/5b189f...
[[文章] 深入了解 weex](https://juejin.im/post/5b18a0...
[[文章] weex-eros 入門指南](https://bmfe.github.io/eros-d...
項目地址
文檔地址
最后附上部分產品截圖~
(逃~)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95917.html
摘要:其實就是我們開始掛載上去的我們在這里出去,我們就可以在回調里面只處理我們的業務邏輯,而其他如斷網超時服務器出錯等均通過攔截器進行統一處理。 showImg(https://segmentfault.com/img/remote/1460000015472616?w=845&h=622); 開始之前 隨著業務的不斷累積,目前我們 ToC 端主要項目,除去 node_modules, bu...
摘要:我的書簽我的書簽謹慎導入,小心覆蓋工具類版本管理快速切換源配置教程指南可視化工具前端工具集前端助手網絡封包截取工具格式化工具標注工具模擬請求類深入淺出布局你所不知道的動畫技巧與細節常用代碼黑魔法小技巧,讓你少寫不必要的,代碼更優雅一勞永 我的書簽 我的書簽(謹慎導入,小心覆蓋) 工具類 nvm: node版本管理 nrm: 快速切換npm源 shell: zsh+on-my-zsh配...
摘要:我的書簽我的書簽謹慎導入,小心覆蓋工具類版本管理快速切換源配置教程指南可視化工具前端工具集前端助手網絡封包截取工具格式化工具標注工具模擬請求類深入淺出布局你所不知道的動畫技巧與細節常用代碼黑魔法小技巧,讓你少寫不必要的,代碼更優雅一勞永 我的書簽 我的書簽(謹慎導入,小心覆蓋) 工具類 nvm: node版本管理 nrm: 快速切換npm源 shell: zsh+on-my-zsh配...
摘要:整理收藏一些優秀的文章及大佬博客留著慢慢學習原文協作規范中文技術文檔協作規范阮一峰編程風格凹凸實驗室前端代碼規范風格指南這一次,徹底弄懂執行機制一次弄懂徹底解決此類面試問題瀏覽器與的事件循環有何區別筆試題事件循環機制異步編程理解的異步 better-learning 整理收藏一些優秀的文章及大佬博客留著慢慢學習 原文:https://www.ahwgs.cn/youxiuwenzhan...
閱讀 3649·2021-09-22 15:15
閱讀 3555·2021-08-12 13:24
閱讀 1309·2019-08-30 15:53
閱讀 1815·2019-08-30 15:43
閱讀 1178·2019-08-29 17:04
閱讀 2792·2019-08-29 15:08
閱讀 1573·2019-08-29 13:13
閱讀 3084·2019-08-29 11:06