公司做的大部分都是後臺管理專案,剔除每個專案的業務邏輯,其實都可以用通用的一套模版來做。線上預覽地址
登入邏輯
每個系統都有自己的登入登出邏輯,而我們前端所要做的其實是請求後臺,拿到登入許可權,帶上登入許可權,獲取使用者資訊和選單資訊。
在vue
專案開發當中,我們一般都是在全域性路由鉤子做這一系列判斷。
router.beforeEach(async(to, from, next) => {
NProgress.start();
await store.dispatch('SetConfigApi'); // 獲取配置
await store.dispatch('SetApi'); // 設定基本配置
const token = await store.dispatch('getToken'); // 獲取token
if (token) {
// 使用者資訊不存在
if (!store.getters.userInfo) {
await store.dispatch('GetUser'); // 獲取使用者資訊
const menuList = await store.dispatch('GetMenu', localRoute); // 獲取選單
await store.dispatch('GenerateRoutes', localRoute);
router.addRoutes(store.getters.addRoutes);
...
} else {
next();
}
} else {
if (whiteList.includes(to.path)) {
// 在免登入白名單,直接進入
next();
} else {
window.location.href = store.getters.api.IPORTAL_LOCAL_API;
NProgress.done();
}
}
});
複製程式碼
當使用者進入系統的時候,先獲取系統的配置資訊,這個配置資訊可以是前端json
檔案,或者是後臺介面;用這種方式可以靈活的修改專案中的配置,而不用每次都打包死進入專案,直接可以要運維童靴修改對應的配置資訊,就可以了。
選單許可權
以前的選單路由是直接寫死在前端,但是當我們直接訪問這個路由時,使用者還是可以進入到這個功能頁面;後來直接改成動態新增路由的方式router.addRoutes
。
- 前端先獲取選單列表
- 根據獲取的選單列表迴圈新增使用者選單路由集合
- 動態新增路由
具體可檢視
請求方案
專案請求是使用的axios
,可以對它新增攔截器來處理我們的請求,也可以處理通過axios.CancelToken
重複請求,具體可看程式碼:
// 設定請求統一資訊
import axios from 'axios';
import store from '@/store/index.js';
import qs from 'qs';
import { messages } from './msg-box.js';
const service = axios.create({
timeout: 300000, // 超時設定
withCredentials: true // 跨域請求
});
let hasLogoutStatus = false; // 是否某個請求存在需要退出的狀態
const queue = []; // 請求佇列
const CancelToken = axios.CancelToken; // axios內建的中斷方法
/**
* 拼接請求的url和方法;
* 同樣的`url + method` 可以視為相同的請求
* @param {Object} config 請求頭物件
*/
const token = config => {
return `${config.url}_${config.method}`;
};
/**
* 中斷重複的請求,並從佇列中移除
* @param {Object} config 請求頭物件
*/
const removeQueue = config => {
for (let i = 0, size = queue.length; i < size; i++) {
const task = queue[i];
if (!task) return;
// 出現401,403狀態碼中斷後續請求
const isLogout = token(config).includes('logout');
// 退出介面跳過中斷邏輯
if (!isLogout && hasLogoutStatus) {
task.token();
queue.splice(i, 1);
} else {
const cancelMethods = ['post', 'put', 'delete']; // 需要中斷的請求方式
const { method } = config;
if (cancelMethods.includes(method)) {
if (task.token === token(config)) {
task.cancel();
queue.splice(i, 1);
}
}
}
}
};
/**
* 請求錯誤統一處理
* @param {Object} response 錯誤物件
*/
const errorHandle = response => {
// eslint-disable-next-line prettier/prettier
const { status, data: { message = '' }} = response;
let msg = message;
if (!message) {
switch (status) {
case 401:
msg = '您沒有許可權訪問此操作!';
break;
case 403:
msg = '您的登入狀態已失效,請重新登入。';
break;
case 424:
msg = response.data.error;
break;
default:
msg = '服務請求異常,請重新整理重試。';
}
}
hasLogoutStatus = status === 401 || status === 403;
if (hasLogoutStatus) {
messages('error', msg, () => {
store.dispatch('Logout');
});
}
messages('error', msg);
};
// 請求攔截器
service.interceptors.request.use(
config => {
// 中斷之前的同名請求
removeQueue(config);
// 新增cancelToken
config.cancelToken = new CancelToken(c => {
queue.push({ token: token(config), cancel: c });
});
// 登入後新增token
if (store.getters.token) {
config.headers['Authorization'] =
store.getters.token.token_type + ' ' + store.getters.token.access_token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 響應攔截器
service.interceptors.response.use(
response => {
// 在請求完成後,自動移出佇列
removeQueue(response.config);
// 關閉全域性按鈕Loading響應
store.dispatch('CancalLoading');
// 錯誤碼處理
if (response.status !== 200) {
return Promise.reject(response);
}
return response;
},
error => {
const { response } = error;
if (response) {
// 錯誤處理
errorHandle(response);
return Promise.reject(response);
} else {
// 請求超時
if (error.message.includes('timeout')) {
console.log('超時了');
messages('error', '請求已超時,請重新整理或檢查網際網路連線');
} else {
// 斷網,可以展示斷網元件
console.log('斷網了');
messages('error', '請檢查網路是否已連線');
}
}
}
);
export default {
get: (url, data = {}) => {
return new Promise((resolve, reject) => {
service
.get(store.getters.api.API + url, { params: data })
.then(response => {
resolve(response.data);
})
.catch(error => {
reject(error);
});
}).catch(error => {
throw new Error(error);
});
},
post: (url, data = {}) => {
return new Promise((resolve, reject) => {
service
.post(store.getters.api.API + url, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
},
withCredentials: true,
transformRequest: [
data => {
return qs.stringify(data);
}
]
})
.then(response => {
resolve(response.data);
})
.catch(error => {
reject(error);
});
}).catch(error => {
return Promise.reject(error);
});
},
...
/**
* blob下載
* @param {String} url 請求地址
* @param {String} method 請求方式 預設`get`
* @param {Object} data 請求資料
*/
exportFile({ url = '', data = {}, method = 'get' }) {
return new Promise((resolve, reject) => {
const isPost =
method.toLocaleUpperCase() === 'POST'
? {
headers: { 'Content-Type': 'application/json' },
data
}
: {
params: data
};
const downConfig = {
withCredentials: true,
responseType: 'blob',
...isPost
};
service
// eslint-disable-next-line no-unexpected-multiline
[method](store.getters.api.API + url, downConfig)
.then(response => {
resolve(response);
})
.catch(error => {
reject(error);
});
}).catch(error => {
return Promise.reject(error);
});
}
};
複製程式碼
當需要使用請求時,可以引用檔案http.js
,也可以掛在到vue
原型上,在元件內使用this.$http
// user.js
import http from '@/utils/http.js';
export function getUser() {
return http.get('/user');
}
// main.js
Vue.prototype.$http = http;
複製程式碼
按鈕Loading處理
按鈕的loading
效果可以處理後臺響應時間有點長場景,這裡使用store
封裝了下處理方式。
// loading.js
import Vue from 'vue';
const loading = {
state: {},
mutations: {
SET_LOADING: (state, data) => {
const isObject =
Object.prototype.toString.call(data) === '[object Object]';
if (!isObject) return;
Object.keys(data).forEach(key => {
Vue.set(state, key, data[key]);
});
},
CANCAL_LOADING: state => {
Object.keys(state).forEach(key => {
Vue.delete(state, key);
});
}
},
actions: {
SetLoading({ commit }, data) {
commit('SET_LOADING', data);
},
CancalLoading({ commit }, data) {
commit('CANCAL_LOADING', data);
}
}
};
export default loading;
// http.js
service.interceptors.response.use(
response => {
// 關閉全域性按鈕Loading響應
store.dispatch('CancalLoading');
...
})
複製程式碼
在元件內定義
<el-button :loading="btn.save" @click="handleClick">儲存</el-button>
computed: {
btn() {
return this.$store.state.loading;
}
}
methods: {
handleClick() {
this.$store.dispatch('SetLoading', { save: true });
}
}
複製程式碼
以上就可以完美的使用loading
,而不用每個都在data
中定義了。
總結
以上都是後臺系統中可以用到的一些處理方式,具體程式碼可檢視。
其他總結文章: