2022年10月25日

封装原理

前端异步请求封装 - 图1

uniapp 封装

方式一,全功能,结合Promise,自定义拦截器

  1. // 默认方式
  2. uni.request({
  3. url: "https://www.example.com/request",
  4. success: (res) => {
  5. console.log(res.data);
  6. },
  7. fail: (err) => {
  8. console.error(err);
  9. },
  10. });
  11. // 使用 Promise then/catch 方式调用
  12. uni
  13. .request({
  14. url: "https://www.example.com/request",
  15. })
  16. .then((res) => {
  17. // 此处的 res 参数,与使用默认方式调用时 success 回调中的 res 参数一致
  18. console.log(res.data);
  19. })
  20. .catch((err) => {
  21. // 此处的 err 参数,与使用默认方式调用时 fail 回调中的 err 参数一致
  22. console.error(err);
  23. });
  24. // 使用 Async/Await 方式调用
  25. async function request() {
  26. try {
  27. var res = await uni.request({
  28. url: "https://www.example.com/request",
  29. });
  30. // 此处的 res 参数,与使用默认方式调用时 success 回调中的 res 参数一致
  31. console.log(res);
  32. } catch (err) {
  33. // 此处的 err 参数,与使用默认方式调用时 fail 回调中的 err 参数一致
  34. console.error(err);
  35. }
  36. }
  1. // 可以从外部引入baseURL,也可以定义到config下的baseURL中
  2. import baseURL from '@/common/baseURL.js'
  3. export default {
  4. config: {
  5. baseURL: baseURL,
  6. // 处理token的方法
  7. getToken() {
  8. let token = uni.getStorageSync('token');
  9. if (!token) {
  10. return uni.reLaunch({
  11. url: '/pages/login/login'
  12. })
  13. }
  14. return token
  15. },
  16. // 请求拦截器
  17. beforeRequest(options = {}) {
  18. return new Promise((resolve, reject) => {
  19. options.url = this.baseURL + options.url;
  20. options.method = options.method || 'GET';
  21. // 封装自己的请求头
  22. options.header = {
  23. token: this.getToken()
  24. }
  25. resolve(options)
  26. })
  27. },
  28. // 响应拦截器
  29. beforeResponse(data) {
  30. return new Promise((resolve, reject) => {
  31. const [err, res] = data;
  32. // 处理错误
  33. if (res && res.statusCode !== 200) {
  34. let msg = res.data.msg || '请求错误';
  35. uni.showToast({
  36. icon: 'none',
  37. title: msg
  38. })
  39. return reject(msg);
  40. }
  41. if (err) {
  42. uni.showToast({
  43. icon: 'none',
  44. title: '请求错误'
  45. })
  46. return reject(err);
  47. }
  48. return resolve(res.data)
  49. })
  50. }
  51. },
  52. // request 请求
  53. request(options = {}) {
  54. return this.config.beforeRequest(options) // 1、请求拦截器
  55. .then((opt) => {
  56. return uni.request(opt) // 2、发送请求
  57. })
  58. .then((res) => this.config.beforeResponse(res)) // 3、响应拦截器
  59. }
  60. }

方式二,uniapp 默认拦截器

  1. uni.request({
  2. url: 'request/login', //仅为示例,并非真实接口地址。
  3. success: (res) => {
  4. console.log(res.data);
  5. // 打印: {code:1,...}
  6. }
  7. });
  8. // 拦截 request 请求
  9. uni.addInterceptor('request', {
  10. invoke(args) {
  11. // request 触发前拼接 url
  12. args.url = 'https://www.example.com/'+args.url
  13. },
  14. success(args) {
  15. // 请求成功后,修改code值为1
  16. args.data.code = 1
  17. },
  18. fail(err) {
  19. // 失败回调拦截
  20. console.log('interceptor-fail',err)
  21. },
  22. complete(res) {
  23. // 完成回调拦截
  24. console.log('interceptor-complete',res)
  25. }
  26. })

方式三,包装为一个函数

  1. async function ajax(method, datalist, param, header, successFunc, errFunc, isLoading) {
  2. let url = LinkUrl + datalist;
  3. var _this = this;
  4. if (isLoading === true) {
  5. loading();
  6. }
  7. var datalist = datalist;
  8. var myDate = new Date();
  9. var requestTime = myDate.getFullYear() + '-' + (myDate.getMonth() + 1) + '-' + myDate.getDate() + ' ' + myDate
  10. .getHours() + ':' + myDate.getMinutes() + ':' + myDate.getSeconds();
  11. var strval = '+6Hp9X5zR39SOI6oP0685Bk77gG56m7PkV89xYvl86A=' + datalist + requestTime
  12. var customerKey = md5.hexMD5(strval)
  13. try {
  14. var value = uni.getStorageSync('key')
  15. if (value) {
  16. data['token'] = value.token;
  17. var headeritem = value.token;
  18. }
  19. } catch (e) {
  20. console.log(e)
  21. }
  22. param.site_id = uni.getStorageSync('site_id') ? uni.getStorageSync('site_id') : '0';
  23. //#ifdef H5
  24. param.be_from = 'H5';
  25. //#endif
  26. //#ifdef MP-WEIXIN
  27. param.be_from = 'program';
  28. //#endif
  29. param.customerID = '8001';
  30. param.customerKey = customerKey;
  31. param.requestTime = requestTime;
  32. param.Method = datalist;
  33. var _errFunc = function(_msg) {
  34. hideLoading();
  35. if (isLoading === true) {
  36. hideLoading();
  37. }
  38. if (!!errFunc) {
  39. errFunc(_msg || '操作失败,请稍后重试');
  40. }
  41. }
  42. var header;
  43. if (method == 'POST') {
  44. header = {
  45. 'content-type': 'application/x-www-form-urlencoded',
  46. 'token': headeritem
  47. }
  48. } else {
  49. header = {
  50. 'content-type': 'multipart/form-data',
  51. token: headeritem
  52. };
  53. }
  54. uni.request({
  55. url: url,
  56. data: param,
  57. method: method,
  58. header: header,
  59. success(res) {
  60. if (res.data.code == 401) {
  61. //#ifdef H5
  62. window.location.href = res.data.data;
  63. //#endif
  64. }
  65. if (res.data) {
  66. if (isLoading === true) {
  67. uni.hideLoading();
  68. }
  69. successFunc(res.data);
  70. }
  71. },
  72. fail(res) {
  73. console.log(res)
  74. _errFunc();
  75. }
  76. })
  77. }

axios 封装

  1. // 请求超时时间
  2. axios.defaults.timeout = 120000
  3. // 添加请求拦截器
  4. // 请求拦截器的作用是在请求发送前进行一些操作,例如在每个请求体里加上token,
  5. // 统一做了处理如果以后要改也非常容易。
  6. axios.interceptors.request.use(
  7. config => {
  8. // 在发送请求之前做些什么
  9. // 判断是否存在token,如果存在将每个页面header都添加token
  10. if (window.sessionStorage.getItem('DT')) {
  11. // 请求头配置全局token
  12. config.headers.DT = window.sessionStorage.getItem('DT')
  13. }
  14. return config
  15. },
  16. err => {
  17. // 对请求错误做些什么
  18. Vue.prototype.$message.error('请求超时')
  19. return Promise.reject(err)
  20. }
  21. )
  1. // 响应拦截器
  2. axios.interceptors.response.use(
  3. response => {
  4. // res是响应的结果
  5. switch (response.data.code) {
  6. case 401:
  7. // 登录失效
  8. // 响应成功的拦截
  9. console.log('响应拦截器:')
  10. // console.log(response.data)
  11. Vue.prototype.$message.error(response.data.message)
  12. sessionStorage.removeItem('DT')
  13. router.push('/login')
  14. break
  15. case 404:
  16. if (response.data.message !== null) {
  17. Vue.prototype.$message.error(response.data.message)
  18. } else {
  19. Vue.prototype.$message.error('未知错误')
  20. }
  21. break
  22. case 500:
  23. // 错误
  24. if (response.data.message !== null) {
  25. Vue.prototype.$message.error(response.data.message)
  26. } else {
  27. Vue.prototype.$message.error('未知错误')
  28. }
  29. break
  30. default:
  31. return response
  32. }
  33. return response
  34. },
  35. err => {
  36. if (err.response.data.message) {
  37. Vue.prototype.$message.error(err.response.data.message)
  38. return Promise.reject(err.response.data.message) // 返回接口返回的错误信息
  39. } else {
  40. // 返回状态码不为200时候的错误处理
  41. Vue.prototype.$message.error(err.toString())
  42. return Promise.resolve(err)
  43. }
  44. }
  45. )
  1. // 用户登录提交
  2. login() {
  3. // debugger
  4. this.$refs.loginFormRef.validate(async valid => {
  5. if (!valid) {
  6. return
  7. }
  8. if (valid) {
  9. const userInfo = {
  10. username: this.loginForm.username,
  11. password: this.loginForm.password
  12. }
  13. // 登录之前去除token
  14. window.sessionStorage.removeItem('DT')
  15. const {
  16. data: res
  17. } = await this.$http.post('/system/login', userInfo)
  18. if (res.code !== 200) {
  19. this.initCaptcha()
  20. return
  21. }
  22. this.$message.success('登录成功')
  23. window.sessionStorage.setItem('DT', res.result.token)
  24. this.$router.push('/main')
  25. }
  26. })
  27. }

在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库, 可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、 客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。
1、安装axios

  1. npm install axios; // 安装axios

2、一般我会在项目的src目录中,新建一个request文件夹,然后在里面新建一个http.js 和一个api.js文件。http.js文件用来封装我们的axios,api.js用来统一管理我们的接口。

  1. // http.js
  2. import axios from 'axios'; // 引入axios
  3. import QS from 'qs'; // 引入qs模块,用来序列化post类型的数据,后面会提到
  4. // vant的toast提示框组件,大家可根据自己的ui组件更改。
  5. import { Toast } from 'vant';
  6. // 环境的切换
  7. // 我们的项目环境可能有开发环境、测试环境和生产环境。我们通过node的环境变量来匹配我们的
  8. // 默认的接口url前缀。axios.defaults.baseURL可以设置axios的默认请求地址就不多说了。
  9. if (process.env.NODE_ENV == 'development') {
  10. axios.defaults.baseURL = 'https://www.baidu.com';}
  11. else if (process.env.NODE_ENV == 'debug') {
  12. axios.defaults.baseURL = 'https://www.ceshi.com';
  13. }
  14. else if (process.env.NODE_ENV == 'production') {
  15. axios.defaults.baseURL = 'https://www.production.com';
  16. }
  17. // 设置请求超时通过axios.defaults.timeout设置默认的请求超时时间。
  18. // 例如超过了10s,就会告知用户当前请求超时,请刷新等。
  19. axios.defaults.timeout = 10000;
  20. // post请求头的设置post请求的时候,我们需要加上一个请求头,
  21. // 所以可以在这里进行一个默认的设置,
  22. // 即设置post的请求头为application/x-www-form-urlencoded;charset=UTF-8
  23. axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
  24. // 请求拦截
  25. // 我们在发送请求前可以进行一个请求的拦截,为什么要拦截呢,我们拦截请求是用来做什么的呢?
  26. // 比如,有些请求是需要用户登录之后才能访问的,或者post请求的时候,我们需要序列化我们提交的数据。
  27. // 这时候,我们可以在请求被发送之前进行一个拦截,从而进行我们想要的操作。
  28. // 先导入vuex,因为我们要使用到里面的状态对象
  29. // vuex的路径根据自己的路径去写
  30. import store from '@/store/index';
  31. // 请求拦截器
  32. axios.interceptors.request.use(
  33. config => {
  34. // 每次发送请求之前判断vuex中是否存在token
  35. // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况
  36. // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
  37. const token = store.state.token;
  38. token && (config.headers.Authorization = token);
  39. return config;
  40. },
  41. error => {
  42. return Promise.error(error);
  43. })
  44. // 这里说一下token,一般是在登录完成之后,将用户的token通过localStorage或者cookie存在本地,
  45. // 然后用户每次在进入页面的时候(即在main.js中),会首先从本地存储中读取token,
  46. // 如果token存在说明用户已经登陆过,则更新vuex中的token状态。然后,在每次请求接口的时候,
  47. // 都会在请求的header中携带token,后台人员就可以根据你携带的token来判断你的登录是否过期,
  48. // 如果没有携带,则说明没有登录过。这时候或许有些小伙伴会有疑问了,就是每个请求都携带token,
  49. // 那么要是一个页面不需要用户登录就可以访问的怎么办呢?其实,你前端的请求可以携带token,
  50. // 但是后台可以选择不接收啊!
  51. // 响应拦截器
  52. axios.interceptors.response.use(
  53. response => {
  54. // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
  55. // 否则的话抛出错误
  56. if (response.status === 200) {
  57. return Promise.resolve(response);
  58. } else {
  59. return Promise.reject(response);
  60. }
  61. },
  62. // 服务器状态码不是2开头的的情况
  63. // 这里可以跟你们的后台开发人员协商好统一的错误状态码
  64. // 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等
  65. // 下面列举几个常见的操作,其他需求可自行扩展
  66. error => {
  67. if (error.response.status) {
  68. switch (error.response.status) {
  69. // 401: 未登录
  70. // 未登录则跳转登录页面,并携带当前页面的路径
  71. // 在登录成功后返回当前页面,这一步需要在登录页操作。
  72. case 401:
  73. router.replace({
  74. path: '/login',
  75. query: {
  76. redirect: router.currentRoute.fullPath
  77. }
  78. });
  79. break;
  80. // 403 token过期
  81. // 登录过期对用户进行提示
  82. // 清除本地token和清空vuex中token对象
  83. // 跳转登录页面
  84. case 403:
  85. Toast({
  86. message: '登录过期,请重新登录',
  87. duration: 1000,
  88. forbidClick: true
  89. });
  90. // 清除token
  91. localStorage.removeItem('token');
  92. store.commit('loginSuccess', null);
  93. // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转
  94. // 需要访问的页面
  95. setTimeout(() => {
  96. router.replace({
  97. path: '/login',
  98. query: {
  99. redirect: router.currentRoute.fullPath
  100. }
  101. });
  102. }, 1000);
  103. break;
  104. // 404请求不存在
  105. case 404:
  106. Toast({
  107. message: '网络请求不存在',
  108. duration: 1500,
  109. forbidClick: true
  110. });
  111. break;
  112. // 其他错误,直接抛出错误提示
  113. default:
  114. Toast({
  115. message: error.response.data.message,
  116. duration: 1500,
  117. forbidClick: true
  118. });
  119. }
  120. return Promise.reject(error.response);
  121. }
  122. }
  123. });
  124. // 响应拦截器很好理解,就是服务器返回给我们的数据,我们在拿到之前可以对他进行一些处理。
  125. // 例如上面的思想:如果后台返回的状态码是200,则正常返回数据,否则的根据错误的状态码
  126. // 类型进行一些我们需要的错误,其实这里主要就是进行了错误的统一处理和没登录或登录过期后
  127. // 调整登录页的一个操作。
  128. // 要注意的是,上面的Toast()方法,是我引入的vant库中的toast轻提示组件,你根据你的ui库,
  129. // 对应使用你的一个提示组件。
  130. // 封装get方法和post方法
  131. // 我们常用的ajax请求方法有get、post、put等方法,相信小伙伴都不会陌生。axios对应的也有很多
  132. // 类似的方法,不清楚的可以看下文档。但是为了简化我们的代码,我们还是要对其进行一个简单的封装。
  133. // 下面我们主要封装两个方法:get和post。
  134. // get方法:我们通过定义一个get函数,get函数有两个参数,第一个参数表示我们要请求的url地址,
  135. // 第二个参数是我们要携带的请求参数。get函数返回一个promise对象,当axios其请求成功时
  136. // resolve服务器返回 值,请求失败时reject错误值。最后通过export抛出get函数。
  137. /**
  138. * get方法,对应get请求
  139. * @param {String} url [请求的url地址]
  140. * @param {Object} params [请求时携带的参数]
  141. */
  142. export function get(url, params){
  143. return new Promise((resolve, reject) =>{
  144. axios.get(url, {
  145. params: params
  146. }).then(res => {
  147. resolve(res.data);
  148. }).catch(err =>{
  149. reject(err.data)
  150. })
  151. });
  152. }
  153. // post方法:原理同get基本一样,但是要注意的是,post方法必须要使用对提交从参数对象进行
  154. // 序列化的操作,所以这里我们通过node的qs模块来序列化我们的参数。这个很重要,
  155. // 如果没有序列化操作,后台是拿不到你提交的数据的。这就是文章开头我们
  156. // import QS from 'qs';的原因。
  157. /**
  158. * post方法,对应post请求
  159. * @param {String} url [请求的url地址]
  160. * @param {Object} params [请求时携带的参数]
  161. */
  162. export function post(url, params) {
  163. return new Promise((resolve, reject) => {
  164. axios.post(url, QS.stringify(params))
  165. .then(res => {
  166. resolve(res.data);
  167. })
  168. .catch(err =>{
  169. reject(err.data)
  170. })
  171. });
  172. }
  173. // 这里有个小细节说下,axios.get()方法和axios.post()在提交数据时参数的书写方式还是有区别的。
  174. // 区别就是,get的第二个参数是一个{},然后这个对象的params属性值是一个参数对象的。
  175. // 而post的第二个参数就是一个参数对象。两者略微的区别要留意哦!
  176. // axios的封装基本就完成了,下面再简单说下api的统一管理。
  177. // --------------------------------
  178. // api.js
  179. // 整齐的api就像电路板一样,即使再复杂也能很清晰整个线路。上面说了,我们会新建一个api.js,
  180. // 然后在这个文件中存放我们所有的api接口。
  181. // 首先我们在api.js中引入我们封装的get和post方法
  182. /**
  183. * api接口统一管理
  184. */
  185. import { get, post } from './http'
  186. // 现在,例如我们有这样一个接口,是一个post请求:
  187. // http://www.baiodu.com/api/v1/users/my_address/address_edit_before
  188. // 我们可以在api.js中这样封装:
  189. export const apiAddress = p => post('api/v1/users/my_address/address_edit_before', p);
  190. // 我们定义了一个apiAddress方法,这个方法有一个参数p,p是我们请求接口时携带的参数对象。
  191. // 而后调用了我们封装的post方法,post方法的第一个参数是我们的接口地址,
  192. // 第二个参数是apiAddress的p参数,即请求接口时携带的参数对象。
  193. // 最后通过export导出apiAddress。
  194. // 然后在我们的页面中可以这样调用我们的api接口:
  195. import { apiAddress } from '@/request/api';// 导入我们的api接口
  196. export default {
  197. name: 'Address',
  198. created () {
  199. this.onLoad();
  200. },
  201. methods: {
  202. // 获取数据
  203. onLoad() {
  204. // 调用api接口,并且提供了两个参数
  205. apiAddress({
  206. type: 0,
  207. sort: 1
  208. }).then(res => {
  209. // 获取数据成功后的其他操作
  210. // ………………
  211. })
  212. }
  213. }
  214. }
  215. // 其他的api接口,就在pai.js中继续往下面扩展就可以了。友情提示,为每个接口写好注释哦!!!
  216. // api接口管理的一个好处就是,我们把api统一集中起来,如果后期需要修改接口,
  217. // 我们就直接在api.js中找到对应的修改就好了,而不用去每一个页面查找我们的接口
  218. // 然后再修改会很麻烦。关键是,万一修改的量比较大,就规格gg了。还有就是如果直接在
  219. // 我们的业务代码修改接口,一不小心还容易动到我们的业务代码造成不必要的麻烦。
  220. // 好了,最后把完成的axios封装代码奉上。
  221. /**axios封装
  222. * 请求拦截、相应拦截、错误统一处理
  223. */
  224. import axios from 'axios';import QS from 'qs';
  225. import { Toast } from 'vant';
  226. import store from '../store/index'
  227. // 环境的切换
  228. if (process.env.NODE_ENV == 'development') {
  229. axios.defaults.baseURL = '/api';
  230. } else if (process.env.NODE_ENV == 'debug') {
  231. axios.defaults.baseURL = '';
  232. } else if (process.env.NODE_ENV == 'production') {
  233. axios.defaults.baseURL = 'http://api.123dailu.com/';
  234. }
  235. // 请求超时时间
  236. axios.defaults.timeout = 10000;
  237. // post请求头
  238. axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
  239. // 请求拦截器
  240. axios.interceptors.request.use(
  241. config => {
  242. // 每次发送请求之前判断是否存在token,如果存在,
  243. // 则统一在http请求的header都加上token,不用每次请求都手动添加了
  244. // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
  245. const token = store.state.token;
  246. token && (config.headers.Authorization = token);
  247. return config;
  248. },
  249. error => {
  250. return Promise.error(error);
  251. })
  252. // 响应拦截器
  253. axios.interceptors.response.use(
  254. response => {
  255. if (response.status === 200) {
  256. return Promise.resolve(response);
  257. } else {
  258. return Promise.reject(response);
  259. }
  260. },
  261. // 服务器状态码不是200的情况
  262. error => {
  263. if (error.response.status) {
  264. switch (error.response.status) {
  265. // 401: 未登录
  266. // 未登录则跳转登录页面,并携带当前页面的路径
  267. // 在登录成功后返回当前页面,这一步需要在登录页操作。
  268. case 401:
  269. router.replace({
  270. path: '/login',
  271. query: { redirect: router.currentRoute.fullPath }
  272. });
  273. break;
  274. // 403 token过期
  275. // 登录过期对用户进行提示
  276. // 清除本地token和清空vuex中token对象
  277. // 跳转登录页面
  278. case 403:
  279. Toast({
  280. message: '登录过期,请重新登录',
  281. duration: 1000,
  282. forbidClick: true
  283. });
  284. // 清除token
  285. localStorage.removeItem('token');
  286. store.commit('loginSuccess', null);
  287. // 跳转登录页面,并将要浏览的页面fullPath传过去,
  288. //登录成功后跳转需要访问的页面
  289. setTimeout(() => {
  290. router.replace({
  291. path: '/login',
  292. query: {
  293. redirect: router.currentRoute.fullPath
  294. }
  295. });
  296. }, 1000);
  297. break;
  298. // 404请求不存在
  299. case 404:
  300. Toast({
  301. message: '网络请求不存在',
  302. duration: 1500,
  303. forbidClick: true
  304. });
  305. break;
  306. // 其他错误,直接抛出错误提示
  307. default:
  308. Toast({
  309. message: error.response.data.message,
  310. duration: 1500,
  311. forbidClick: true
  312. });
  313. }
  314. return Promise.reject(error.response);
  315. }
  316. }
  317. );
  318. /**
  319. * get方法,对应get请求
  320. * @param {String} url [请求的url地址]
  321. * @param {Object} params [请求时携带的参数]
  322. */
  323. export function get(url, params){
  324. return new Promise((resolve, reject) =>{
  325. axios.get(url, {
  326. params: params
  327. })
  328. .then(res => {
  329. resolve(res.data);
  330. })
  331. .catch(err => {
  332. reject(err.data)
  333. })
  334. });
  335. }
  336. /**
  337. * post方法,对应post请求
  338. * @param {String} url [请求的url地址]
  339. * @param {Object} params [请求时携带的参数]
  340. */
  341. export function post(url, params) {
  342. return new Promise((resolve, reject) => {
  343. axios.post(url, QS.stringify(params))
  344. .then(res => {
  345. resolve(res.data);
  346. })
  347. .catch(err => {
  348. reject(err.data)
  349. })
  350. });
  351. }
  352. // **********************************
  353. // axios的封装根据需求的不同而不同。
  354. // 1.优化axios封装,去掉之前的get和post
  355. // 2.断网情况处理
  356. // 3.更加模块化的api管理
  357. // 4.接口域名有多个的情况
  358. // 5.api挂载到vue.prototype上省去引入的步骤
  359. // http.js中axios封装的优化,先直接贴代码:
  360. /**
  361. * axios封装
  362. * 请求拦截、响应拦截、错误统一处理
  363. */
  364. import axios from 'axios';
  365. import router from '../router';
  366. import store from '../store/index';
  367. import { Toast } from 'vant';
  368. /**
  369. * 提示函数
  370. * 禁止点击蒙层、显示一秒后关闭
  371. */
  372. const tip = msg => {
  373. Toast({
  374. message: msg,
  375. duration: 1000,
  376. forbidClick: true
  377. });
  378. }
  379. /**
  380. * 跳转登录页
  381. * 携带当前页面路由,以期在登录页面完成登录后返回当前页面
  382. */
  383. const toLogin = () => {
  384. router.replace({
  385. path: '/login',
  386. query: {
  387. redirect: router.currentRoute.fullPath
  388. }
  389. });
  390. }
  391. /**
  392. * 请求失败后的错误统一处理
  393. * @param {Number} status 请求失败的状态码
  394. */
  395. const errorHandle = (status, other) => {
  396. // 状态码判断
  397. switch (status) {
  398. // 401: 未登录状态,跳转登录页
  399. case 401:
  400. toLogin();
  401. break;
  402. // 403 token过期
  403. // 清除token并跳转登录页
  404. case 403:
  405. tip('登录过期,请重新登录');
  406. localStorage.removeItem('token');
  407. store.commit('loginSuccess', null);
  408. setTimeout(() => {
  409. toLogin();
  410. }, 1000);
  411. break;
  412. // 404请求不存在
  413. case 404:
  414. tip('请求的资源不存在');
  415. break;
  416. default:
  417. console.log(other);
  418. }
  419. }
  420. // 创建axios实例
  421. var instance = axios.create({ timeout: 1000 * 12});
  422. // 设置post请求头
  423. instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
  424. /**
  425. * 请求拦截器
  426. * 每次请求前,如果存在token则在请求头中携带token
  427. */
  428. instance.interceptors.request.use(
  429. config => {
  430. // 登录流程控制中,根据本地是否存在token判断用户的登录情况
  431. // 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token
  432. // 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码
  433. // 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。
  434. const token = store.state.token;
  435. token && (config.headers.Authorization = token);
  436. return config;
  437. },
  438. error => Promise.error(error))
  439. // 响应拦截器
  440. instance.interceptors.response.use(
  441. // 请求成功
  442. res => res.status === 200 ? Promise.resolve(res) : Promise.reject(res),
  443. // 请求失败
  444. error => {
  445. const { response } = error;
  446. if (response) {
  447. // 请求已发出,但是不在2xx的范围
  448. errorHandle(response.status, response.data.message);
  449. return Promise.reject(response);
  450. } else {
  451. // 处理断网的情况
  452. // eg:请求超时或断网时,更新state的network状态
  453. // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
  454. // 关于断网组件中的刷新重新获取数据,会在断网组件中说明
  455. if (!window.navigator.onLine) {
  456. store.commit('changeNetwork', false);
  457. } else {
  458. return Promise.reject(error);
  459. }
  460. }
  461. });
  462. export default instance;
  463. // 这个axios和之前的大同小异,做了如下几点改变:
  464. 1.去掉了之前getpost方法的封装,通过创建一个axios实例然后export default方法导出,
  465. 这样使用起来更灵活一些。
  466. 2.去掉了通过环境变量控制baseUrl的值。考虑到接口会有多个不同域名的情况,所以准备通过js变量
  467. 来控制接口域名。这点具体在api里会介绍。
  468. 3.增加了请求超时,即断网状态的处理。说下思路,当断网时,通过更新vuexnetwork的状态来
  469. 控制断网提示组件的显示隐藏。断网提示一般会有重新加载数据的操作,这步会在后面对应的地方介绍。
  470. 4.公用函数进行抽出,简化代码,尽量保证单一职责原则。
  471. 下面说下api这块,考虑到一下需求:
  472. 1.更加模块化
  473. 2.更方便多人开发,有效减少解决命名冲突
  474. 3.处理接口域名有多个情况
  475. 这里这里呢新建了一个api文件夹,里面有一个index.js和一个base.js,以及多个根据模块
  476. 划分的接口js文件。index.js是一个api的出口,base.js管理接口域名,其他js则用来管理
  477. 各个模块的接口。
  478. // 先放index.js代码:
  479. /**
  480. * api接口的统一出口
  481. */
  482. // 文章模块接口
  483. import article from '@/api/article';
  484. // 其他模块的接口……
  485. // 导出接口
  486. export default {
  487. article,
  488. // ……
  489. }
  490. index.js是一个api接口的出口,这样就可以把api接口根据功能划分为多个模块,
  491. 利于多人协作开发,比如一个人只负责一个模块的开发等,还能方便每个模块中接口的命名哦。
  492. base.js:
  493. /**
  494. * 接口域名的管理
  495. */
  496. const base = {
  497. sq: 'https://xxxx111111.com/api/v1',
  498. bd: 'http://xxxxx22222.com/api'
  499. }
  500. export default base;
  501. 通过base.js来管理我们的接口域名,不管有多少个都可以通过这里进行接口的定义。
  502. 即使修改起来,也是很方便的。
  503. 最后就是接口模块的说明,例如上面的article.js:
  504. /**
  505. * article模块接口列表
  506. */
  507. import base from './base'; // 导入接口域名列表
  508. import axios from '@/utils/http'; // 导入http中创建的axios实例
  509. import qs from 'qs'; // 根据需求是否导入qs模块
  510. const article = {
  511. // 新闻列表
  512. articleList () {
  513. return axios.get(`${base.sq}/topics`);
  514. },
  515. // 新闻详情,演示
  516. articleDetail (id, params) {
  517. return axios.get(`${base.sq}/topic/${id}`, {
  518. params: params
  519. });
  520. },
  521. // post提交
  522. login (params) {
  523. return axios.post(`${base.sq}/accesstoken`, qs.stringify(params));
  524. }
  525. // 其他接口…………
  526. }
  527. export default article;
  528. 1.通过直接引入我们封装好的axios实例,然后定义接口、调用axios实例并返回,
  529. 可以更灵活的使用axios,比如你可以对post请求时提交的数据进行一个qs序列化的处理等。
  530. 2.请求的配置更灵活,你可以针对某个需求进行一个不同的配置。关于配置的优先级,
  531. axios文档说的很清楚,这个顺序是:在 lib/defaults.js 找到的库的默认值,
  532. 然后是实例的 defaults 属性,最后是请求的 config 参数。后者将优先于前者。
  533. 3.restful风格的接口,也可以通过这种方式灵活的设置api接口地址。
  534. 最后,为了方便api的调用,我们需要将其挂载到vue的原型上。在main.js中:
  535. import Vue from 'vue'
  536. import App from './App'
  537. import router from './router' // 导入路由文件
  538. import store from './store' // 导入vuex文件
  539. import api from './api' // 导入api接口
  540. Vue.prototype.$api = api; // 将api挂载到vue的原型上
  541. 然后我们可以在页面中这样调用接口,eg
  542. methods: {
  543. onLoad(id) {
  544. this.$api.article.articleDetail(id, {
  545. api: 123
  546. }).then(res=> {
  547. // 执行某些操作
  548. })
  549. }
  550. }
  551. 再提一下断网的处理,这里只做一个简单的示例:
  552. <template>
  553. <div id="app">
  554. <div v-if="!network">
  555. <h3>我没网了</h3>
  556. <div @click="onRefresh">刷新</div>
  557. </div>
  558. <router-view/>
  559. </div>
  560. </template>
  561. <script>
  562. import { mapState } from 'vuex';
  563. export default {
  564. name: 'App',
  565. computed: {
  566. ...mapState(['network'])
  567. },
  568. methods: {
  569. // 通过跳转一个空页面再返回的方式来实现刷新当前页面数据的目的
  570. onRefresh () {
  571. this.$router.replace('/refresh')
  572. }
  573. }
  574. }
  575. </script>
  576. // 这是app.vue,这里简单演示一下断网。在http.js中介绍了,我们会在断网的时候,
  577. // 来更新vue中network的状态,那么这里我们根据network的状态来判断是否需要加载这
  578. // 个断网组件。断网情况下,加载断网组件,不加载对应页面的组件。当点击刷新的时候,
  579. // 我们通过跳转refesh页面然后立即返回的方式来实现重新获取数据的操作。因此我们需要
  580. // 新建一个refresh.vue页面,并在其beforeRouteEnter钩子中再返回当前页面。
  581. // refresh.vue
  582. beforeRouteEnter (to, from, next) {
  583. next(vm => {
  584. vm.$router.replace(from.fullPath)
  585. })
  586. }

(完)