在Vue项目中,和后台交互数据这块,咱们一般使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。它有不少优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防护XSRF等。因此咱们的尤大大也是果断放弃了对其官方库vue_resource的维护,直接推荐咱们使用axios库。若是还对axios不了解的,能够移步axios文档前端
npm install axios; // 安装axios
复制代码
通常我会在项目的src目录中,新建一个request文件夹,而后在里面新建一个http.js和一个api.js文件。http.js文件用来封装咱们的axios,api.js用来统一管理咱们的接口。vue
// 在http.js中引入axios import axios from 'axios'; // 引入axios import QS from 'qs'; // 引入qs模块,用来序列化post类型的数据,后面会提到 // vant的toast提示框组件,你们可根据本身的UI组件更改 import {Toast} from 'vant' 复制代码
咱们的项目环境可能有开发环境、测试环境和生产环境。咱们经过node的环境变量来匹配咱们的默认的接口url前缀。axios.defaults.baseURL能够设置axios的默认请求地址就很少说了。node
// 环境的切换 if (process.env.NODE_ENV === 'development') { axios.defaults.baseURL = 'http://www.baidu.com'; } else if (process.env.NODE_ENV === 'debug') { axios.defaults.baseURL = 'https://www.ceshi.com'; } else if (process.env.NODE_ENV === 'production') { axios.defaults.baseURL = 'https://www.production.com' } 复制代码
经过axios.defaults.timeout设置默认的请求超时时间。例如超过了10s,就会告知用户当前请求超时,请刷新等。ios
axios.defaults.timeout = 10000;
复制代码
post请求的时候,咱们须要加上一个请求头,因此能够在这里进行一个默认的设置,即设置post的请求头为application/x-www-form-urlencoded;charset=UTF-8
ajax
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; 复制代码
咱们在发送请求前能够进行一个请求的拦截,为何要拦截呢,咱们拦截请求是用来作什么的呢?好比,有些请求时用户须要登陆以后才能访问的,或者post请求的时候,咱们须要序列化咱们提价的数据。这时候,咱们能够在请求被发送以前进行一个拦截,从而进行咱们想要的操做。vuex
// 先导入vuex,由于咱们要使用到里面的状态对象 // vuex的路径根据本身的路径去写; import store from '@/store/index' // 请求拦截器 axios.interceptors.request.use( config => { // 每次发送请求以前判断vuex中是否存在token // 若是存在,则统一在http请求的header都加上token,这样后台根据token判断你的登陆状况 // 即便本地存在token,也有可能token是过时的,因此在响应拦截器中要对返回状态进行判断 const token = store.state.token; token && (config.headers.Authorization = token); return config; }, error => { return Promise.error(error); }) 复制代码
这里说一下token,通常是在登陆完成以后,将用户的token经过localStorage或者cookie存在本地,而后用户每次在进入页面的时候(即在main.js中),会首先从本地存储中读取token,若是token存在说明用户已经登录过,则更新vuex中的token状态。而后,在没档次请求接口的时候,都会在请求的header中携带token,后台人员就能够根据你携带的token来判断你的登陆是否过时,若是没有携带,则说明没有登陆过。这时候或许有些小伙伴就会有疑问了,就是每一个请求都携带token,那么要是一个页面不须要用户登陆就能够访问的怎么办呢?其实,你前端的请求能够携带token,可是后台能够选择不接受啊!npm
// 响应器拦截 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: Toast({ message: '登陆过时,请从新登陆', duration: 1000, forbidClick: true }); // 清除token localStorage.removeItem('token'); store.commit('loginSuccess', null); // 跳转登陆页面,并将要浏览的页面fullPath传过去,登陆成功后跳转须要访问的页面 setTimeout(() => { router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }); }, 1000); break; // 404请求不存在 case 404: Toast({ message: '网络请求不存在', duration: 1500, forbidClick: true }); break; // 其余错误,直接抛出错误提示 default: Toast({ message: error.response.data.message, duration: 1500, forbidClick: true }); } return Promise.reject(error.response); } } }); 复制代码
响应拦截器很好理解,就是服务器返回给咱们的数据,咱们在拿到以前能够对他进行一些处理。例如上面的思想:若是后台返回的状态码是200,则正常返回数据,不然的根据错误的状态码类型进行一些咱们须要的错误,其实这里主要就是进行了错误的统一处理和没登陆或登陆过时后调整登陆页的一个操做。json
要注意的是,上面的Toast()方法,是我引入的vant库中的toast轻提示组件,你根据你的ui库,对应使用你的一个提示组件。axios
咱们经常使用的ajax请求方法有get、post、put等方法,相信小伙伴都不会陌生。axios对应的也有不少相似的方法,不清楚的能够看下文档。可是为了简化咱们的代码,咱们仍是要对其进行一个简单的封装。下面咱们主要封装两个方法:get和post。api
get方法: 咱们经过定义一个get函数,get函数有两个参数,第一个参数表示咱们要请求的url地址,第二个参数是咱们要携带的请求参数。get函数返回一个promise对象,当axios其请求成功时,resolve服务器返回值,请求失败时reject错误值。最后经过export抛出get函数
/** * get方法,对应get请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */ export function get(url, params){ return new Promise((resolve, reject) =>{ axios.get(url, { params: params }).then(res => { resolve(res.data); }).catch(err =>{ reject(err.data) }) });} 复制代码
post: 原理同get基本同样,可是要注意的是,post方法必需要使用对提交从参数对象进行序列化的操做,因此这里咱们经过node的qs模块来序列化咱们的参数。这个很重要,若是没有序列化操做,后台是拿不到你提交的数据的。这就是文章开头咱们import QS from 'qs';的缘由。若是不明白序列化是什么意思的,就百度一下吧,答案一大堆。
/** * post方法,对应post请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */ export function post(url, params) { return new Promise((resolve, reject) => { axios.post(url, QS.stringify(params)) .then(res => { resolve(res.data); }) .catch(err =>{ reject(err.data) }) }); } 复制代码
这里有个小细节说下,axios.get()
方法和axios.post()
在提交数据时参数的书写方式仍是有区别的。区别就是,get的第二个参数是一个{},而后这个对象的params属性值是一个参数对象的。而post的第二个参数就是一个参数对象。二者略微的区别要留意哦!
整齐的api就像电路板同样,即便再复杂也能很清晰整个线路。上面说了,咱们会新建一个api.js,而后再整个文件中存放咱们全部的api接口。
/** * api接口统一管理 */ import { get, post } from './http' 复制代码
如今,例如咱们有这样一个接口,是一个post请求:
http://www.baiodu.com/api/v1/users/my_address/address_edit_before
复制代码
咱们能够在api.js中这样封装:
export const apiAddress = p => post('api/v1/users/my_address/address_edit_before', p); 复制代码
咱们定义了一个apiAddress
方法,整个方法有一个参数p,p是咱们请求接口时携带的参数对象。然后调用了咱们封装的post
方法,post
方法的第一个参数是咱们的接口地址,第二个参数是apiAddress
的p参数,即请求接口时携带的参数对象。最后经过export导出apiAddress
而后再咱们的页面中能够这样调用咱们的api接口:
import { apiAddress } from '@/request/api';// 导入咱们的api接口 export default { name: 'Address', created () { this.onLoad(); }, methods: { // 获取数据 onLoad() { // 调用api接口,而且提供了两个参数 apiAddress({ type: 0, sort: 1 }).then(res => { // 获取数据成功后的其余操做 ……………… }) } } } 复制代码
其余的api接口,就在api.js中继续往下面扩展就能够了。友情提示,为每一个接口写好注释哦!!!
api接口管理的一个好处就是,咱们把api统一集中起来,若是后期须要修改接口就,咱们就直接在api.js中找到对应的修改就行了,而不用去每个页面查找咱们的接口而后再修改会很麻烦。关键是,万一修改的量比较大,就规格gg了。还有就是若是直接在咱们的业务代码修改接口,一不当心还容易动到咱们的业务代码形成没必要要的麻烦。
好了,最后把完成的axios封装代码奉上。
/**axios封装 * 请求拦截、相应拦截、错误统一处理 */ import axios from 'axios';import QS from 'qs'; import { Toast } from 'vant'; import store from '../store/index' // 环境的切换 if (process.env.NODE_ENV == 'development') { axios.defaults.baseURL = '/api'; } else if (process.env.NODE_ENV == 'debug') { axios.defaults.baseURL = ''; } else if (process.env.NODE_ENV == 'production') { axios.defaults.baseURL = 'http://api.123dailu.com/'; } // 请求超时时间 axios.defaults.timeout = 10000; // post请求头 axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; // 请求拦截器 axios.interceptors.request.use( config => { // 每次发送请求以前判断是否存在token,若是存在,则统一在http请求的header都加上token,不用每次请求都手动添加了 // 即便本地存在token,也有可能token是过时的,因此在响应拦截器中要对返回状态进行判断 const token = store.state.token; token && (config.headers.Authorization = token); return config; }, error => { return Promise.error(error); }) // 响应拦截器 axios.interceptors.response.use( response => { if (response.status === 200) { return Promise.resolve(response); } else { return Promise.reject(response); } }, // 服务器状态码不是200的状况 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: Toast({ message: '登陆过时,请从新登陆', duration: 1000, forbidClick: true }); // 清除token localStorage.removeItem('token'); store.commit('loginSuccess', null); // 跳转登陆页面,并将要浏览的页面fullPath传过去,登陆成功后跳转须要访问的页面 setTimeout(() => { router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }); }, 1000); break; // 404请求不存在 case 404: Toast({ message: '网络请求不存在', duration: 1500, forbidClick: true }); break; // 其余错误,直接抛出错误提示 default: Toast({ message: error.response.data.message, duration: 1500, forbidClick: true }); } return Promise.reject(error.response); } } ); /** * get方法,对应get请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */ export function get(url, params){ return new Promise((resolve, reject) =>{ axios.get(url, { params: params }) .then(res => { resolve(res.data); }) .catch(err => { reject(err.data) }) }); } /** * post方法,对应post请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */ export function post(url, params) { return new Promise((resolve, reject) => { axios.post(url, QS.stringify(params)) .then(res => { resolve(res.data); }) .catch(err => { reject(err.data) }) }); } 复制代码
若是喜欢,就给个❤❤吧(^▽^)
axios的封装根据需求的不一样而不一样。这里很是感谢一些同事很中肯的建议,我也对此进行了思考和针对不一样需求的改善。主要有如下改变:
http.js中axios封装的优化,先直接贴代码:
/** * axios封装 * 请求拦截、响应拦截、错误统一处理 */ import axios from 'axios'; import router from '../router'; import store from '../store/index'; import { Toast } from 'vant'; /** * 提示函数 * 禁止点击蒙层、显示一秒后关闭 */ const tip = msg => { Toast({ message: msg, duration: 1000, forbidClick: true }); } /** * 跳转登陆页 * 携带当前页面路由,以期在登陆页面完成登陆后返回当前页面 */ const toLogin = () => { router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }); } /** * 请求失败后的错误统一处理 * @param {Number} status 请求失败的状态码 */ const errorHandle = (status, other) => { // 状态码判断 switch (status) { // 401: 未登陆状态,跳转登陆页 case 401: toLogin(); break; // 403 token过时 // 清除token并跳转登陆页 case 403: tip('登陆过时,请从新登陆'); localStorage.removeItem('token'); store.commit('loginSuccess', null); setTimeout(() => { toLogin(); }, 1000); break; // 404请求不存在 case 404: tip('请求的资源不存在'); break; default: console.log(other); }} // 建立axios实例 var instance = axios.create({ timeout: 1000 * 12}); // 设置post请求头 instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; /** * 请求拦截器 * 每次请求前,若是存在token则在请求头中携带token */ instance.interceptors.request.use( config => { // 登陆流程控制中,根据本地是否存在token判断用户的登陆状况 // 可是即便token存在,也有可能token是过时的,因此在每次的请求头中携带token // 后台根据携带的token判断用户的登陆状况,并返回给咱们对应的状态码 // 然后咱们能够在响应拦截器中,根据状态码进行一些统一的操做。 const token = store.state.token; token && (config.headers.Authorization = token); return config; }, error => Promise.error(error)) // 响应拦截器 instance.interceptors.response.use( // 请求成功 res => res.status === 200 ? Promise.resolve(res) : Promise.reject(res), // 请求失败 error => { const { response } = error; if (response) { // 请求已发出,可是不在2xx的范围 errorHandle(response.status, response.data.message); return Promise.reject(response); } else { // 处理断网的状况 // eg:请求超时或断网时,更新state的network状态 // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏 // 关于断网组件中的刷新从新获取数据,会在断网组件中说明 if (!window.navigator.onLine) { store.commit('changeNetwork', false); } else { return Promise.reject(error); } } }); export default instance; 复制代码
这个axios和以前的大同小异,作了以下几点改变:
1.去掉了以前get和post方法的封装,经过建立一个axios实例然而后export default方法处处,这样使用起来更灵活一些。
2.去掉了经过环境变量控制baseUrl的值。考虑到接口会有多个不一样域名的状况,因此准备经过js变量来控制接口域名。这点具体在api里会介绍。
3.增长了请求超时,即断网状态的处理。说下思路,当断网时,经过更新vuex中network的状态来控制断网提示组件的显示隐藏。断网提示通常会有从新加载数据的操做,这步会在后面对应的地方介绍。
4.公共函数进行抽出,简化代码,尽可能保证单一职责原则。
下面说下api这块,考虑到如下需求:
1.更加模块化
2.更方便多人开发,有效减小解决命名冲突
3.处理接口域名有多个状况
这里呢新建了一个api文件夹,里面有一个index.js和一个base.js,以及多个根据模块划分的接口js文件。index.js是一个api的出口,base.js管理接口域名,其余js则用来管理各个模块的接口。
先放index.js代码:
/** * api接口的统一出口 */ // 文章模块接口 import article from '@/api/article'; // 其余模块的接口…… // 导出接口 export default { article, // …… } 复制代码
index.js是一个api接口的出口,这样就能够把api接口根据功能划分为多个模块,利于多人协做开发,好比一我的只负责一个模块的开发等,还能方便每一个模块中接口的命名哦。
base.js:
/** * 接口域名的管理 */ const base = { sq: 'https://xxxx111111.com/api/v1', bd: 'http://xxxxx22222.com/api' } export default base; 复制代码
经过base.js来管理咱们的接口域名,无论有多少个均可以经过这里进行接口的定义。即便修改起来,也是很方便的。
最后就是接口模块的说明,例如上面的article.js:
/** * article模块接口列表 */ import base from './base'; // 导入接口域名列表 import axios from '@/utils/http'; // 导入http中建立的axios实例 import qs from 'qs'; // 根据需求是否导入qs模块 const article = { // 新闻列表 articleList () { return axios.get(`${base.sq}/topics`); }, // 新闻详情,演示 articleDetail (id, params) { return axios.get(`${base.sq}/topic/${id}`, { params: params }); }, // post提交 login (params) { return axios.post(`${base.sq}/accesstoken`, qs.stringify(params)); } // 其余接口………… } export default article; 复制代码
1.经过直接引入咱们封装好的axios实例,而后定义接口、调用axios实例并返回,能够更灵活的使用axios,好比你能够对post请求时提交的数据进行一个qs序列化的处理等。
2.请求的配置更灵活,你能够针对某个需求进行一个不一样的配置。关于配置的优先级,axios文档说的很清楚,这个顺序是:在 lib/defaults.js
找到的库的默认值,而后是实例的 defaults
属性,最后是请求的 config
参数。后者将优先于前者。
3.restful风格的接口,也能够经过这种方式灵活的设置api接口地址。
最后,为了方便api的调用,咱们须要将其挂载到vue的原型上。在main.js中:
import Vue from 'vue' import App from './App' import router from './router' // 导入路由文件 import store from './store' // 导入vuex文件 import api from './api' // 导入api接口 Vue.prototype.$api = api; // 将api挂载到vue的原型上 复制代码
而后咱们能够在页面中这样调用接口,eg:
methods: { onLoad(id) { this.$api.article.articleDetail(id, { api: 123 }).then(res=> { // 执行某些操做 }) } } 复制代码
再提一下断网的处理,这里只作一个简单的示例:
<template> <div id="app"> <div v-if="!network"> <h3>我没网了</h3> <div @click="onRefresh">刷新</div> </div> <router-view/> </div> </template> <script> import { mapState } from 'vuex'; export default { name: 'App', computed: { ...mapState(['network']) }, methods: { // 经过跳转一个空页面再返回的方式来实现刷新当前页面数据的目的 onRefresh () { this.$router.replace('/refresh') } } } </script> 复制代码
这是app.vue,这里简单演示一下断网。在http.js中介绍了,咱们会在断网的时候,来更新vue中network的状态,那么这里咱们根据network的状态来判断是否须要加载这个断网组件。断网状况下,加载断网组件,不加载对应页面的组件。当点击刷新的时候,咱们经过跳转refesh页面而后当即返回的方式来实现从新获取数据的操做。所以咱们须要新建一个refresh.vue页面,并在其beforeRouteEnter
钩子中再返回当前页面。
// refresh.vue beforeRouteEnter (to, from, next) { next(vm => { vm.$router.replace(from.fullPath) }) } 复制代码
这是一种全局通用的断网提示,固然了,也能够根据本身的项目需求操做。具体操做就仁者见仁智者见智了。
若是更多的需求,或者说是不同的需求,能够根据本身的需求进行一个改进。
若是感受对你有帮助,那就收藏❤❤吧!(此文借鉴他人及作此我的记录)