摘要:下面也是以模塊的模塊集為例,可以發(fā)現(xiàn)和路由有一些不同就是這里為了防止模塊跟全局耦合,運(yùn)用函數(shù)式編程思想類似于依賴注入,將全局的實例作為函數(shù)參數(shù)傳入,再返回出一個包含的對象,這個導(dǎo)出的對象將會被以模塊名命名,合并到全局的集中。
前言
web前端發(fā)展到現(xiàn)代,已經(jīng)不再是嚴(yán)格意義上的后端MVC的V層,它越來越向類似客戶端開發(fā)的方向發(fā)展,已獨(dú)立擁有了自己的MVVM設(shè)計模型。前后端的分離也使前端人員擁有更大的自由,可以獨(dú)立設(shè)計客戶端部分的架構(gòu)。
【科普】MVVM是Model-View-ViewModel的簡寫。它本質(zhì)上就是MVC 的改進(jìn)版。MVVM 就是將其中的View 的狀態(tài)和行為抽象化,讓我們將視圖 UI 和業(yè)務(wù)邏輯分開。當(dāng)然這些事 ViewModel 已經(jīng)幫我們做了,它可以取出 Model 的數(shù)據(jù)同時幫忙處理 View 中由于需要展示內(nèi)容而涉及的業(yè)務(wù)邏輯。
Vue作為現(xiàn)在流行的MVVM框架,也是本人平常業(yè)務(wù)中用得最多的框架。如何才能更合理、優(yōu)雅的寫VueSPA,是本人一直研究的課題,經(jīng)過一年左右的思考和實踐總結(jié)出本文。
本文屬于中高級實踐討論,不適合新手。
本人個人的觀點(diǎn),不代表是最佳實踐,歡迎大牛一起討論,批評指正。
秉著不重復(fù)造輪子的原則(其實就是懶),工程直接使用Vue2.0官方腳手架生成,使用最新webpack模板。與標(biāo)準(zhǔn)模板的主要差異:
增加了Sass預(yù)編譯器
增加了Vuex狀態(tài)管理
增加了Axios基礎(chǔ)Ajax工具庫
新增部分的安裝請參考他們各自的文檔,這里不贅述。
項目結(jié)構(gòu) 模擬需求討論架構(gòu)前我們需要一個項目需求,這里簡單模擬一個。
需求點(diǎn):3個一級頁面,2個二級頁面,底部的tabbar只在一級頁面出現(xiàn),首頁、個人中心和登錄頁面是未登錄也可以進(jìn)入;財務(wù)和編輯個人信息是只有登錄用戶可見,簡單原型如下:
下面不討論腳手架生成的部分目錄,只聚焦src開發(fā)目錄,依據(jù)原型我們可以大致規(guī)劃出下面的目錄:
├── build ├── config ├── dist ├── src 開發(fā)目錄 │ ├── api 公共api集 │ │ ├── axiosConfig.js axios實例配置 | | └── index.js 公共api集入口 │ ├── assets 資源目錄 │ │ ├── images 圖片 │ │ ├── scripts 第三方腳本 | | └── styles 基礎(chǔ)樣式庫 │ ├── components 公共組件 │ │ ├── common 一般通用組件 │ │ ├── form 表單通用組件 │ │ └── popup 彈出類通用組件 │ │── config 項目配置 │ │ ├── dev.env.js 開發(fā)模式配置 │ │ ├── env.js 一般配置 │ │ ├── modules.js 模塊配置 │ │ └── prod.env.js 生產(chǎn)模式配置 │ │── mixin 用于vue文件混合的模板 │ │── modules 模塊 │ │ ├── finance 財務(wù)模塊 │ │ │ ├── components 財務(wù)模塊私有組件 │ │ │ │ └── FinanceIndexItem.vue 財務(wù)模塊首頁里的條目項 │ │ │ ├── pages 財務(wù)模塊頁面 │ │ │ │ └── FinanceIndex.vue 財務(wù)模塊首頁 │ │ │ ├── api.js 模塊api集 │ │ │ ├── index.js 模塊入口 │ │ │ ├── Layout.vue 模塊承載頁 │ │ │ └── router.js 模塊內(nèi)路由 │ │ ├── home 首頁模塊(子目錄同上) │ │ └── user 用戶模塊(子目錄同上) │ │── pages 公共頁面 │ │ ├── Success.vue 公共狀態(tài)管理模塊 │ │ └── NotFound.vue 用戶模塊(子目錄同上) │ ├── router 路由管理 │ ├── store 公共狀態(tài)管理 │ │ ├── modules 公共狀態(tài)管理模塊 │ │ │ ├── com.js 通用狀態(tài) │ │ │ └── user.js 用戶狀態(tài) │ │ └── index.js 公共狀態(tài)管理入口 │ └── utils 基礎(chǔ)工具 └── static一些規(guī)范約定
根據(jù)本人個人開發(fā)經(jīng)驗總結(jié)的規(guī)范,不代表必須這么做。
所有vue組件都以大寫字面開頭的駝峰命名法命名,這樣保持到模板代碼上,可以便于區(qū)分開html的原生標(biāo)簽;
人為劃分vue組件為“頁面”和“頁面上的組件”,原則上“頁面上的組件”不發(fā)請求,不改變公共狀態(tài),全部通過事件交由“頁面”完成,本人更傾向用˙集中管理。(其實vue中并沒有頁面概念);
各個模塊,包括路由管理、公共狀態(tài)管理、接口集等都在目錄下有個index.js的入口文件,方便引用;
基礎(chǔ)工具內(nèi)的工具使用函數(shù)式編程,做到可移植,不要對本項目產(chǎn)生依賴;
資源圖片只在項目中保留小圖(就是會被webpack處理成base64那些),大圖應(yīng)使用cdn,可以動態(tài)獲取也可以把地址寫到一個腳本里;
使用eslint使js代碼符合Airbnb規(guī)范。
低耦合模塊化開發(fā)項目過程中常遇到要把原來的項目分開部署,或是組件間耦合、或是多人開發(fā)時組件沖突等問題。本人提出的解決辦法是將項目細(xì)分成模塊進(jìn)行開發(fā),每個模塊由若干相關(guān)“頁面”組成,擁有私有組件、路由、api等,如示例所示:劃分了三個模塊,首頁模塊、財務(wù)模塊、用戶模塊。
【小結(jié)】這種方案的核心就是要將太過零散的組件(頁面)聚合成模塊,每個模塊都有一定遷移性,互不耦合,實現(xiàn)按需打包,并且在代碼分割上比單純的分頁面加載更加靈活可控。Layout模塊承載頁
這個是為了讓開發(fā)這個模塊的程序員有類似根組件
一般來說它只是個空的路由跳轉(zhuǎn)頁,當(dāng)然你把模塊的公共數(shù)據(jù)放這里也可以的,在子路由就能this.$parent拿到數(shù)據(jù),可以當(dāng)成子路由間的bus使用,如下以示例的user模塊為例:
模塊內(nèi)路由
模塊內(nèi)路由最后都會被導(dǎo)入總路由中,不要以為只是簡單合并了文件,這里的設(shè)計也跟Layout模塊承載頁有關(guān),
下面以user模塊為例,我們把個人中心、登錄和修改個人信息這三個頁面歸為user模塊,路由規(guī)劃如下。
個人中心:/user
登錄:/user/login
修改個人信息:/user/userInfo
其中由于“個人中心”是一級頁面,需求要求底部有tabBar,所以使它只能是一級路由。
接下來你會發(fā)現(xiàn)Layout模塊承載頁的路由路勁也是"/user",這里不用擔(dān)心會亂,因為路由管理是按順序匹配的,至于為什么要路徑一樣,這只是為了滿足路由規(guī)劃,讓路徑好看而已。
// 通用的tabbar import IndexTabBar from "@/components/common/IndexTabBar"; // 模塊內(nèi)的頁面 import UserIndex from "./pages/UserIndex"; import UserLogin from "./pages/UserLogin"; import UserInfo from "./pages/UserInfo"; export default [ // 一級路由 { name: "userIndex", path: "/user", meta: { title: "個人中心", }, components: { default: UserIndex, footer: IndexTabBar, }, }, { path: "/user", // 這里分割子路由 component: () => import("./layout.vue"), children: [ // 二級路由 { name: "userLogin", path: "login", meta: { title: "登錄", }, component: UserLogin, }, { name: "userInfo", path: "info", meta: { title: "修改個人信息", requiresAuth: true, }, component: UserInfo, }, ], }, ];
模塊承載頁以懶加載的形式component: () => import("./layout.vue")引入,這會使webpack在此處分割代碼,也就是說進(jìn)入模塊內(nèi)是需要再此請求的,可以減少首次加載的數(shù)據(jù)量,提高速度。
官方關(guān)于懶加載的文檔
這里你會發(fā)現(xiàn)后續(xù)的子路由,又是以直接引入的方式加載,也就是說整個模塊會一起加載,實現(xiàn)了分模塊加載。
這與簡單的分頁面加載不同,分頁面加載一直有個難點(diǎn),就是分割的量比較難把握(太多會增加請求次數(shù),太少又降低了速度),而分模塊可以將相關(guān)頁面一起加載(跟提高緩存命中率很像),可以更靈活的規(guī)劃我們的加載,最終效果:
用戶進(jìn)入應(yīng)用,首頁的三個頁面(有tabbar的)就已經(jīng)加載完畢,這時點(diǎn)擊哪個tabbar按鈕都能流暢;
當(dāng)用戶進(jìn)入某個頁面內(nèi)的子頁面,會產(chǎn)生一次請求;
這時整個模塊的頁面都加載完(不一定要全部),用戶在這個模塊內(nèi)又能流暢訪問。
模塊api集這個設(shè)計跟模塊內(nèi)路由類似,目的也是為了按需加載和隔離全局。
下面也是以user模塊的模塊api集為例,可以發(fā)現(xiàn)和路由有一些不同就是這里為了防止模塊跟全局耦合,運(yùn)用函數(shù)式編程思想(類似于依賴注入),將全局的axios實例作為函數(shù)參數(shù)傳入,再返回出一個包含api的對象,這個導(dǎo)出的對象將會被以模塊名命名,合并到全局的api集中。
export default function (axios) { return { postHeadImg(token, userId, data) { const options = { method: "post", name: "換頭像", url: "/data/user/updateHeadImg", headers: { token, userId, }, data, }; return axios(options); }, postProduct(token, userId, data) { const options = { method: "post", name: "提交產(chǎn)品選擇", url: "/product/opt", headers: { token, userId, }, data, }; return axios(options); }, }; }模塊入口
為了方便引用,每個模塊目錄下都有一個index.js,引入模塊的時候可以省略,node會自動讀這個文件。
還是以user模塊為例,這里主要是引入模塊專屬api和模塊內(nèi)路由,并定義了模塊的名字,這個名字是后面掛載專屬api是時候用的。
import api from "./api"; import router from "./router"; export default { name: "user", api, router, };按需打包
示例中config目錄下有個modules.js文件是指定打包需要的模塊,測試一下打包不同數(shù)量的模塊,會發(fā)現(xiàn)產(chǎn)品文件大小會改變,這就證明了已經(jīng)實現(xiàn)按需打包。
至于路由和api集的子模塊整合實現(xiàn),后面會提到。
import home from "@/modules/home"; import finance from "@/modules/finance"; import user from "@/modules/user"; export default [ home, finance, user ]api集的配置
【背景】示例項目模擬常見的接口約定,服務(wù)器與應(yīng)用交互有兩個自定義頭部:token和userId。token是權(quán)限標(biāo)識符,幾乎全部api都需要帶上,為了防CSRF;userId是登錄狀態(tài)標(biāo)識符,有些需要登錄狀態(tài)才能使用的接口才需要帶上,這兩個標(biāo)識符都有有效期。本示例暫不考慮自動續(xù)期的機(jī)制。
在api管理方面本人比較喜歡集中管理接口和配置,但發(fā)起請求和請求回調(diào)傾向與每個接口多帶帶處理。
導(dǎo)出axios實例axios是比較流行的ajax的promise封裝。axios官方文檔
本人推薦在全局保留唯一的axios實例,所有的請求都使用這個公共實例發(fā)起,實現(xiàn)配置的統(tǒng)一。
示例項目的在api文件夾下的axiosConfig.js就是axios的配置,主要是導(dǎo)出一個符合項目設(shè)置的實例,并進(jìn)行一些攔截器設(shè)置。
【PS】至于為什么到導(dǎo)出實例而不是直接修改axios默認(rèn)值?
這是為了預(yù)防某些特例情況下公共實例無法滿足需求,需要多帶帶配置axios的情況,所以為了不污染原始的axios默認(rèn)值,不推薦修改默認(rèn)值。
// 引入axios包 import axios from "axios"; // 引入環(huán)境配置 import env from "../config/env"; // 引入公共狀態(tài)管理 import store from "../store/index"; // 全局默認(rèn)配置 const myAxios = axios.create({ // 跨域帶cookie withCredentials: true, // 基礎(chǔ)url baseURL: `${env.apiUrl}/${env.apiVersion}`, // 超時時間 timeout: 12000, }); // 請求發(fā)起前攔截器 myAxios.interceptors.request.use((_config) => { // ... return config; }, () => { // 異常處理 }); // 響應(yīng)攔截器 myAxios.interceptors.response.use((response) => { // ... }, (error) => { // 異常處理 return Promise.reject(error); }); export default myAxios;公共api集
項目的所有公共api都會編寫到這里,實現(xiàn)集中化管理,最后公共api集會掛載到vue根實例下,使用this.$api就可以方便的訪問。
由于token和userId不是必須頭部,這里我推薦每個接口函數(shù)都多帶帶處理,按需傳入,這樣api函數(shù)也能更加清晰。
給每個接口起名字,是為了后續(xù)取消請求所設(shè)計的。
整體思路:先定義公共api,再將模塊內(nèi)api(按需)掛載進(jìn)來,最后導(dǎo)出api集。
// 引入已經(jīng)配置好的axios實例 import axios from "./axiosConfig"; // 引入模塊 import modules from "../config/modules"; const apiList = { // 獲取token不需要 getToken() { const options = { method: "post", name: "獲取token", url: "/token/get", }; return axios(options); }, loginWithName(token, data) { const options = { method: "post", name: "用戶名密碼登錄", url: "/data/user/login4up", headers: { token, }, data, }; return axios(options); }, postHeadImg(token, userId, data) { const options = { method: "post", name: "換頭像", url: "/data/user/updateHeadImg", headers: { token, userId, }, data, }; return axios(options); }, }; // 使每個模塊里的api集掛載到以模塊名為名的命名空間下 modules.forEach((i) => { Object.assign(apiList, { [i.name]: i.api(axios), }); }); export default apiList;路由管理配置 導(dǎo)入模塊內(nèi)路由
使用示例中用router文件夾下的index.js配置全局路由,api集類似實現(xiàn)集中化管理,導(dǎo)出路由實例會掛載到vue根實例下,使用this.$router就可以方便的訪問。
配置參考官方文檔,這里主要提的一點(diǎn)是,模塊內(nèi)路由的整合,見實例代碼段。
Vue.use(Router); // 路由配置 const routerConfig = { routes: [ { path: "/", meta: { title: env.appName, }, redirect: { name: "home" }, }, { name: "success", path: "/success", meta: { title: "成功", }, component: Success, }, { path: "*", component: NotFound, }, ], }; // 將模塊內(nèi)的路由拼接到全局 modules.forEach((i) => { routerConfig.routes = routerConfig.routes.concat(i.router); }); const router = new Router(routerConfig);在路由鉤子函數(shù)中處理標(biāo)題和權(quán)限
路由的鉤子函數(shù)有很多妙用,這里列舉了一些例子。
路由元信息meta可以自定義需要的數(shù)據(jù),相當(dāng)于給路由一個標(biāo)記,然后在router.afterEach鉤子函數(shù)中可以讀取到并進(jìn)行處理。
回顧上面示例的模塊內(nèi)路由,meta中定義了title(標(biāo)題)和requiresAuth(是否要登錄狀態(tài)),這就會在這里體現(xiàn)出用處。把登錄權(quán)限設(shè)置在這里判斷是為了防止用戶進(jìn)入某些需要權(quán)限的“頁面”。
router.beforeEach((to, from, next) => { // 關(guān)閉公共彈框 if (window.loading) { window.loading.close(); } // 設(shè)置微信分享(如果有) wxShare({ title: "哇哈哈", desc: "在路由鉤子函數(shù)中處理標(biāo)題和權(quán)限", link: env.shareBaseUrl, imgUrl: env.shareBaseUrl + "/images/shareLogo.png" }); // 設(shè)置標(biāo)題 document.title = to.meta.title ? to.meta.title : "示例"; // 檢查登錄狀態(tài) if (to.meta.requiresAuth) { // 目標(biāo)路由需要登錄狀態(tài) // ... } next(); });自動化管理權(quán)限標(biāo)識符(token)
權(quán)限標(biāo)識符的特點(diǎn)就是幾乎每個鏈接都要帶上,需要維護(hù)有效期,為了不浪費(fèi)服務(wù)器資源還需要持久化并保證請求唯一。
本人比較推薦使用公共狀態(tài)管理vuex進(jìn)行自動化管理,減少代碼編寫時的顧慮。
示例中公共狀態(tài)中的com模塊里有tokenObj和waitToken兩個字段,其中tokenObj包含了token和過期時間,waitToken是一個標(biāo)記是否當(dāng)前在獲取token的布爾值。
【PS】為什么要token保證唯一一次請求?
常見的場景:當(dāng)用戶進(jìn)入應(yīng)用,這時候token要么沒有要么已過期,這時頁面需要并發(fā)兩個ajax請求,由于都沒有token,不唯一化處理的話,會同時先發(fā)起兩個token請求,這樣首先是浪費(fèi)了請求資源,其次由于是異步請求,不能保證兩次token的順序,如果服務(wù)器對token管理較嚴(yán)格則會出問題。
由于獲取token是異步操作,所以getToken寫在actions中,把主要過程包裹成立即執(zhí)行函數(shù),并通過waitToken判斷是否要等待,如果要等待就隔一段時間再檢查,這樣就保證了并發(fā)請求時,token能唯一。
const actions = { // needToRegain是為了特殊條件下強(qiáng)制獲取使用 getToken({ commit, state: _state }, needToRegain) { return new Promise((resolve, reject) => { (function main() { // 如果waitToken為真即表示發(fā)起了請求但還未回應(yīng) if (_state.waitToken) { console.log("等待token"); setTimeout(() => { main(); }, 1000); return; } // 是否過期標(biāo)記 let isExpire = false; // 提取現(xiàn)有的tokenObj let tokenObj = { ..._state.tokenObj, }; // 如果沒有token就從本地存儲中讀取 if (!tokenObj.token) { tokenObj = JSON.parse(localStorage.getItem("tokenObj")); // 如果本地有tokenObj會順便添加到狀態(tài)管理 if (tokenObj) { commit("setTokenObj", tokenObj); } } // token是否過時 if (tokenObj && tokenObj.token) { isExpire = new Date().getTime() - tokenObj.expireTime > -10000; } // 綜合判斷是否需要獲取token if (!tokenObj || !tokenObj.token || isExpire || needToRegain) { commit("setWaitToken", true); api.getToken().then((res) => { // 檢查返回的數(shù)據(jù) const checkedData = connect.dataCheck(res); if (checkedData.isDataReady) { const newTokenObj = { token: checkedData.data.token, expireTime: new Date().getTime() + (checkedData.data.expire_time * 1000), }; // 設(shè)置TokenObj會順便保留一份到本地存儲 commit("setTokenObj", newTokenObj); commit("setWaitToken", false); console.log("獲取token成功"); resolve(newTokenObj.token); } else { commit("setWaitToken", false); console.error("獲取token失敗"); reject(checkedData.msg); } }).catch((err) => { window.toast("網(wǎng)絡(luò)錯誤"); commit("setWaitToken", false); reject(err); }); } else { console.log("token已存在,直接返回"); resolve(tokenObj.token); } }()); }); }, };token在請求代碼中使用
將需要token的api函數(shù)套在getToken的回調(diào)中,就能方便的使用,不用再擔(dān)心token是否過期。
const sendData = { mobile: this.formData1.mobile, }; this.$store.dispatch("getToken").then((token) => { this.$api.sendSMS(token, sendData).then((res) => { const checkedData = this.$connect.dataCheck(res); if (checkedData.isDataReady) { window.toast("驗證碼已發(fā)送,請查收短信"); } else { window.toast("驗證碼發(fā)送失敗"); } }).catch(() => { window.toast("網(wǎng)絡(luò)錯誤"); }); });
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/93844.html
摘要:接上篇議題合理的架構(gòu)討論上傳送門。處理思路如下使用上面定義的方法獲取如果能獲取到則說明有有效的,則時候即可跳轉(zhuǎn)到目標(biāo)頁如果獲取到空字符串,則說明無效或不存在,跳轉(zhuǎn)至登錄頁面。 接上篇《【Geek議題】合理的VueSPA架構(gòu)討論(上)》傳送門。 自動化維護(hù)登錄狀態(tài) 登錄狀態(tài)標(biāo)識符跟token類似,都是需要自動維護(hù)有效期,但也有些許不同,獲取過程只在用戶登錄或注冊的時候,不需要自動獲取。 ...
摘要:接上篇議題合理的架構(gòu)討論上傳送門。處理思路如下使用上面定義的方法獲取如果能獲取到則說明有有效的,則時候即可跳轉(zhuǎn)到目標(biāo)頁如果獲取到空字符串,則說明無效或不存在,跳轉(zhuǎn)至登錄頁面。 接上篇《【Geek議題】合理的VueSPA架構(gòu)討論(上)》傳送門。 自動化維護(hù)登錄狀態(tài) 登錄狀態(tài)標(biāo)識符跟token類似,都是需要自動維護(hù)有效期,但也有些許不同,獲取過程只在用戶登錄或注冊的時候,不需要自動獲取。 ...
摘要:下面也是以模塊的模塊集為例,可以發(fā)現(xiàn)和路由有一些不同就是這里為了防止模塊跟全局耦合,運(yùn)用函數(shù)式編程思想類似于依賴注入,將全局的實例作為函數(shù)參數(shù)傳入,再返回出一個包含的對象,這個導(dǎo)出的對象將會被以模塊名命名,合并到全局的集中。 前言 web前端發(fā)展到現(xiàn)代,已經(jīng)不再是嚴(yán)格意義上的后端MVC的V層,它越來越向類似客戶端開發(fā)的方向發(fā)展,已獨(dú)立擁有了自己的MVVM設(shè)計模型。前后端的分離也使前端人...
摘要:同源策略年,同源政策由公司引入瀏覽器。標(biāo)簽不受同源策略限制,但只能發(fā)起請求。這一行為使得不同域的特定文檔可以讀取該屬性值,因此可以繞過同源策略并使跨域消息通信成為可能。 前言 現(xiàn)在cross-origin resource sharing(跨域資源共享,下簡稱CORS)已經(jīng)十分普及,算上IE8的不標(biāo)準(zhǔn)兼容(XDomainRequest),各大瀏覽器基本都已支持,當(dāng)年為了前后端分離、if...
摘要:同源策略年,同源政策由公司引入瀏覽器。標(biāo)簽不受同源策略限制,但只能發(fā)起請求。這一行為使得不同域的特定文檔可以讀取該屬性值,因此可以繞過同源策略并使跨域消息通信成為可能。 前言 現(xiàn)在cross-origin resource sharing(跨域資源共享,下簡稱CORS)已經(jīng)十分普及,算上IE8的不標(biāo)準(zhǔn)兼容(XDomainRequest),各大瀏覽器基本都已支持,當(dāng)年為了前后端分離、if...
閱讀 2134·2019-08-29 16:53
閱讀 2708·2019-08-29 16:07
閱讀 2052·2019-08-29 13:13
閱讀 3274·2019-08-26 13:57
閱讀 1340·2019-08-26 13:31
閱讀 2444·2019-08-26 13:22
閱讀 1231·2019-08-26 11:43
閱讀 2094·2019-08-23 17:14