开始之前
最近开始对原来做过的项目复盘一下,找找以前项目中的存在的优缺点,该保留的保留,需要改进的改进。大多数的项目是在饿了么组件库上开发的。既然用了这个组件库当然也一定会借鉴用用该组件库开发的一些实例,我们项目搭建借鉴了很多 vue-element-admin 这个项目,对于一个用 vue 搭建的项目,vue-element-admin 官网上裤衩哥讲的特别好,所以做如果想用 vue 做一个企业后台管理项目推荐手动撸一下这个代码,个人感觉收获还是蛮大的。这次和大家分享的是这个项目中一个小的地方——封装的axios 和 api 接口的统一管理。
axios
对于 vue 项目和后台交互获取数据这一块,我接触的所有项目都是用 axios 库,它是基于 promise 的 http,它又以下几个特点:
- 支持浏览器和node.js
- 支持promise
- 能拦截请求和响应
- 能转换请求和响应数据
- 能取消请求
- 自动转换JSON数据
- 浏览器端支持防止CSRF(跨站请求伪造)
对于向后台发起的请求和接收的响应,我们肯定是要处理一下来简化代码,统一管理的。裤衩哥手摸手系类教程里面对这个提到了几点,比如说我们给每个请求统一添加 token;统一的异常处理;多环境的动态切换等。vue-element-admin 这个项目 utils/request.js 就是对 axios 的一个封装,我们就用这个文件来说一下他的一些配置。
引入和基本设置部分
下面的代码中引入 element-ui 的部分是用来弹窗对用户进行提示;这个项目中登录用户 token 是放在 store 中的,这里引入store是用来取token,如果没有用到就不用引入。getToken 是一个自定义获取token的方法。
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
对于create 方法中设置了 baseURL, 这个的作用就是后面英文解释的 每次请求我们写在的真实 URL就是 baseURL 与 我们以后方法中写的 request 一个拼接。举个例子:baseURL: http://localhost:8080/; 我们调用get方法 axios.get('user') 那实际请求的就是 http://localhost:8080/user. timeout 是设置了一个超时时间,设置的规定时间里面后台没有响应,就认为超时了,就抛出一个错误。
上面的代码中我们发送的每一个请求是 Content-Type 默认是 application/json;charset=utf-8
,有的时候后台的POST请求要求不是这个,比如后台要 application/x-www-form-urlencoded
这个。我们可以通过下面两种方法设置:
// 第一中在全局上设置
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// 第二种在实例上设置
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
headers:{'Content-Type':'application/x-www-form-urlencoded'}
})
上面两种方法都可以说是统一设置,但是有的项目由于种种原因有时候需要一部分设置成这种,一部分设置成为另一种,这个说到实例的方法时再说。
设置好了是设置好了,但是如果我们在传参数的时候还是按照默认的方式传一个 json
对象,后台就会接受不到,这种情况不知道你们遇没遇,我最近现在做的这个项目就遇到了,开始以为是后端的锅,结果甩来甩去,就是还是前端的问题,还浪费了一下午时间。具体什么原因,遇到这个问题的同学可以打开控制台看一下传递参数的形式,这两种是有差别的。application/json
格式是一个object的形式。application/x-www-form-urllencodes
是 key-value 的形式。改的方法也有两种,一种是每次我们发送 post 请求的时候都手动使用 qs.stringify(data) 去更改一下数据格式(qs: npm 安装一下),另一种是借助 axios transfromRequest属性来统一配置:
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
headers:{'Content-Type':'application/x-www-form-urlencoded'},
transformRequest: [function (data) {
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
return qs.stringify(data)
}],
})
请求拦截器
请求拦截器就是再请求发出拦截下来,对它做一些处理,下面这个就是对请求加上 token 的验证信息。
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['X-Token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
这里说一下token,一般是在登录完成之后,将用户的token通过localStorage或者cookie存在本地,然后用户每次在进入页面的时候(即在main.js中),会首先从本地存储中读取token,如果token存在说明用户已经登陆过,则更新vuex中的token状态。然后,在每次请求接口的时候,都会在请求的header中携带token,后台人员就可以根据你携带的token来判断你的登录是否过期,如果没有携带,则说明没有登录过。这时候或许有些小伙伴会有疑问了,就是每个请求都携带token,那么要是一个页面不需要用户登录就可以访问的怎么办呢?其实,你前端的请求可以携带token,但是后台可以选择不接收!
响应拦截器
响应拦截器很好理解,就是服务器返回给我们的数据,我们在拿到之前可以对他进行一些处理。例如下面就是多响应错误的统一处理:
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
裤衩哥这个响应拦截的就是拿到的后台返回的数据中判断 code 的状态。当然了具体用什么字段就看前端跟后端的约定了。约定好了你们爱用啥用啥,谁也管不着。
前天跟一起再武汉做过项目的同时聊天,他说他们想用服务器的状态码,不用返回数据的 code, 想了想其实也很好做,上面把 const res = response.data 去掉,直接对 response 做判断就好。比如:
// 响应拦截器
axios.interceptors.response.use(
response => {
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
// 否则的话抛出错误
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
// 服务器状态码不是2开头的的情况
// 这里可以跟你们的后台开发人员协商好统一的错误状态码
// 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等
// 下面列举几个常见的操作,其他需求可自行扩展
error => {
if (error.response.status) {
switch (error.response.status) {
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
break;
// 403 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
case 403:
Message({
message: '登录过期,请重新登录',
duration: 1000,
type: error
});
// 清除token
localStorage.removeItem('token');
store.commit('loginSuccess', null);
// 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
setTimeout(() => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
}, 1000);
break;
// 404请求不存在
case 404:
Message({
message: '网络请求不存在',
duration: 1500,
type: error
});
break;
// 其他错误,直接抛出错误提示
default:
Message({
message: error.response.data.message,
duration: 1500,
type: error
});
}
return Promise.reject(error.response);
}
}
});
响应拦截器很好理解,就是服务器返回给我们的数据,我们在拿到之前可以对他进行一些处理。例如上面的思想:如果后台返回的状态码是200,则正常返回数据,否则的根据错误的状态码类型进行一些我们需要的错误,其实这里主要就是进行了错误的统一处理和没登录或登录过期后调整登录页的一个操作。
裤衩哥整个vue-element-admin
项目都是非常好的,但是有一个地方我就很疑惑,就是在 api 接口的时候没有使用 get 和 post ,造成了对每一个借口我搜需要写一遍 method 这个参数。就像下面这样:
export function fetchArticle(id) {
return request({
url: '/article/detail',
method: 'get',
params: { id }
})
}
这个也是我最近发觉到的。因为最近在写的一个项目我在搭建这个项目的前端框架的时候这个地方就没有改,最近写多了这些重复代码了想封装一下减少代码量就想到了这里。其实我们可以直接使用 axios 自带的方法就可以了。比如上面的代码:
export function fetchAriticle(id){
return request.get('/article/detail',{
params:{id}
})
}
注意:上面是 get 请求,我们看第二个参数,它是一个对象,params是它的一个属性,这个千万不要忘记了。
对于 post 请求我们正常些即可。
裤衩哥写法:
export function createArticle(data) {
return request({
url: '/article/create',
method: 'post',
data
})
}
使用 axios.post 写法:
export function createArticle(data){
return request.post('/article/create', data)
}
项目中大多数时候是使用 post 的, 不知不觉我们每个接口就少按了13次键盘,一个项目那么多 api 接口,省下的时间够咱们泡个茶遛个弯了。
API 的统一管理
在 vue-element-admin
我们会发现在 src 下面有一个 api 文件夹,里面存放了该项目的所有的 api 接口定义。接口。api接口管理的一个好处就是,我们把api统一集中起来,如果后期需要修改接口,我们就直接在api.js中找到对应的修改就好了,而不用去每一个页面查找我们的接口然后再修改会很麻烦。关键是,万一修改的量比较大,就规格gg了。还有就是如果直接在我们的业务代码修改接口,一不小心还容易动到我们的业务代码造成不必要的麻烦。
举个例子,例如我们现在有这样一个接口:
http://localhost:8080/api/user/user_edit
现在可以在 api.js 中这样封装
export const userEdit = data => request.post('/user/user_edit', data)
我们定义了一个apiAddress方法,这个方法有一个参数data,data是我们请求接口时携带的参数对象。而后调用了post方法,post方法的第一个参数是我们的接口地址,第二个参数是userEdit的data参数,即请求接口时携带的参数对象。最后通过export导出userEdit。
然后再我们的页面中这样调用:
import { userEdit } from '@/api/api';// 导入我们的api接口
export default {
name: 'UserEdit',
methods: {
// 获取数据
submit() {
// 调用api接口,并且提供了两个参数
userEdit({
name: 'zhangsan',
age: 18
}).then(res => {
// 获取数据成功后的其他操作
………………
})
}
}
}
其他的api接口,就在api.js中继续往下面扩展就可以了。我们的项目后台都是用swagger,这个里面后台的同事会写好一些注释和说明,如果你们的项目也有类似的API管理工具,前端的接口定义最好和后台的接口统一起来,这样就共享一份注释信息(哈哈 咱们前端就偷个懒)。如果没有,最好为每个接口写好注释。
这样其实也就差不多了,但是呢我们每次在使用接口的时候还要有一个引用的过程,感觉这一块也是可以省略的。我在武汉做一个项目的时候,武汉的同事就对这个做了一些改进,使这个更加模块化,使用起来也更加方便。现在把思路跟大家说一下:
- 根据后台的接口划分,前台也建立一个一个模块的 api 文件,比如 user 模块,全部放置 user 相关的 api 接口。
- 在 api 文件下面建立一个 index.js 作为全部 api 接口的出口。
- 将 api 挂在到 vue.prototype 上面。
// user.js user模块api文件
export const userEdit = data => request.post('/user/user_edit', data)
// api.index.js 所有模块的出口文件
// 用户模块接口
import user from '@/api/user';
// 其他模块的接口...
// 导出接口
export default {
user,
//....
}
// main.js
import api from '@/api';
// 将 api 挂在 vue.prototype 上
Vue.prototype.$api = api
以后再页面中调用就不用先导入了,直接使用 this.$api.user.userEdit
就可以了。
还有一个地方是如果一个项目 由多个服务构成,不同服务接口路径不一样,又没有网管层的处理,我们可以设置一个文件定义不同服务的路径。例如:
/**
* 接口域名的管理
*/
const base = {
sq: 'https://xxxx111111.com/api/v1',
bd: 'http://xxxxx22222.com/api'
}
export default base;
我们再在 api 接口文件使用 (这样在创建axios实例的时候就不要指定 baseURL 了)
// user.js user模块api文件
import base from './base';
export const userEdit = data => request.post(`${base.sq}/user/user_edit`, data)
写在最后
今天不用禁足了,去公司办公一天,坐回了自己熟悉的座位,见到了好久没见的可爱的同事们,心情是非常的好。美中不足的地方是还没吃上公司原来可口的饭菜。感谢裤衩哥奉献的vue-element-admin
和手膜手系列教程。建议大家看一遍。如果不想看伟哥也可以手摸手教你,男的就算了/:B-)。分享使我快乐,希望能和大家能在前端学习的道路一起进步。
对了还有一个遗留问题,就是有时候项目中个别接口会有配置 比如说 content-type,这个该怎么办,我把 axios 官网上的例子放到这里大家就明白了。
配置的优先顺序
配置会以一个优先顺序进行合并。这个顺序是:在 lib/defaults.js
找到的库的默认值,然后是实例的 defaults
属性,最后是请求的 config
参数。后者将优先于前者。这里是一个例子:
// 使用由库提供的配置的默认值来创建实例
// 此时超时配置的默认值是 `0`
var instance = axios.create();
// 覆写库的超时默认值
// 现在,在超时前,所有请求都会等待 2.5 秒
instance.defaults.timeout = 2500;
// 为已知需要花费很长时间的请求覆写超时设置
instance.get('/longRequest', {
timeout: 5000
});
// post 一样
instance.post('/longRequest', data,{
timeout: 5000
});