主要内容
实际项目中
api模块
的目录结构及使用方式-
统一处理请求和响应
- 处理后端通用错误,如登录过期、访问权限不足、系统维护等
- 处理后端接口级错误,如新建文章功能,会有标题不合法、未选择文章分类等
-
用面向对象的方式开发项目的
http
功能- 好处是利于维护和扩展,下面会有使用场景和示例
目录结构及使用方式
API模块的目录结构
|-- api
|-- modules -------------------- 模块文件夹,包含项目中关于请求的所有模块
|-- auth.js ---------------- 模块名,如auth一般会包含login、logout等接口的api方法
|-- xxx.js
|-- xxx.js
|-- http.js -------------------- http请求方法,在模块方法中被调用
|-- index.js ------------------- 导出所有封装好的api方法
目录结构及使用方式
假设我们有一个请求文章详情的接口,后端在接口文档中提供以下信息
- 地址:
http://xx.com/api/article/detail
- 参数:
参数名 类型 说明 id int 文章的id - 接口请求成功时返回文章详情
- 接口参数错误时,返回如下内容
{ "isSuccess": false, "errorCode": "INVALID_ARTICLE_ID", }
错误码 说明 ARTICLE_ID_INVALID 无效的文章ID TOKEN_INVALID 无效的用户TOKEN
使用方式
在
api/modules/
下新建article.js
文件-
在
api/index.js
中引入并导出article
模块// 授权 import auth from './modules/auth'; // 文章 import article from './modules/article'; export default { auth, article, };
-
在
article.js
文件中新建获取文章详情
的方法import CommonHttp from '../http' const commonHttp = new CommonHttp() export default { getArticleDetail(params) { return commonHttp.http({ url: '/api/article/detail', params: params, errorCodes: { // 是否要自己处理错误,设为true时,将不会弹出通用错误提示框 dealSelf: true // INVALID_ID 为后台返回的错误码,可以在后面定义说明,若未定义说明,则会弹出错误码 ARTICLE_ID_INVALID: '文章ID好像不太对呦!' }, }) }, }
-
在需要使用该接口的地方引入
getArticleDetail
方法import api from '@/api' api.auth.getArticleDetail(params).then((res) => { // do something })
封装http类
- 下面我们会定义一个CommonHttp的类,并为类定义一个名为http的实例方法供外部调用
由于代码量较大,建议您从http方法开始看
// http.js
import { Message } from 'element-ui'
import axios from 'axios'
/**
* 错误提示
*/
const errorTip = (msg) => {
Message.error(msg || '后端未返回错误码')
}
/**
* 重新登录
*/
function resetLogin() {
// 这里写跳转到登录页的方法
}
class CommonHttp {
constructor() {
this.url = ''
this.params = null
this.method = ''
this.errorCodes = null
this.responseAdapter = null
this.requestHeaders = ''
this.withCredentials = false
this.headerContentType = 'application/json; charset=utf-8'
}
/**
* 生成请求头
*/
createRequestHeaders() {
this.requestHeaders = {
'Content-Type': this.headerContentType,
}
if (token) {
requestHeaders.Authorization = localStorage.getItem('token')
}
}
/**
* 生成请求参数
*/
createRequestParams() {
const params = {}
this.params && Object.keys(this.params).forEach((objKey) => {
const val = this.params[objKey]
// 剔除 undefined 、null和空字符串(是否需要剔除需要和后端沟通)
if (val !== null && val !== '' && val !== undefined) {
params[objKey] = val
}
})
this.params = params
}
requestInterceptor() {
const { method, url } = this
// 如果未在调用http时指定请求方式,则从环境变量中读取
const requestMethod = method === null ? env.REQUEST_METHOD : method
// 组装请求配置,具体可见axios文档
this.requestConfig = {
url,
headers: this.requestHeaders,
method: requestMethod,
timeout: 1000 * 10,
withCredentials: this.withCredentials,
baseURL: env.API_LOCATION,
}
if (requestMethod === 'GET') {
this.requestConfig.params = this.params
}
if (requestMethod === 'POST') {
this.requestConfig.data = this.params
}
}
/**
* 处理响应错误
*/
handleError(reject, resultData) {
// errorCodes 为调用http方法时传入的错误码映射对象
const { errorCodes } = this
if (resultData.errorCode === 'TOKEN_INVALID') {
// 后端同事规定 TOKEN_INVALID 为TOKEN过期,需要重新登录
resetLogin()
} else {
const errorCode = resultData.errorCode
// 如果在传入errorCodes映射表中找不到,那么就直接显示返回的errorCode
const errorText = errorCodes[errorCode] || errorCode
// dealSelf 为调用http方法时传入,值为true,则不使用通用处理方式
errorCodes.dealSelf || errorTip(errorText)
}
// 最后要把返回的错误抛出去,让外界能用catch捕捉到
reject(resultData)
}
/**
* 响应拦截器
*/
responseInterceptor(resolve, reject, result) {
// responseAdapter 为调用http方式时传入的响应适配器
// 用于在后端返回的json不符合要求时,可以使用此方法重构json
const { responseAdapter } = this
// 返回的数据
const resultData = result.data
// 后端同事规定,只要接口正常,就不会返回200以外的问题
if (result.status === 200) {
if (resultData.isSuccess) {
// 当isSuccess为true时,表示成功,则返回对应的内容
resolve(responseAdapter(resultData))
} else {
// 当isSuccess为false时,则表示请求出现了问题
// 我们需要根据返回的错误码做出不同的处理
this.handleError(reject, resultData)
}
} else {
errorTip(`未知错误 ${result}`)
}
}
initConfig() {
// 构造请求头
this.createRequestHeaders()
// 构造请求参数
this.createRequestParams()
// 组装构造好的请求头、请求参数、请求配置放到 this.requestConfig 里
this.requestInterceptor()
}
// 发送请求
request() {
return new Promise((resolve, reject) => {
axios(this.requestConfig).then((result) => {
// 如果请求成功,则统一处理TOKEN过期、错误码映射、结果返回等问题
this.responseInterceptor(resolve, reject, result)
}).catch((error) => {
// 后端规定,凡是200以外的状态码全部是服务端问题,前端不需要处理
errorTip('服务端未响应,请检查网络或稍后重试')
reject(error)
})
})
}
http({ url, params, method = null, errorCodes = {}, responseAdapter = data => data }) {
// 参数传入后会放到实例属性里,方便其他方法调用
this.url = url
this.params = params
this.method = method
this.errorCodes = errorCodes
this.responseAdapter = responseAdapter
// 初始化请求配置
this.initConfig()
// 发送请求
return this.request()
}
}
export default CommonHttp
为什么要用面向对象来写?
-
比如说我们在开发中,需要提交多媒体内容,那么我们就可以继承
CommonHttp
类来实现一个新的类import CommonHttp from './http' class HttpFormMultipart extends CommonHttp { constructor() { super() this.headerContentType = 'multipart/form-data' } createRequestParams() { const formData = new FormData() Object.keys(this.params).forEach((objKey) => { formData.append(objKey, this.params[objKey]) }) this.params = formData } } export default HttpFormMultipart
-
在使用时,只需要实例化 HttpFormMultipart 类就可以了
import HttpFormMultipart from '../HttpFormMultipart' const httpFormMultipart = new HttpFormMultipart() modifyUser(params) { return httpFormMultipart.http({ url: '/user/avatar', params, }) }