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