一、需求:

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