一、需求:
1.API独立管理;
2.按模块拆分;
3.拦截控制;
4.响应数据一致化;
5.消息提示
二、实现:
1.目录结构设计:
src/api/index.js // 入口(从main.js引入)
src/api/configs.js // 全局配置
src/api/request.js // 封装的主程序
src/api/common.js // 公共类api接口
src/api/modules // 模块-对齐微服务
2.全流程:
1.引入axios --> 2.全局配置 --> 3.实例配置 --> 4.注入Vue原型 -->
5.实例调用 --> 6.请求拦截 --> 7.请求 -->
8.响应 --> 9.响应拦截 --> 10.数据解析 --> 11.响应提示
3.拦截控制:
配置类型:全局配置, 请求拦截, 响应拦截
事件钩子:
beforeRequest,
requestError,
response,
responseError
4.数据一致化:
返回对象的封装:
- 内容
- 分页
- 正常响应
- 报错响应
- 提示信息
{
result: any, // 内容
page: {} || null, // 分页
response: {}, // 正常响应
error: {}, // 报错响应
baseMessage: String, // 基础成功/错误信息
presetMessage: String, // 预设成功/错误信息
message(msg) {}, // 提示信息
notify(msg) {}, // 右侧通知
}
5.注入vue.prototype原型
// src/index.js
import common from './common';
import article from './modules/article';
const API = {
...common,
article
}
export default {
install(Vue, options) {
Vue.prototype.$api = API
}
}
// src/main.js
import api from ./api
vue.prototype.$api = api
6.接口实例配置:
// src/api/common.js
import request from ./request
const $api = request.create({
baseURL: '/api/user',
timeout: 1000 * 10
})
/*
接口适配层函数命名规范:
新增:addXXX
删除:deleteXXX
更新:updateXXX
根据ID查询记录:getXXXDetail
条件查询一条记录:findOneXXX
条件查询:findXXXs
查询所有记录:getAllXXXs
分页查询:getXXXPage
搜索:searchXXX
其余个性化接口根据语义进行命名
*/
export default {
getUsersPage(req = {}) {
let {params, page} = req
return $api.get({
url: '/page',
params: {id: params && params.id, ...page}
})
},
addUser(req = {}) {
let { data } = req
return $api.post({
url: '/api/user',
data: data.map(x => x),
headers: {
'content-type': 'application/json'
},
// 结果验证(从200状态中返回的结果合格校验)
validator(res) {
return res.data && res.data.code === 200
},
// 结果过滤器
filter(res) {
return res.data.map(x => x)
},
// 出错消息过滤器
errorMessage(err) {
return err.response.data.message
},
// 成功消息过滤器
successMessage(res) {
return '新增用户成功!'
}
})
}
}
7.组件内调用配置:
// demo.vue
this.$api.getUsersPage({
params: {keyword: 'xxx'},
page: {pageSize: 10, currPage: 1},
})
.then(res => {
let {result, page, message} = res
message('用户加载成功!')
})
.catch(err => {
let {presetMessage, message, notify} = res
message('用户加载失败,'+ presetMessage)
})
8.主程设计
// src/api/request.js
import axios from 'axios';
import configs from './configs';
import router from '../router';
import store from '../store';
import { Message, Notification } from 'element-ui'
/**
* http 状态码
*/
const httpStatusMap = {
// 1 消息
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
// 2 成功
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
// 3 重定向
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Move Temporarily',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Switch Proxy',
'307': 'Temporary Redirect',
// 4 请求错误
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Request Entity Too Large',
'414': 'Request-URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Requested Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I\'m a teapot',
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Too Early',
'426': 'Upgrade Required',
'449': 'Retry With',
'451': 'Unavailable For Legal Reasons',
// 5 服务器错误
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'509': 'Bandwidth Limit Exceeded',
'510': 'Not Extended',
'600': 'Unparseable Response Headers',
}
/**
* 跳转登录页
* 携带当前页面路由,以期在登录页面完成登录后返回当前页面
*/
const toLogin = () => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
}
/**
* 请求失败后的错误统一处理
* @param {Number} status 请求失败的状态码
*/
const baseErrorHandler = error => {
const { response, message } = error
// 状态码判断
switch (response.status) {
// 401: 未登录状态,跳转登录页
case 401:
toLogin();
break;
// 403 token过期
// 清除token并跳转登录页
case 403:
error.message = '登录过期,请重新登录';
localStorage.removeItem('token');
store.commit('loginSuccess', null);
setTimeout(() => {
toLogin();
}, 1000);
break;
// 404请求不存在
case 404:
error.message = '请求的资源不存在';
break;
// 500服务器内部错误
case 500:
error.message = '服务器内部错误';
break;
// 502服务网关出错
case 502:
error.message = '服务网关出错';
break;
// 504服务网关超时
case 504:
error.message = '服务网关超时';
break;
}
}
// 默认配置
const defConfigs = configs
// 全局拦截钩子
const interceptors = {
// 请求前
beforeRequest(config) {
// 登录流程控制中,根据本地是否存在token判断用户的登录情况
// 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token
// 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码
// 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。
/*
const token = store.state.token;
token && (config.headers.Authorization = token);
*/
return config;
},
// 请求出错
requestError(error) {
return Promise.reject(error)
},
response(res) {
return res.status >= 200 && res.status < 300 ? Promise.resolve(res) : Promise.reject(res)
},
responseError(error) {
const { response } = error;
if (response) {
// 响应码不在2xx的范围
baseErrorHandler(error);
} else {
// 处理断网的情况
// eg:请求超时或断网时,更新state的network状态
// network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
// 关于断网组件中的刷新重新获取数据,会在断网组件中说明
// store.commit('changeNetwork', false);
}
return Promise.reject(error);
}
}
/**
* 创建请求实例
* @param {Object} configs 全局配置
*/
const createInstance = (handler, configs) => {
// 创建axios实例
var instance = handler.create(configs);
// 设置post请求头
instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
/**
* 请求拦截器
* 每次请求前,如果存在token则在请求头中携带token
*/
instance.interceptors.request.use(
// 请求成功
interceptors.beforeRequest,
// 请求失败
interceptors.requestError
)
/**
* 响应拦截器
*/
instance.interceptors.response.use(
// 响应成功
interceptors.response,
// 响应失败
interceptors.responseError
);
return instance
}
/**
* 请求处理器
* @param {Object} handler 处理器
* @param {String} method 方法
* @param {Object} options 选项
*
* 1.创建请求实例
* 2.整合请求配置、数据
* 3.处理响应
*/
const requestHandler = (handler, method, options) => {
let { url, params, data, headers, validator, filter, errorMessage, successMessage } = options
return new Promise((resolve, reject) => {
handler(
{
url,
method,
params,
data,
headers
}
).then(res => {
const valid = validator && validator(res)
if (valid === undefined || valid === true) {
resolve(successHandler(res, filter, successMessage))
} else {
reject(errorHandler(res, res, errorMessage))
}
})
.catch(err => {
reject(errorHandler(null, err, errorMessage))
})
})
}
/**
* 响应处理器
* @param {Object} response 正常响应
* @param {Function} callback 回调函数
* @param {Object} error 报错响应
*/
const successHandler = (response, filter, successMessage) => {
let { data } = response
let presetMessage = successMessage && successMessage(response);
return {
result: filter && filter(data) || data, // 内容
page: data && data.page || null, // 分页
response, // 正常响应
// 提示信息
presetMessage,
message(msg) {
Message.success(msg || presetMessage || baseMessage)
},
notify(msg) {
Notification({
type: 'success',
title: '提示',
message: msg || presetMessage || baseMessage
})
}
}
}
/**
* 响应处理器
* @param {Object} response 正常响应
* @param {Object} error 报错响应
* @param {Function} callback 回调函数
* @param {Function} filter 过滤器
*/
const errorHandler = (response, error, errorMessage) => {
let baseMessage = error && error.message;
let presetMessage = errorMessage && errorMessage(error);
return {
result: null, // 内容
page: null, // 分页
response, // 响应信息
error, // 报错响应
// 提示信息
baseMessage,
presetMessage,
message(msg) {
Message.error(msg || presetMessage || baseMessage)
},
notify(msg = {}) {
Notification({
type: 'error',
title: msg.title || '服务出错了,请联系管理员,错误信息如下:',
message: msg.message || presetMessage || baseMessage
})
}
}
}
/**
* 创建请求方法对象
* @params {handler} 处理器
*/
const createRequsetMethods = handler => {
// 请求方法列表
const methods = ['get', 'post', 'put', 'delete', 'head', 'patch'];
return methods.reduce((total, x) => {
total[x] = options => requestHandler(handler, x, options)
return total
}, {})
}
/**
* 默认实例
*/
const defInstance = createInstance(axios, defConfigs);
/**
* 对外接口对象
*/
const Interface = {
/**
* 默认请求方法
*/
...createRequsetMethods(defInstance),
/**
* 使用创建方法生成实可调用例,可传入自定义配置
* @param {Object} configs 配置项
* @demo
* let $api = request.create({....})
* $api.get({...})
*/
create(configs) {
const instance = createInstance(axios, {
...defConfigs,
...configs
});
/**
* 封装接口对象
* options来自调用实例的选项
* req来自调用实例的原始选项
*/
return createRequsetMethods(instance)
}
}
export default Interface