【Vue项目总结】后台管理项目总结
公司做的大部分都是后台管理项目,剔除每个项目的业务逻辑,其实都可以用通用的一套模版来做。
登录逻辑
每个系统都有自己的登录登出逻辑,而我们前端所要做的其实是请求后台,拿到登录权限,带上登录权限,获取用户信息和菜单信息。 在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
中定义了。
总结
以上都是后台系统中可以用到的一些处理方式,具体代码可查看。
其他总结文章: