摘要:其實(shí)就是我們開始掛載上去的我們?cè)谶@里出去,我們就可以在回調(diào)里面只處理我們的業(yè)務(wù)邏輯,而其他如斷網(wǎng)超時(shí)服務(wù)器出錯(cuò)等均通過(guò)攔截器進(jìn)行統(tǒng)一處理。
開始之前
隨著業(yè)務(wù)的不斷累積,目前我們 ToC 端主要項(xiàng)目,除去 node_modules, build 配置文件,dist 靜態(tài)資源文件的代碼量為 137521 行,后臺(tái)管理系統(tǒng)下各個(gè)子應(yīng)用代碼,除去依賴等文件的總行數(shù)也達(dá)到 100萬(wàn) 多一點(diǎn)。
代碼量意味不了什么,只能證明模塊很多,但相同兩個(gè)項(xiàng)目,在運(yùn)行時(shí)性能相同情況下,你的 10 萬(wàn)行代碼能容納并維護(hù) 150 個(gè)模塊,并且開發(fā)順暢,我的項(xiàng)目中 10 萬(wàn)行代碼卻只能容納 100 個(gè)模塊,添加功能也好,維護(hù)起來(lái)也較為繁瑣,這就很值得思考。
本文會(huì)在主要描述以 Vue 技術(shù)棧為技術(shù)主體,ToC 端項(xiàng)目業(yè)務(wù)主體,在構(gòu)建過(guò)程中,遇到或者總結(jié)的點(diǎn)(也會(huì)提及一些 ToB 項(xiàng)目的場(chǎng)景),可能并不適合你的業(yè)務(wù)場(chǎng)景(僅供參考),我會(huì)盡可能多的描述問題與其中的思考,最大可能的幫助到需要的同學(xué),也辛苦開發(fā)者發(fā)現(xiàn)問題或者不合理/不正確的地方及時(shí)向我反饋,會(huì)盡快修改,歡迎有更好的實(shí)現(xiàn)方式來(lái) pr。
vue-develop-template 完善中,可以運(yùn)行
可以參考螞蟻金服數(shù)據(jù)體驗(yàn)技術(shù)團(tuán)隊(duì)編寫的文章:
如何管理好10萬(wàn)行代碼的前端單頁(yè)面應(yīng)用
本文并不是基于上面文章寫的,不過(guò)當(dāng)時(shí)在看到他們文章之后覺得有相似的地方,相較于這篇文章,本文可能會(huì)枯燥些,會(huì)有大量代碼,同學(xué)可以直接用上倉(cāng)庫(kù)看。
① 單頁(yè)面,多頁(yè)面首先要思考我們的項(xiàng)目最終的構(gòu)建主體是單頁(yè)面,還是多頁(yè)面,還是單頁(yè) + 多頁(yè),通過(guò)他們的優(yōu)缺點(diǎn)來(lái)分析:
單頁(yè)面(SPA)
優(yōu)點(diǎn):體驗(yàn)好,路由之間跳轉(zhuǎn)流程,可定制轉(zhuǎn)場(chǎng)動(dòng)畫,使用了懶加載可有效減少首頁(yè)白屏?xí)r間,相較于多頁(yè)面減少了用戶訪問靜態(tài)資源服務(wù)器的次數(shù)等。
缺點(diǎn):初始會(huì)加載較大的靜態(tài)資源,并且隨著業(yè)務(wù)增長(zhǎng)會(huì)越來(lái)越大,懶加載也有他的弊端,不做特殊處理不利于 SEO 等。
多頁(yè)面(MPA):
優(yōu)點(diǎn):對(duì)搜索引擎友好,開發(fā)難度較低。
缺點(diǎn):資源請(qǐng)求較多,整頁(yè)刷新體驗(yàn)較差,頁(yè)面間傳遞數(shù)據(jù)只能依賴 URL,cookie,storage 等方式,較為局限。
SPA + MPA
這種方式常見于較老 MPA 項(xiàng)目遷移至 SPA 的情況,缺點(diǎn)結(jié)合兩者,兩種主體通信方式也只能以兼容MPA 為準(zhǔn)
不過(guò)這種方式也有他的好處,假如你的 SPA 中,有類似文章分享這樣(沒有后端直出,后端返 HTML 串的情況下),想保證用戶體驗(yàn)在 SPA 中開發(fā)一個(gè)頁(yè)面,在 MPA 中也開發(fā)一個(gè)頁(yè)面,去掉沒用的依賴,或者直接用原生 JS 來(lái)開發(fā),分享出去是 MPA 的文章頁(yè)面,這樣可以加快分享出去的打開速度,同時(shí)也能減少靜態(tài)資源服務(wù)器的壓力,因?yàn)槿绻窒沓鋈サ氖?SPA 的文章頁(yè)面,那 SPA 所需的靜態(tài)資源至少都需要去進(jìn)行協(xié)商請(qǐng)求,當(dāng)然如果服務(wù)配置了強(qiáng)緩存就忽略以上所說(shuō)。
我們首先根據(jù)業(yè)務(wù)所需,來(lái)最終確定構(gòu)建主體,而我們選擇了體驗(yàn)至上的 SPA,并選用 Vue 技術(shù)棧。
② 目錄結(jié)構(gòu)其實(shí)我們看開源的絕大部分項(xiàng)目中,目錄結(jié)構(gòu)都會(huì)差不太多,我們可以綜合一下來(lái)個(gè)通用的 src 目錄:
src ├── assets // 資源目錄 圖片,樣式,iconfont ├── components // 全局通用組件目錄 ├── config // 項(xiàng)目配置,攔截器,開關(guān) ├── plugins // 插件相關(guān),生成路由、請(qǐng)求、store 等實(shí)例,并掛載 Vue 實(shí)例 ├── directives // 拓展指令集合 ├── routes // 路由配置 ├── service // 服務(wù)層 ├── utils // 工具類 └── views // 視圖層③ 通用組件
components 中我們會(huì)存放 UI 組件庫(kù)中的那些常見通用組件了,在項(xiàng)目中直接通過(guò)設(shè)置別名來(lái)使用,如果其他項(xiàng)目需要使用,就發(fā)到 npm 上。
結(jié)構(gòu)// components 簡(jiǎn)易結(jié)構(gòu) components ├── dist ├── build ├── src ├── modal ├── toast └── ... ├── index.js └── package.json項(xiàng)目中使用
如果想最終編譯成 es5,直接在 html 中使用或者部署 CDN 上,在 build 配置簡(jiǎn)單的打包邏輯,搭配著 package.json 構(gòu)建 UI組件 的自動(dòng)化打包發(fā)布,最終部署 dist 下的內(nèi)容,并發(fā)布到 npm 上即可。
而我們也可直接使用 es6 的代碼:
import "Components/src/modal"其他項(xiàng)目使用
假設(shè)我們發(fā)布的 npm 包叫 bm-ui,并且下載到了本地 npm i bm-ui -S:
修改項(xiàng)目的最外層打包配置,在 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 直接在項(xiàng)目中使用即可:
import { modal } from "bm-ui"多個(gè)組件庫(kù)
同時(shí)有多個(gè)組件庫(kù)的話,又或者有同學(xué)專門進(jìn)行組件開發(fā)的話,把 `components
內(nèi)部細(xì)分`一下,多一個(gè)文件分層。
components ├── bm-ui-1 ├── bm-ui-2 └── ...
你的打包配置文件可以放在 components 下,進(jìn)行統(tǒng)一打包,當(dāng)然如果要開源出去還是放在對(duì)應(yīng)庫(kù)下。
④ 全局配置,插件與攔截器這個(gè)點(diǎn)其實(shí)會(huì)是項(xiàng)目中經(jīng)常被忽略的,或者說(shuō)很少聚合到一起,但同時(shí)我認(rèn)為是整個(gè)項(xiàng)目中的重要之一,后續(xù)會(huì)有例子說(shuō)道。
全局配置,攔截器目錄結(jié)構(gòu)config ├── index.js // 全局配置/開關(guān) ├── interceptors // 攔截器 ├── index.js // 入口文件 ├── axios.js // 請(qǐng)求/響應(yīng)攔截 ├── router.js // 路由攔截 └── ... └── ...全局配置
我們?cè)?config/index.js 可能會(huì)有如下配置:
// config/index.js // 當(dāng)前宿主平臺(tái) 兼容多平臺(tái)應(yīng)該通過(guò)一些特定函數(shù)來(lái)取得 export const HOST_PLATFORM = "WEB" // 這個(gè)就不多說(shuō)了 export const NODE_ENV = process.env.NODE_ENV || "prod" // 是否強(qiáng)制所有請(qǐng)求訪問本地 MOCK,看到這里同學(xué)不難猜到,每個(gè)請(qǐng)求也可以多帶帶控制是否請(qǐng)求 MOCK export const AJAX_LOCALLY_ENABLE = false // 是否開啟監(jiān)控 export const MONITOR_ENABLE = true // 路由默認(rèn)配置,路由表并不從此注入 export const ROUTER_DEFAULT_CONFIG = { waitForData: true, transitionOnLoad: true } // axios 默認(rèn)配置 export const AXIOS_DEFAULT_CONFIG = { timeout: 20000, maxContentLength: 2000, headers: {} } // vuex 默認(rèn)配置 export const VUEX_DEFAULT_CONFIG = { strict: process.env.NODE_ENV !== "production" } // API 默認(rèn)配置 export const API_DEFAULT_CONFIG = { mockBaseURL: "", mock: true, debug: false, sep: "/" } // CONST 默認(rèn)配置 export const CONST_DEFAULT_CONFIG = { sep: "/" } // 還有一些業(yè)務(wù)相關(guān)的配置 // ... // 還有一些方便開發(fā)的配置 export const CONSOLE_REQUEST_ENABLE = true // 開啟請(qǐng)求參數(shù)打印 export const CONSOLE_RESPONSE_ENABLE = true // 開啟響應(yīng)參數(shù)打印 export const CONSOLE_MONITOR_ENABLE = true // 監(jiān)控記錄打印
可以看出這里匯集了項(xiàng)目中所有用到的配置,下面我們?cè)?plugins 中實(shí)例化插件,注入對(duì)應(yīng)配置,目錄如下:
插件目錄結(jié)構(gòu)plugins ├── api.js // 服務(wù)層 api 插件 ├── axios.js // 請(qǐng)求實(shí)例插件 ├── const.js // 服務(wù)層 const 插件 ├── store.js // vuex 實(shí)例插件 ├── inject.js // 注入 Vue 原型插件 └── router.js // 路由實(shí)例插件實(shí)例化插件并注入配置
這里先舉出兩個(gè)例子,看我們是如何注入配置,攔截器并實(shí)例化的
實(shí)例化 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) // 注入默認(rèn)配置和路由表 let routerInstance = new Router({ ...ROUTER_DEFAULT_CONFIG, routes: ROUTES }) // 注入攔截器 routerInstance.beforeEach(routerBeforeEachFunc) export default routerInstance
實(shí)例化 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) // 注入請(qǐng)求攔截 axiosInstance .interceptors.request.use(requestSuccessFunc, requestFailFunc) // 注入響應(yīng)攔截 axiosInstance .interceptors.response.use(responseSuccessFunc, responseFailFunc) export default axiosInstance
我們?cè)?main.js 注入插件:
// main.js import Vue from "vue" GLOBAL.vbus = new Vue() // import "Components"http:// 全局組件注冊(cè) import "Directives" // 指令 // 引入插件 import router from "Plugins/router" import inject from "Plugins/inject" import store from "Plugins/store" // 引入組件庫(kù)及其組件庫(kù)樣式 // 不需要配置的庫(kù)就在這里引入 // 如果需要配置都放入 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 實(shí)例我們并沒有直接引用,相信你也猜到他是通過(guò) 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 // 需要掛載的都放在這里 } }
這里可以掛載你想在業(yè)務(wù)中( vue 實(shí)例中)便捷訪問的 api,除了 $ajax 之外,api 和 const 兩個(gè)插件是我們服務(wù)層中主要的功能,后續(xù)會(huì)介紹,這樣我們插件流程大致運(yùn)轉(zhuǎn)起來(lái),下面寫對(duì)應(yīng)攔截器的方法。
請(qǐng)求,路由攔截器在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) // 自定義請(qǐng)求攔截邏輯,可以處理權(quán)限,請(qǐng)求發(fā)送監(jiān)控等 // ... return requestObj } export function requestFailFunc (requestError) { // 自定義發(fā)送請(qǐng)求失敗邏輯,斷網(wǎng),請(qǐng)求發(fā)送監(jiān)控等 // ... return Promise.reject(requestError); } export function responseSuccessFunc (responseObj) { // 自定義響應(yīng)成功邏輯,全局?jǐn)r截接口,根據(jù)不同業(yè)務(wù)做不同處理,響應(yīng)成功監(jiān)控等 // ... // 假設(shè)我們請(qǐng)求體為 // { // code: 1010, // msg: "this is a msg", // data: null // } let resData = responseObj.data let {code} = resData switch(code) { case 0: // 如果業(yè)務(wù)成功,直接進(jìn)成功回調(diào) return resData.data; case 1111: // 如果業(yè)務(wù)失敗,根據(jù)不同 code 做不同處理 // 比如最常見的授權(quán)過(guò)期跳登錄 // 特定彈窗 // 跳轉(zhuǎn)特定頁(yè)面等 location.href = xxx // 這里的路徑也可以放到全局配置里 return; default: // 業(yè)務(wù)中還會(huì)有一些特殊 code 邏輯,我們可以在這里做統(tǒng)一處理,也可以下方它們到業(yè)務(wù)層 !responseObj.config.noShowDefaultError && GLOBAL.vbus.$emit("global.$dialog.show", resData.msg); return Promise.reject(resData); } } export function responseFailFunc (responseError) { // 響應(yīng)失敗,可根據(jù) responseError.message 和 responseError.response.status 來(lái)做監(jiān)控處理 // ... return Promise.reject(responseError); }
定義路由攔截器(config/interceptors/router.js):
// config/interceptors/router.js export function routerBeforeFunc (to, from, next) { // 這里可以做頁(yè)面攔截,很多后臺(tái)系統(tǒng)中也非常喜歡在這里面做權(quán)限處理 // next(...) }
最后在入口文件(config/interceptors/index.js)中引入并暴露出來(lái)即可:
import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from "./ajax" import {routerBeforeEachFunc} from "./router" let interceptors = { requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc, routerBeforeEachFunc } export default interceptors
請(qǐng)求攔截這里代碼都很簡(jiǎn)單,對(duì)于 responseSuccessFunc 中 switch default 邏輯做下簡(jiǎn)單說(shuō)明:
responseObj.config.noShowDefaultError 這里可能不太好理解
我們?cè)谡?qǐng)求的時(shí)候,可以傳入一個(gè) axios 中并沒有意義的 noShowDefaultError 參數(shù)為我們業(yè)務(wù)所用,當(dāng)值為 false 或者不存在時(shí),我們會(huì)觸發(fā)全局事件 global.dialog.show,global.dialog.show我們會(huì)注冊(cè)在 app.vue 中:
// app.vue export default { ... created() { this.bindEvents }, methods: { bindEvents() { GLOBAL.vbus.$on("global.dialog.show", (msg) => { if(msg) return // 我們會(huì)在這里注冊(cè)全局需要操控試圖層的事件,方便在非業(yè)務(wù)代碼中通過(guò)發(fā)布訂閱調(diào)用 this.$dialog.popup({ content: msg }); }) } ... } }
這里也可以把彈窗狀態(tài)放入 Store 中,按團(tuán)隊(duì)喜好,我們習(xí)慣把公共的涉及視圖邏輯的公共狀態(tài)在這里注冊(cè),和業(yè)務(wù)區(qū)分開來(lái)。
GLOBAL 是我們掛載 window 上的全局對(duì)象,我們把需要掛載的東西都放在 window.GLOBAL 里,減少命名空間沖突的可能。
vbus 其實(shí)就是我們開始 new Vue() 掛載上去的
GLOBAL.vbus = new Vue()
我們?cè)谶@里 Promise.reject 出去,我們就可以在 error 回調(diào)里面只處理我們的業(yè)務(wù)邏輯,而其他如斷網(wǎng)、超時(shí)、服務(wù)器出錯(cuò)等均通過(guò)攔截器進(jìn)行統(tǒng)一處理。
攔截器處理前后對(duì)比對(duì)比下處理前后在業(yè)務(wù)中的發(fā)送請(qǐng)求的代碼:
攔截器處理前:
this.$axios.get("test_url").then(({code, data}) => { if( code === 0 ) { // 業(yè)務(wù)成功 } else if () {} // em... 各種業(yè)務(wù)不成功處理,如果遇到通用的處理,還需要抽離出來(lái) }, error => { // 需要根據(jù) error 做各種抽離好的處理邏輯,斷網(wǎng),超時(shí)等等... })
攔截器處理后:
// 業(yè)務(wù)失敗走默認(rèn)彈窗邏輯的情況 this.$axios.get("test_url").then(({data}) => { // 業(yè)務(wù)成功,直接操作 data 即可 }) // 業(yè)務(wù)失敗自定義 this.$axios.get("test_url", { noShowDefaultError: true // 可選 }).then(({data}) => { // 業(yè)務(wù)成功,直接操作 data 即可 }, (code, msg) => { // 當(dāng)有特定 code 需要特殊處理,傳入 noShowDefaultError:true,在這個(gè)回調(diào)處理就行 })為什么要如此配置與攔截器?
在應(yīng)對(duì)項(xiàng)目開發(fā)過(guò)程中需求的不可預(yù)見性時(shí),讓我們能處理的更快更好
到這里很多同學(xué)會(huì)覺得,就這么簡(jiǎn)單的引入判斷,可有可無(wú),
就如我們最近做的一個(gè)需求來(lái)說(shuō),我們 ToC 端項(xiàng)目之前一直是在微信公眾號(hào)中打開的,而我們需要在小程序中通過(guò) webview 打開大部分流程,而我們也沒有時(shí)間,沒有空間在小程序中重寫近 100 + 的頁(yè)面流程,這是我們開發(fā)之初并沒有想到的。這時(shí)候必須把項(xiàng)目兼容到小程序端,在兼容過(guò)程中可能需要解決以下問題:
請(qǐng)求路徑完全不同。
需要兼容兩套不同的權(quán)限系統(tǒng)。
有些流程在小程序端需要做改動(dòng),跳轉(zhuǎn)到特定頁(yè)面。
有些公眾號(hào)的 api ,在小程序中無(wú)用,需要調(diào)用小程序的邏輯,需要做兼容。
很多也頁(yè)面上的元素,小程序端不做展示等。
可以看出,稍微不慎,會(huì)影響公眾號(hào)現(xiàn)有邏輯。
添加請(qǐng)求攔截 interceptors/minaAjax.js, interceptors/minaRouter.js,原有的換更為 interceptors/officalAjax.js,interceptors/officalRouter.js,在入口文件interceptors/index.js,根據(jù)當(dāng)前宿主平臺(tái),也就是全局配置 HOST_PLATFORM,通過(guò)代理模式和策略模式,注入對(duì)應(yīng)平臺(tái)的攔截器,在minaAjax.js中重寫請(qǐng)求路徑和權(quán)限處理,在 minaRouter.js 中添加頁(yè)面攔截配置,跳轉(zhuǎn)到特定頁(yè)面,這樣一并解決了上面的問題 1,2,3。
問題 4 其實(shí)也比較好處理了,拷貝需要兼容 api 的頁(yè)面,重寫里面的邏輯,通過(guò)路由攔截器一并做跳轉(zhuǎn)處理。
問題 5 也很簡(jiǎn)單,拓展兩個(gè)自定義指令 v-mina-show 和 v-mina-hide ,在展示不同步的地方可以直接使用指令。
最終用最少的代碼,最快的時(shí)間完美上線,絲毫沒影響到現(xiàn)有 toC 端業(yè)務(wù),而且這樣把所有兼容邏輯絕大部分聚合到了一起,方便二次拓展和修改。
雖然這只是根據(jù)自身業(yè)務(wù)結(jié)合來(lái)說(shuō)明,可能沒什么說(shuō)服力,不過(guò)不難看出全局配置/攔截器 雖然代碼不多,但卻是整個(gè)項(xiàng)目的核心之一,我們可以在里面做更多 awesome 的事情。
⑤ 路由配置與懶加載directives 里面沒什么可說(shuō)的,不過(guò)很多難題都可以通過(guò)他來(lái)解決,要時(shí)刻記住,我們可以再指令里面操作虛擬 DOM。
路由配置而我們根據(jù)自己的業(yè)務(wù)性質(zhì),最終根據(jù)業(yè)務(wù)流程來(lái)拆分配置:
routes ├── index.js // 入口文件 ├── common.js // 公共路由,登錄,提示頁(yè)等 ├── account.js // 賬戶流程 ├── register.js // 掛號(hào)流程 └── ...
最終通過(guò) index.js 暴露出去給 plugins/router 實(shí)例使用,這里的拆分配置有兩個(gè)注意的地方:
需要根據(jù)自己業(yè)務(wù)性質(zhì)來(lái)決定,有的項(xiàng)目可能適合業(yè)務(wù)線劃分,有的項(xiàng)目更適合以 功能 劃分。
在多人協(xié)作過(guò)程中,盡可能避免沖突,或者減少?zèng)_突。
懶加載文章開頭說(shuō)到單頁(yè)面靜態(tài)資源過(guò)大,首次打開/每次版本升級(jí)后都會(huì)較慢,可以用懶加載來(lái)拆分靜態(tài)資源,減少白屏?xí)r間,但開頭也說(shuō)到懶加載也有待商榷的地方:
如果異步加載較多的組件,會(huì)給靜態(tài)資源服務(wù)器/ CDN 帶來(lái)更大的訪問壓力的同時(shí),如果當(dāng)多個(gè)異步組件都被修改,造成版本號(hào)的變動(dòng),發(fā)布的時(shí)候會(huì)大大增加 CDN 被擊穿的風(fēng)險(xiǎn)。
懶加載首次加載未被緩存的異步組件白屏的問題,造成用戶體驗(yàn)不好。
異步加載通用組件,會(huì)在頁(yè)面可能會(huì)在網(wǎng)絡(luò)延時(shí)的情況下參差不齊的展示出來(lái)等。
這就需要我們根據(jù)項(xiàng)目情況在空間和時(shí)間上做一些權(quán)衡。
以下幾點(diǎn)可以作為簡(jiǎn)單的參考:
對(duì)于訪問量可控的項(xiàng)目,如公司后臺(tái)管理系統(tǒng)中,可以以操作 view 為單位進(jìn)行異步加載,通用組件全部同步加載的方式。
對(duì)于一些復(fù)雜度較高,實(shí)時(shí)度較高的應(yīng)用類型,可采用按功能模塊拆分進(jìn)行異步組件加載。
如果項(xiàng)目想保證比較高的完整性和體驗(yàn),迭代頻率可控,不太關(guān)心首次加載時(shí)間的話,可按需使用異步加載或者直接不使用。
打包出來(lái)的 main.js 的大小,絕大部分都是在路由中引入的并注冊(cè)的視圖組件。⑥ Service 服務(wù)層
服務(wù)層作為項(xiàng)目中的另一個(gè)核心之一,“自古以來(lái)”都是大家比較關(guān)心的地方。
不知道你是否看到過(guò)如下組織代碼方式:
views/ pay/ index.vue service.js components/ a.vue b.vue
在 service.js 中寫入編寫數(shù)據(jù)來(lái)源
export const CONFIAG = { apple: "蘋果", banana: "香蕉" } // ... // ① 處理業(yè)務(wù)邏輯,還彈窗 export function getBInfo ({name = "", id = ""}) { return this.$ajax.get("/api/info", { name, id }).then({age} => { this.$modal.show({ content: age }) }) } // ② 不處理業(yè)務(wù),僅僅寫請(qǐng)求方法 export function getAInfo ({name = "", id = ""}) { return this.$ajax.get("/api/info", { name, id }) } ...
簡(jiǎn)單分析:
① 就不多說(shuō)了,拆分的不夠單純,當(dāng)做二次開發(fā)的時(shí)候,你還得去找這彈窗到底哪里出來(lái)的。
② 看起來(lái)很美好,不摻雜業(yè)務(wù)邏輯,但不知道你與沒遇到過(guò)這樣情況,經(jīng)常會(huì)有其他業(yè)務(wù)需要用到一樣的枚舉,請(qǐng)求一樣的接口,而開發(fā)其他業(yè)務(wù)的同學(xué)并不知道你在這里有一份數(shù)據(jù)源,最終造成的結(jié)果就是數(shù)據(jù)源的代碼到處冗余。
我相信②在絕大多數(shù)項(xiàng)目中都能看到。
那么我們的目的就很明顯了,解決冗余,方便使用,我們把枚舉和請(qǐng)求接口的方法,通過(guò)插件,掛載到一個(gè)大對(duì)象上,注入 Vue 原型,方面業(yè)務(wù)使用即可。
目錄層級(jí)(僅供參考)service ├── api ├── index.js // 入口文件 ├── order.js // 訂單相關(guān)接口配置 └── ... ├── const ├── index.js // 入口文件 ├── order.js // 訂單常量接口配置 └── ... ├── store // vuex 狀態(tài)管理 ├── expands // 拓展 ├── monitor.js // 監(jiān)控 ├── beacon.js // 打點(diǎn) ├── localstorage.js // 本地存儲(chǔ) └── ... // 按需拓展 └── ...抽離模型
首先抽離請(qǐng)求接口模型,可按照領(lǐng)域模型抽離 (service/api/index.js):
{ user: [{ name: "info", method: "GET", desc: "測(cè)試接口1", path: "/api/info", mockPath: "/api/info", params: { a: 1, b: 2 } }, { name: "info2", method: "GET", desc: "測(cè)試接口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" } }] ... }
定制下需要的幾個(gè)功能:
請(qǐng)求參數(shù)自動(dòng)截取。
請(qǐng)求參數(shù)不傳,則發(fā)送默認(rèn)配置參數(shù)。
得需要命名空間。
通過(guò)全局配置開啟調(diào)試模式。
通過(guò)全局配置來(lái)控制走本地 mock 還是線上接口等。
插件編寫定制好功能,開始編寫簡(jiǎn)單的 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 // 通過(guò)全局配置開啟調(diào)試模式。 debug && console.info(`調(diào)用服務(wù)層接口${apiname},接口描述為${desc}`) debug && assert(name, `${apiUrl} :接口name屬性不能為空`) debug && assert(apiUrl.indexOf("/") === 0, `${apiUrl} :接口路徑path,首字符應(yīng)為/`) Object.defineProperty(this.api, `${namespace}${sep}${name}`, { value(outerParams, outerOptions) { // 請(qǐng)求參數(shù)自動(dòng)截取。 // 請(qǐng)求參數(shù)不穿則發(fā)送默認(rèn)配置參數(shù)。 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) { // 這里可以做大小寫轉(zhuǎn)換,也可以做其他類型 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 原型上,上文有說(shuō)到,通過(guò) plugins/inject.js
import api from "./api" export default { install: (Vue, options) => { Vue.prototype.$api = api // 需要掛載的都放在這里 } }使用
這樣我們可以在業(yè)務(wù)中愉快的使用業(yè)務(wù)層代碼:
// .vue 中 export default { methods: { test() { this.$api["order/info"]({ a: 1, b: 2 }) } } }
即使在業(yè)務(wù)之外也可以使用:
import api from "Plugins/api" api["order/info"]({ a: 1, b: 2 })
當(dāng)然對(duì)于運(yùn)行效率要求高的項(xiàng)目中,避免內(nèi)存使用率過(guò)大,我們把命名空間支持成駝峰,直接用解構(gòu)的方式引入使用,最終利用 webpack 的 tree-shaking 減少打包體積即可。
import {orderInfo as getOrderInfo} from "Plugins/api" getOrderInfo({ a: 1, b: 2 })
一般來(lái)說(shuō),多人協(xié)作時(shí)候大家都可以先看 api 是否有對(duì)應(yīng)接口,當(dāng)業(yè)務(wù)量上來(lái)的時(shí)候,也肯定會(huì)有人出現(xiàn)找不到,或者找起來(lái)比較費(fèi)勁,這時(shí)候我們完全可以在 請(qǐng)求攔截器中,把當(dāng)前請(qǐng)求的 url 和 api 中的請(qǐng)求做下判斷,如果有重復(fù)接口請(qǐng)求路徑,則提醒開發(fā)者已經(jīng)配置相關(guān)請(qǐng)求,根據(jù)情況是否進(jìn)行二次配置即可。
最終我們可以拓展 Service 層的各個(gè)功能:
基礎(chǔ)
api:異步與后端交互
const:常量枚舉
store:Vuex 狀態(tài)管理
拓展
localStorage:本地?cái)?shù)據(jù),稍微封裝下,支持存取對(duì)象即可
monitor:監(jiān)控功能,自定義搜集策略,調(diào)用 api 中的接口發(fā)送
beacon:打點(diǎn)功能,自定義搜集策略,調(diào)用 api 中的接口發(fā)送
...
const,localStorage,monitor 和 beacon 根據(jù)業(yè)務(wù)自行拓展暴露給業(yè)務(wù)使用即可,思想也是一樣的,下面著重說(shuō)下 store(Vuex)。
插一句:如果看到這里沒感覺不妥的話,想想上面 plugins/api.js 有沒有用單例模式?該不該用?⑦ 狀態(tài)管理與視圖拆分
Vuex 源碼分析可以看我之前寫的文章。
我們是不是真的需要狀態(tài)管理?答案是否定的,就算你的項(xiàng)目達(dá)到 10 萬(wàn)行代碼,那也并不意味著你必須使用 Vuex,應(yīng)該由業(yè)務(wù)場(chǎng)景決定。業(yè)務(wù)場(chǎng)景
第一類項(xiàng)目:業(yè)務(wù)/視圖復(fù)雜度不高,不建議使用 Vuex,會(huì)帶來(lái)開發(fā)與維護(hù)的成本,使用簡(jiǎn)單的 vbus 做好命名空間,來(lái)解耦即可。
let vbus = new Vue() vbus.$on("print.hello", () => { console.log("hello") }) vbus.$emit("print.hello")
第二類項(xiàng)目:類似多人協(xié)作項(xiàng)目管理,有道云筆記,網(wǎng)易云音樂,微信網(wǎng)頁(yè)版/桌面版等應(yīng)用,功能集中,空間利用率高,實(shí)時(shí)交互的項(xiàng)目,無(wú)疑 Vuex 是較好的選擇。這類應(yīng)用中我們可以直接抽離業(yè)務(wù)領(lǐng)域模型:
store ├── index.js ├── actions.js // 根級(jí)別 action ├── mutations.js // 根級(jí)別 mutation └── modules ├── user.js // 用戶模塊 ├── products.js // 產(chǎn)品模塊 ├── order.js // 訂單模塊 └── ...
當(dāng)然對(duì)于這類項(xiàng)目,vuex 或許不是最好的選擇,有興趣的同學(xué)可以學(xué)習(xí)下 rxjs。
第三類項(xiàng)目:后臺(tái)系統(tǒng)或者頁(yè)面之間業(yè)務(wù)耦合不高的項(xiàng)目,這類項(xiàng)目是占比應(yīng)該是很大的,我們思考下這類項(xiàng)目:
全局共享狀態(tài)不多,但是難免在某個(gè)模塊中會(huì)有復(fù)雜度較高的功能(客服系統(tǒng),實(shí)時(shí)聊天,多人協(xié)作功能等),這時(shí)候如果為了項(xiàng)目的可管理性,我們也在 store 中進(jìn)行管理,隨著項(xiàng)目的迭代我們不難遇到這樣的情況:
store/ ... modules/ b.js ... views/ ... a/ b.js ...
試想下有幾十個(gè) module,對(duì)應(yīng)這邊上百個(gè)業(yè)務(wù)模塊,開發(fā)者在兩個(gè)平級(jí)目錄之間調(diào)試與開發(fā)的成本是巨大的。
這些 module 可以在項(xiàng)目中任一一個(gè)地方被訪問,但往往他們都是冗余的,除了引用的功能模塊之外,基本不會(huì)再有其他模塊引用他。
項(xiàng)目的可維護(hù)程度會(huì)隨著項(xiàng)目增大而增大。
如何解決第三類項(xiàng)目的 store 使用問題?先梳理我們的目標(biāo):
項(xiàng)目中模塊可以自定決定是否使用 Vuex。(漸進(jìn)增強(qiáng))
從有狀態(tài)管理的模塊,跳轉(zhuǎn)沒有的模塊,我們不想把之前的狀態(tài)掛載到 store 上,想提高運(yùn)行效率。(冗余)
讓這類項(xiàng)目的狀態(tài)管理變的更加可維護(hù)。(開發(fā)成本/溝通成本)
實(shí)現(xiàn)我們借助 Vuex 提供的 registerModule 和 unregisterModule 一并解決這些問題,我們?cè)?service/store 中放入全局共享的狀態(tài):
service/ store/ index.js actions.js mutations.js getters.js state.js
一般這類項(xiàng)目全局狀態(tài)不多,如果多了拆分 module 即可。
編寫插件生成 store 實(shí)例:
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 })
對(duì)一個(gè)需要狀態(tài)管理頁(yè)面或者模塊進(jìn)行分層:
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,我們?cè)?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 中引入使用, 在頁(yè)面跳轉(zhuǎn)之前注冊(cè)這些狀態(tài)和管理狀態(tài)的規(guī)則,在路由離開之前,先卸載這些狀態(tài)和管理狀態(tài)的規(guī)則:
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() } }
當(dāng)然如果你的狀態(tài)要共享到全局,就不執(zhí)行 uninstall。
這樣就解決了開頭的三個(gè)問題,不同開發(fā)者在開發(fā)頁(yè)面的時(shí)候,可以根據(jù)頁(yè)面特性,漸進(jìn)增強(qiáng)的選擇某種開發(fā)形式。其他
這里簡(jiǎn)單列舉下其他方面,需要自行根據(jù)項(xiàng)目深入和使用。
打包,構(gòu)建這里網(wǎng)上已經(jīng)有很多優(yōu)化方法:dll,happypack,多線程打包等,但隨著項(xiàng)目的代碼量級(jí),每次 dev 保存的時(shí)候編譯的速度也是會(huì)愈來(lái)愈慢的,而一過(guò)慢的時(shí)候我們就不得不進(jìn)行拆分,這是肯定的,而在拆分之前盡可能容納更多的可維護(hù)的代碼,有幾個(gè)可以嘗試和規(guī)避的點(diǎn):
優(yōu)化項(xiàng)目流程:這個(gè)點(diǎn)看起來(lái)好像沒什么用,但改變卻是最直觀的,頁(yè)面/業(yè)務(wù)上的化簡(jiǎn)為繁會(huì)直接體現(xiàn)到代碼上,同時(shí)也會(huì)增大項(xiàng)目的可維護(hù),可拓展性等。
減少項(xiàng)目文件層級(jí)縱向深度。
減少無(wú)用業(yè)務(wù)代碼,避免使用無(wú)用或者過(guò)大依賴(類似 moment.js 這樣的庫(kù))等。
樣式盡可能抽離各個(gè)模塊,讓整個(gè)樣式底層更加靈活,同時(shí)也應(yīng)該盡可能的減少冗余。
如果使用的 sass 的話,善用 %placeholder 減少無(wú)用代碼打包進(jìn)來(lái)。
MPA 應(yīng)用中樣式冗余過(guò)大,%placeholder 也會(huì)給你帶來(lái)幫助。Mock
很多大公司都有自己的 mock 平臺(tái),當(dāng)前后端定好接口格式,放入生成對(duì)應(yīng) mock api,如果沒有 mock 平臺(tái),那就找相對(duì)好用的工具如 json-server 等。
代碼規(guī)范請(qǐng)強(qiáng)制使用 eslint,掛在 git 的鉤子上。定期 diff 代碼,定期培訓(xùn)等。
TypeScript非常建議用 TS 編寫項(xiàng)目,可能寫 .vue 有些別扭,這樣前端的大部分錯(cuò)誤在編譯時(shí)解決,同時(shí)也能提高瀏覽器運(yùn)行時(shí)效率,可能減少 re-optimize 階段時(shí)間等。
測(cè)試這也是項(xiàng)目非常重要的一點(diǎn),如果你的項(xiàng)目還未使用一些測(cè)試工具,請(qǐng)盡快接入,這里不過(guò)多贅述。
拆分系統(tǒng)當(dāng)項(xiàng)目到達(dá)到一定業(yè)務(wù)量級(jí)時(shí),由于項(xiàng)目中的模塊過(guò)多,新同學(xué)維護(hù)成本,開發(fā)成本都會(huì)直線上升,不得不拆分項(xiàng)目,后續(xù)會(huì)分享出來(lái)我們 ToB 項(xiàng)目在拆分系統(tǒng)中的簡(jiǎn)單實(shí)踐。
最后時(shí)下有各種成熟的方案,這里只是一個(gè)簡(jiǎn)單的構(gòu)建分享,里面依賴的版本都是我們穩(wěn)定下來(lái)的版本,需要根據(jù)自己實(shí)際情況進(jìn)行升級(jí)。
項(xiàng)目底層構(gòu)建往往會(huì)成為前端忽略的地方,我們既要從一個(gè)大局觀來(lái)看待一個(gè)項(xiàng)目或者整條業(yè)務(wù)線,又要對(duì)每一行代碼精益求精,對(duì)開發(fā)體驗(yàn)不斷優(yōu)化,慢慢累積后才能更好的應(yīng)對(duì)未知的變化。
關(guān)于我,可以叫我 Zero,附上 Git 地址
文章標(biāo)題圖片地址
最后請(qǐng)?jiān)试S我打一波小小的廣告 EROS如果前端同學(xué)想嘗試使用 Vue 開發(fā) App,或者熟悉 weex 開發(fā)的同學(xué),可以來(lái)嘗試使用我們的開源解決方案 eros,雖然沒做過(guò)什么廣告,但不完全統(tǒng)計(jì),50 個(gè)在線 APP 還是有的,期待你的加入。
[[文章] 淺談混合應(yīng)用演進(jìn)](https://juejin.im/post/5b189f...
[[文章] 深入了解 weex](https://juejin.im/post/5b18a0...
[[文章] weex-eros 入門指南](https://bmfe.github.io/eros-d...
項(xiàng)目地址
文檔地址
最后附上部分產(chǎn)品截圖~
(逃~)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/95918.html
摘要:其實(shí)就是我們開始掛載上去的我們?cè)谶@里出去,我們就可以在回調(diào)里面只處理我們的業(yè)務(wù)邏輯,而其他如斷網(wǎng)超時(shí)服務(wù)器出錯(cuò)等均通過(guò)攔截器進(jìn)行統(tǒng)一處理。 showImg(https://segmentfault.com/img/remote/1460000015472616?w=845&h=622); 開始之前 隨著業(yè)務(wù)的不斷累積,目前我們 ToC 端主要項(xiàng)目,除去 node_modules, bu...
摘要:我的書簽我的書簽謹(jǐn)慎導(dǎo)入,小心覆蓋工具類版本管理快速切換源配置教程指南可視化工具前端工具集前端助手網(wǎng)絡(luò)封包截取工具格式化工具標(biāo)注工具模擬請(qǐng)求類深入淺出布局你所不知道的動(dòng)畫技巧與細(xì)節(jié)常用代碼黑魔法小技巧,讓你少寫不必要的,代碼更優(yōu)雅一勞永 我的書簽 我的書簽(謹(jǐn)慎導(dǎo)入,小心覆蓋) 工具類 nvm: node版本管理 nrm: 快速切換npm源 shell: zsh+on-my-zsh配...
摘要:我的書簽我的書簽謹(jǐn)慎導(dǎo)入,小心覆蓋工具類版本管理快速切換源配置教程指南可視化工具前端工具集前端助手網(wǎng)絡(luò)封包截取工具格式化工具標(biāo)注工具模擬請(qǐng)求類深入淺出布局你所不知道的動(dòng)畫技巧與細(xì)節(jié)常用代碼黑魔法小技巧,讓你少寫不必要的,代碼更優(yōu)雅一勞永 我的書簽 我的書簽(謹(jǐn)慎導(dǎo)入,小心覆蓋) 工具類 nvm: node版本管理 nrm: 快速切換npm源 shell: zsh+on-my-zsh配...
摘要:整理收藏一些優(yōu)秀的文章及大佬博客留著慢慢學(xué)習(xí)原文協(xié)作規(guī)范中文技術(shù)文檔協(xié)作規(guī)范阮一峰編程風(fēng)格凹凸實(shí)驗(yàn)室前端代碼規(guī)范風(fēng)格指南這一次,徹底弄懂執(zhí)行機(jī)制一次弄懂徹底解決此類面試問題瀏覽器與的事件循環(huán)有何區(qū)別筆試題事件循環(huán)機(jī)制異步編程理解的異步 better-learning 整理收藏一些優(yōu)秀的文章及大佬博客留著慢慢學(xué)習(xí) 原文:https://www.ahwgs.cn/youxiuwenzhan...
閱讀 1437·2021-11-25 09:43
閱讀 2580·2021-09-24 10:30
閱讀 3659·2021-09-06 15:02
閱讀 3593·2019-08-30 15:55
閱讀 3299·2019-08-30 15:53
閱讀 1692·2019-08-30 15:52
閱讀 2142·2019-08-30 14:21
閱讀 2010·2019-08-30 13:55