vue項目的一些最佳實踐提煉和經驗總結
- 項目組織結構
- ajax數據請求的封裝和api接口的模塊化管理
- 第三方庫按需加載
- 利用less的深度選擇器優雅覆蓋當前頁面UI庫組件的樣式
- webpack實時打包進度
- vue組件中選項的順序
- 路由的懶加載
- 路由模塊拆分化管理
項目組織結構
清晰的項目結構能讓別人開發進來更容易理解,當然,每個人都有一定的代碼風格習慣。但基於vue開發框架的項目,vue-cli腳手架搭建的項目組織結構大同小異。同時,預想到後面的需求變更及功能增加進展得更有效率,下面截圖是我覺得比較好的項目組織結構:
這個截圖只是針對個人覺得比較通用的vue工程結構,不過這個結構要根據具體的項目情況調整,不必為了模塊化而模塊化。模塊化的優勢就是體現在項目業務比較復雜的情況,如果項目業務邏輯並不復雜,可以適當的刪減部分模塊或文件。
相關說明:
assets: 存放圖片、UI設計的圖標文件
componets:自研的業務型及通用型組件
router:項目的路由管理模塊
store:基於vuex的狀態管理容器,api存放各模塊的數據請求,modules存放將store分割成模塊(module),按官網的說法,每個模塊應該擁有自己的 state、mutation、action、getter,主要是解決應用的所有狀態如果全部集中到一個比較大的store對象,當應用變得非常復雜時,store 對象就有可能變得相當臃腫而難以維護。
例子:
其中的一個模塊configManage.js
import { configManageService } from"../api/index" // state const state = { accountMenuList:[] } // getters const getters = { // 菜單 menuTree: state => { return state.accountMenuList; }, } // actions const actions = { async GET_ACCOUT_MENU({ state, commit }, model) { // 參數 state為當前局部狀態,commit響應式改變當前綁定的菜單數據 const res = await configManageService.getACountMenu(model); commit("CHANGE_MENU", res.data); } } // mutations const mutations = { CHANGE_MENU: (data) => { state.accountMenuList = data; } } export default { state, getters, actions, mutations }
index.js,統一出口,導出全部的store模塊
import Vue from ‘vue‘ import Vuex from ‘vuex‘ import index from ‘./modules/index‘ import report from ‘./modules/report‘ import createLogger from ‘vuex/dist/logger‘ // 控制臺輸出當前變化的某個狀態 Vue.use(Vuex) const debug = process.env.NODE_ENV !== ‘production‘ // 生產或開發環境打包 export const indexStore = new Vuex.Store({ modules: { report, index }, strict: debug, // 按照官網建議,改變state的狀態只能通過getter plugins: debug ? [createLogger()] : [] })
style:
存放重寫UI庫的樣式和不同組件公共樣式文件
util:
存放用es6封裝的工具類,http請求類,配置類、校驗類、事件類等
views:
存放各路由模塊頁面
static:
存放全局配置文件,環境域名等
iconfont:
存放字體圖標文件
ajax數據請求的封裝和api接口的模塊化管理
基於vue的項目,與後臺請求數據我們通常使用的是axios,它是基於promise的http庫,其提供的優秀的特性被廣泛運用在項目當中,官方已推薦使用axios,放棄原有的vue-resource。
1、axios的封裝,在很多業務場景下用來進行請求的攔截、響應的攔截及請求超時等;
// axios請求類,一些基礎化配置 class AjaxRequestModel { constructor(model) { this.url = model.url || ""; this.data = model.data || {}; this.method = model.method || "POST"; // this.success = model.success || function () {}; // this.fail = model.fail || function () {}; // this.slientSuccess = model.slientSuccess || true; this.failMsg = model.failMsg || true; this.baseUrl = model.baseUrl || window.sysConfig.baseUrl; this.loading = model.loading || true; // this.setData(); this.setUrl(); } setData() { // let options = { // sessionid: "" // }; this.data = Object.assign({}, this.data); } setUrl() { this.url = this.baseUrl + this.url; } } // 實例化axios,配置請求超時時間 const axiosInstance = axios.create({ timeout: 1000 * 20 }); // 封裝ajaxService函數,以更少的代碼處理get、post、delete、put請求方式,同時支持async、await異步處理方案,返回promise const ajaxService = param => { let model = new AjaxRequestModel(param); let o = { url: model.url, data: model.data, method: model.method }; // if (model.loading) { // ak.Msg.showLoading(); // } if (model.method === "GET") { o = { url: model.url, params: model.data, method: model.method }; } return new Promise((resolve, reject) => { axiosInstance .request(o) .then(res => { if (res.data.code === 200 || res.data.code === 0) { resolve(res.data); } else { ak.Msg.toast(res.data.message, "error"); reject(res.data); } }) .catch(err => { httpResponseHandle.call(err); reject(err); }); }); };
2、在請求的攔截中,可以攜帶用於接口身份驗證的token,配置headers請求頭、提交參數的序列化等
// 請求頭相關配置 axiosInstance.interceptors.request.use( function (config) { const info = ak.Utils.getSessionStorage("USER_INFO"); config.headers.common[‘token‘] = info ? info[0].token : ""; // config.headers.common[‘Content-Type‘] = "application/json"; return config; }, function (error) { return Promise.reject(error); } );
3、在響應的攔截中,可以進行根據各種狀態碼來進行錯誤的統一處理等
const httpResponseHandle = err => { const opt = err.response; // 請求超時 if (err.code === "ECONNABORTED") { ak.Msg.toast("請求超時,請稍後再試", "error"); } if (opt.status === 401) { ak.Msg.confirm("用戶登錄超時,請重新登錄", () => { sessionStorage.removeItem("USER_INFO"); window.utryVue.$router.replace("/login"); location.reload(); }); } else { ak.Msg.toast(opt.data.message, "error"); } };
4、api接口模塊化管理,業務邏輯和數據請求分層,這樣可以很方便統一管理我們的接口
如圖,把不同的功能拆分,實現代碼模塊化管理,全部的接口均放在api文件夾下面。index.js是一個api接口的導出的出口,這樣就可以把api接口根據功能劃分為多個模塊,利於多人協作開發,比如一個人只負責一個模塊的開發等,還能方便每個模塊中接口的命名
index.js:
import report from ‘./report‘; // 報表模塊 import accountService from ‘./accountService‘; // 登陸、用戶信息相關 // 導出接口 export { accountService, report }
API請求service層:
// 報表管理請求模塊,與後臺請求的參數、請求方式、url均看作一個model import http from "@/util/http.js"; const API_CONTEXT = "sys/"; // 請求的上下文 const report = { async getMenuList() { let model = {}; model.url = API_CONTEXT + "category/getCategoryTree"; model.method = "GET"; let res = await http.ajaxService(model); return res; }, async removeMenu(model) { model.data = { ...model }; model.url = API_CONTEXT + "category/removeCategory"; let res = await http.ajaxService(model); return res; } } export default report;
組件的業務邏輯層調用方式:
// 說明:async、await的寫法省去了不少的回調,在有些必須請求兩個接口或者兩個接口以上場景下,async、await優勢就顯示出來了 import { reportService } from "../../store/api/index"; async getMenuList() { const param = { role: "" }; const res = await reportService.getMenuList(param); // 下面代碼返回成功時才執行,錯誤由上面所講的axios封裝ajaxService統一處理 this.menuList = res.data; }
5、如果後期維護需要修改的接口,我們就直接在api.js中找到對應的修改就好了,而不用去每一個頁面查找我們的接口然後再修改會很麻煩,如果修改的量比較大,難免會自測不充分產生bug,直接gg。還有就是如果直接在我們的業務代碼修改接口,一不小心還容易動到我們的業務代碼造成不必要的麻煩
6、處理接口域名、端口有多個情況
// 無需前端打包,運維環境快速修改配置,eg: window.sysConfig = { // 運維平臺 baseUrl: ‘http://10.0.33.97:7083/‘, // 租戶平臺 tenantUrl: ‘http://10.0.33.96:7082/‘ } // 區分不同平臺的url地址在http.js文件下的AjaxRequestModel類實例化會統一處理 this.baseUrl = model.baseUrl ? window.sysConfig.baseUrl : window.sysConfig.tenantUrl
第三方庫按需加載
按需加載是針對某些第三方庫體積比較大的情況下,優化webpack打包後的js體積,減少頁面的加載時間
以echart為例子:
優化前:
// 全導入 import * as echarts from "echarts";
webpack打包後:
優化後(主js體積減少了400kb,同時build編譯打包速度也得到了減少)
import echarts from "echarts/lib/echarts"; // 依賴註入,目前項目只用到折線圖、餅圖和柱形圖,故只需引入對應的模塊即可,tooltip是提示類,title是鼠標懸停顯示的對應的圖表名稱 import ‘echarts/lib/chart/bar‘; import ‘echarts/lib/chart/line‘; import ‘echarts/lib/chart/pie‘; import ‘echarts/lib/component/tooltip‘; import ‘echarts/lib/component/title‘;
利用less的深度選擇器優雅覆蓋當前頁面UI庫組件的樣式
vue頁面組件的樣式基本是寫在<style scoped lang="less"></style>中,增加scoped屬性的目的讓其樣式只在當前頁面有效。按照這些寫的方式,編譯後當前標簽會加上類似於[data-v-]這樣的屬性,但是第三方的UI組件庫並沒有編譯為帶[data-v-]這樣的屬性,所以就遇到了當前頁面覆蓋的樣式沒生效的情況,有沒有方法處理這種問題呢。有些小夥伴可能會想到我在公共樣式裏面寫,額外添加類名來覆蓋當前組件的樣式,其實,這也不失為一種方案,但是會引來樣式全局汙染和命名可能重名的情況。下面列舉更簡單粗暴的方式,同時避免了樣式汙染和命名沖突的問題:
.menu-tree { /deep/ .el-tree-node__content { height: 32px; } /deep/ .is-current .el-tree-node__content { background-color: #f2f2fa; } }
編譯後,默認給menu-tree加上了[data-v-3c93a211]
/deep/深度選擇器支持less或者sass,如果你用的是原生的css,可以用<<<符號
webpack實時打包進度
在項目用jenkins自動化打包前端項目的時候,常常會遇到打包速度慢而體驗很差,在優化減負依賴包的情況下,同時沒有一個測試環境或生產環境當前打包進度捉雞。這裏推薦一個第三方的插件包
progress-bar-webpack-plugin。
// 需安裝依賴 npm install progress-bar-webpack-plugin --save-dev const ProgressBarPlugin = require(‘progress-bar-webpack-plugin‘) // 在生產環境webpack配置文件的plugin是加上 new ProgressBarPlugin(), // 打包進度
vue組件中寫選項的順序
這裏純屬個人觀點,可能有些小夥伴用vue開發不是遵從這個。為什麽要規定組件的寫法順序呢,或者說它是官方要求的規範,不如說是能讓的代碼更加優雅,更易於維護,因為你寫的代碼不僅是你一個人維護。要是一個團隊都按這個規範來,大家在維護代碼的時候認知一樣,那效率就提高了。
組件依賴:
components(自研的子組件或第三方組件)
service(api請求類,其他服務類)
utils(工具類等)
事件傳遞(vue eventBus)
mixins(復用的屬性或方法)
組合:
mixins
組件的屬性、接口:
components
props
本地響應式屬性、狀態:
data
computed
事件註冊:
watch
組件生命周期:
created
mounted
destroyed等
組件的方法:
methods
例子:
// 例子 import utryTree from "@/components/utry-tree/utry-tree.vue"; import { reportService } from "@/store/api/index"; import Validation from "../../util/Validation"; import eventBus from "@/util/eventBus";
import reportMixins from "@/mixins/reportMixins"; export default { mixins: [], components: { }, props: { menuList: { type: Array, default() { return []; } } }, data(){}, computed:{}, watch:{}, mounted(){}, methods:{}, }
路由的懶加載
有時候,針對有些復雜組件,初始化頁面其實並不需要把全部組件資源加載進來,把業務復雜的組件抽離出來,從而能減少初始化頁面的加載時間
優化前:
import reportManage from ‘@/views/reportManage/index‘; import reportPreview from ‘@/views/reportManage/reportPreview‘; export default [ { path: ‘reportManage/index‘, name: ‘reportManage‘, component: reportManage }, { path: ‘reportManage/reportPreview‘, name: ‘reportPreview‘, component: reportPreview } ];
初始化頁面的加載耗時:
優化後:
import reportManage from ‘@/views/reportManage/index‘; export default [ { path: ‘reportManage/index‘, name: ‘reportManage‘, component: reportManage }, { path: ‘reportManage/reportPreview‘, name: ‘reportPreview‘, component: () => import(‘@/views/reportManage/reportPreview‘), meta: { keepAlive: false } } ];
初始化頁面加載耗時:
時間的差別主要是在js的解析上,主要是是因為初始化頁面沒有加載當前模塊的二次組件的js,等到跳轉到二次頁面再去解析靜態資源,總體優化後初始化頁面的加載時間快了100多毫秒。
路由模塊的拆分化管理
這裏的路由拆分,是指按模塊拆分成不同的路由文件,針對單頁面應用這樣更方便團隊的多人協調同步開發,自己寫的功能模塊互不影響。如果當業務需求多起來的時候,它的優勢就越能體現出來。我們並不想就在一個router.js寫整個工程的路由,這樣會是單文件代碼量龐大而變得很槽糕,同時也會帶來其他同事誤改的問題。
我們在router文件夾下面創建router.js作為路由的入口文件,其他以router.js後綴的文件存放著各個模塊的路由。
router.js:
import Vue from "vue"; import Router from "vue-router"; import NProgress from "nprogress"; // 引入nprogress,每次路由變化網頁頂端有個加載條效果 import ak from "@/util/ak.js"; // 業務路由 import login from "@/views/index/login"; // 租戶平臺 import oamLogin from "@/views/index/oamLogin"; // 運維平臺 import indexRouter from "./index.router"; // 首頁相關 import reportManage from "./reportManage.router"; // 報表管理 Vue.use(Router); // 默認登錄 let routes = [ { path: "/", redirect: "login" }, { path: "/login", name: "login", component: login }, { path: "/oamLogin", name: "oamLogin", component: oamLogin } ]; routes = routes.concat( indexRouter, reportManage ); // router register const router = new Router({ routes }); // 路由相關的攔截操作,在這裏處理,之前有的router相關操作寫在main.js,並不是很友好 router.beforeEach((to, from, next) => { // 每次切換頁面時,調用進度條 NProgress.start(); // cache機制 const info = ak.Utils.getSessionStorage("USER_INFO"); const token = info ? info[0].token : ""; if (token) { next(); } else { if (to.path === "/oamLogin") { next(); } else if (to.path === "/login") { next(); } else { next("/login"); } } }); router.afterEach(() => { // 在即將進入新的頁面組件前,關閉掉進度條 NProgress.done(); });
index.router.js:
import home from ‘@/views/index/home‘; export default [ { path: ‘/index/home‘, name: ‘home‘, component: home } ];
這裏把首頁的路由放在一個數組裏,然後導出去,有router.js統一引入,並實例化當前路由
未完待續......
vue項目的一些最佳實踐提煉和經驗總結