開始之前
隨著業務的不斷累積,目前我們 ToC 端
主要專案,除去 node_modules
, build 配置檔案
,dist 靜態資原始檔
的程式碼量為 137521
行,後臺管理系統下各個子應用程式碼,除去依賴等檔案的總行數也達到 100萬
多一點。
程式碼量意味不了什麼,只能證明模組很多,但相同兩個專案,在
執行時效能相同
情況下,你的10 萬
行程式碼能容納並維護150
個模組,並且開發順暢,我的專案中10 萬
行程式碼卻只能容納100
個模組,新增功能也好,維護起來也較為繁瑣,這就很值得思考。
本文會在主要描述以 Vue 技術棧
為技術主體
,ToC 端
專案業務主體
,在構建過程中,遇到或者總結的點(也會提及一些 ToB 專案的場景),可能並不適合你的業務場景(僅供參考),我會盡可能多的描述問題與其中的思考,最大可能的幫助到需要的同學,也辛苦開發者發現問題或者不合理/不正確的地方及時向我反饋,會盡快修改,歡迎有更好的實現方式來 pr
。
Git 地址
- vue-develop-template 完善中,可以執行
React 專案
可以參考螞蟻金服資料體驗技術團隊
編寫的文章:
本文並不是基於上面文章寫的,不過當時在看到他們文章之後覺得有相似的地方,相較於這篇文章,本文可能會枯燥些,會有大量程式碼,同學可以直接用上倉庫看。
① 單頁面,多頁面
首先要思考我們的專案最終的構建主體
是單頁面
,還是多頁面
,還是單頁 + 多頁
,通過他們的優缺點來分析:
- 單頁面(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`// 全域性元件註冊
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: `<App/>`,
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
有沒有用單例模式
?該不該用?
⑦ 狀態管理與檢視拆分
我們是不是真的需要狀態管理?
答案是否定的,就算你的專案達到 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
專案在拆分系統中的簡單實踐。
最後
時下有各種成熟的方案,這裡只是一個簡單的構建分享,裡面依賴的版本都是我們穩定下來的版本,需要根據自己實際情況進行升級。
專案底層構建往往會成為前端忽略的地方,我們既要從一個大局觀來看待一個專案或者整條業務線,又要對每一行程式碼精益求精,對開發體驗不斷優化,慢慢累積後才能更好的應對未知的變化。
最後請允許我打一波小小的廣告
EROS
如果前端同學想嘗試使用 Vue
開發 App
,或者熟悉 weex
開發的同學,可以來嘗試使用我們的開源解決方案 eros
,雖然沒做過什麼廣告,但不完全統計,50 個線上 APP 還是有的
,期待你的加入。
最後附上部分產品截圖~
(逃~)