介绍

学习了** axios 的源码,重构并优化了一些内容。**

特点:

  • 支持 Promise
  • 支持全局、局部(实例)的请求和响应拦截器
  • 支持添加多个、删除拦截器,并在响应拦截器中添加发送请求的 config 数据
  • 支持添加配置文件
  • 支持所有 method 方法
  • 支持 params 参数属性
  • 支持文件上传和下载
  • 支持重新发送请求(刷新 token)

推荐更新到最新版本

兼容性问题、设置**超时时间,**请查看官方文档官方文档: https://uniapp.dcloud.io/api/request/request?id=request

下载地址

推荐先下载示例项目
github: https://github.com/2460392754/uniapp-tools/tree/master/request
dcloud: https://ext.dcloud.net.cn/plugin?id=468

更新日志

https://ext.dcloud.net.cn/plugin?id=468&update_log

1.兼容性测试

✅微信小程序
✅h5
✅android
❓其他平台(没测试)
❗跨域会直接进入catch

2.目录结构

  1. demo项目
  2. ├── api api数据
  3. └── test.js
  4. ├── components
  5. └── print.vue
  6. ├── pages
  7. ├── index
  8. └── index.vue
  9. └── method 运行demo
  10. ├── download.vue
  11. ├── get_200.vue
  12. ├── get_400.vue
  13. ├── post_200.vue
  14. └── upload.vue
  15. ├── plugins
  16. └── request
  17. ├── js
  18. ├── core
  19. ├── index.js 原型扩展
  20. ├── interceptor.js 拦截器
  21. ├── mergeConfig.js 数据合并
  22. └── network.js 请求封装
  23. ├── config.js 配置文件
  24. ├── index.js 主文件
  25. └── tools.js 工具类
  26. ├── ts
  27. └── todo.txt
  28. ├── App.vue
  29. ├── main.js 插件注册到全局
  30. ├── manifest.json
  31. ├── pages.json
  32. └── README.md

3.运行流程图

“猴子也能看懂的请求运行流程图”
没有配置局部拦截器就进入下一步
uniapp-tools request (2).svg
Promise的链式合并

  1. Promise.resolve()
  2. .then(global_Request)
  3. .catch(global_Request)
  4. .then(scoped_Request)
  5. .catch(scoped_Request)
  6. .then(发送请求)
  7. .catch(请求错误、超时)
  8. .then(global_Response)
  9. .catch(global_Response)
  10. .then(scoped_Response)
  11. .catch(scoped_Response)
  12. .then(获取请求的返回值)
  13. .catch(拦截异常的返回值)

4.设置配置文件(config.js)

参数 类型 必填 默认值 说明
baseURL String
基地址
header Object 请求头,会和请求对象中的header进行合并,请求对象中header的优先级大于全局

全局参数

  1. /**
  2. * 全局配置
  3. * 只能配置 静态数据
  4. * `content-type` 默认为 application/json
  5. * header 中`content-type`设置特殊参数 或 配置其他会导致触发 跨域 问题,出现跨域会直接进入响应拦截器的catch函数中
  6. */
  7. export const config = {
  8. baseURL: 'https://www.fastmock.site/mock/7f2e97ecf7a26f51479a4a08f6c49c8b',
  9. header: {
  10. // uid: 'xxxx',
  11. contentType: 'application/x-www-form-urlencoded'
  12. // 'Content-Type': 'application/json'
  13. },
  14. }

全局拦截器

  1. /**
  2. * 全局 请求拦截器
  3. * 例如: 配置token
  4. *
  5. * `return config` 继续发送请求
  6. * `return false` 会停止发送请求,不会进入错误数据拦截,也不会进入请求对象中的catch函数中
  7. * `return Promise.reject('xxxxx')` 停止发送请求, 会错误数据拦截,也会进入catch函数中
  8. *
  9. * @param {Object} config 发送请求的配置数据
  10. */
  11. globalInterceptor.request.use(config => {
  12. console.log('is global request interceptor 1');
  13. return config;
  14. // return false;
  15. // return Promise.reject('is error')
  16. }, err => {
  17. console.error('is global fail request interceptor: ', err);
  18. return false;
  19. });
  20. // 支持添加多个请求、响应拦截器
  21. globalInterceptor.request.use(config => {
  22. console.log('is global request interceptor 2');
  23. return config;
  24. }, err => {
  25. console.error('global request: ', err);
  26. return false;
  27. });
  28. /**
  29. * 全局 响应拦截器
  30. * 例如: 根据状态码选择性拦截、过滤转换数据
  31. *
  32. * `return res` 继续返回数据
  33. * `return false` 停止返回数据,不会进入错误数据拦截,也不会进入catch函数中
  34. * `return Promise.reject('xxxxx')` 返回错误信息, 会错误数据拦截,也会进入catch函数中
  35. *
  36. * @param {Object} res 请求返回的数据
  37. * @param {Object} config 发送请求的配置数据
  38. * @return {Object|Boolean|Promise<reject>}
  39. */
  40. globalInterceptor.response.use((res, config) => {
  41. console.log('is global response interceptor');
  42. // 回传数据中没有携带 code
  43. if (!(res.data && res.data.code)) {
  44. return res;
  45. }
  46. // 用code模拟http状态码
  47. const code = parseInt(res.data.code);
  48. // 20x ~ 30x
  49. if (200 <= code && code < 400) {
  50. return res;
  51. } else {
  52. return Promise.reject(res, config);
  53. }
  54. // return false;
  55. // return Promise.reject('is error')
  56. }, (err, config) => {
  57. console.error('is global response fail interceptor');
  58. console.error('err: ', err)
  59. console.error('config: ', config)
  60. const { errMsg, data } = err;
  61. return Promise.reject({ errMsg, data, config });
  62. });

5.请求对象的参数

普通请求

参数 类型 必填 默认值 说明
url String 填写相对地址或者绝对地址
method String get 请求类型
data Object 请求的参数
header Object 请求的header,会和全局的header进行合并,优先级大于全局
params Object 设置QueryString

get方法中data和params具有同样的效果,但data优先级大于params

上传、下载

参数 类型 必填 默认值 说明
url String 填写相对地址或者绝对地址
method String get 请求类型
header Object 请求的header,会和全局的header进行合并,优先级大于全局
params Object 设置QueryString
onProgressUpdate Function 与官方相同
onHeadersReceived Function 与官方相同、注意兼容问题
offProgressUpdate Function 与官方相同、注意兼容问题
offHeadersReceived Function 与官方相同、注意兼容问题

上传

参数 类型
files Array
fileType String
filePath String
name String
formData Object

是否必填、说明、平台差异说明,与官方相同

6.如何使用

挂载到全局上

把插件挂载到全局则无法使用局部拦截器,可以自由取舍
1.挂载到 global 对象上
main.js

  1. import Request from './plugins/request/js/index'
  2. global.$http = Request();
  1. global.$http.request({
  2. url: '/test/get/400',
  3. method: 'get',
  4. params: {
  5. name: 'xxx',
  6. age: 20
  7. },
  8. }).then(res=>{
  9. // do something...
  10. }).catch(err=>{
  11. // do something...
  12. })

2.挂载到 vue 的原型上
main.js

  1. import Vue from 'vue'
  2. import Request from './plugins/request/js/index'
  3. Vue.prototype.$http = Request();
  1. this.$http.request({
  2. url: '/test/get/400',
  3. method: 'get',
  4. params: {
  5. name: 'xxx',
  6. age: 20
  7. },
  8. }).then(res=>{
  9. // do something...
  10. }).catch(err=>{
  11. // do something...
  12. })

⭐把api抽离成单独的文件

/api/xxxx.js

  1. import Request from '../plugins/request/js/index';
  2. export default {
  3. // get 请求, 200 响应码
  4. getMockDataMethodGet200 () {
  5. let r = Request();
  6. let reqId = r.interceptors.scoped.request.use(config => {
  7. console.log('is scoped request')
  8. return config;
  9. // return false;
  10. // return Promise.reject('xxx')
  11. }, err => {
  12. console.error('scoped request: ', err)
  13. // return Promise.reject(err)
  14. return false;
  15. });
  16. let repId = r.interceptors.scoped.response.use((res, config) => {
  17. console.log('is scoped response')
  18. return res;
  19. // return false;
  20. // return Promise.reject('xxx')
  21. }, err => {
  22. console.error('scoped response: ', err)
  23. return Promise.reject(err)
  24. });
  25. // 卸载 私有 请求 拦截器
  26. // instance.interceptors.scoped.request.eject(reqId)
  27. let instance = r.request({
  28. url: '/test/get/200',
  29. method: 'get',
  30. header: {
  31. // sid: 'xx',
  32. ContentType: "application/json"
  33. },
  34. params: {
  35. name: 'xxx',
  36. age: 20
  37. },
  38. data: {
  39. text: 'method type is get'
  40. }
  41. });
  42. // 超时1500ms就中断请求
  43. setTimeout(() => {
  44. r.abort(instance)
  45. }, 1500);
  46. return instance;
  47. },
  48. // get 请求, 400 响应码
  49. getMockDataMethodGet400 () {
  50. return global.$http.request({
  51. url: '/test/get/400',
  52. method: 'get',
  53. params: {
  54. name: 'xxx',
  55. age: 20
  56. },
  57. })
  58. // return Request().request({
  59. // url: '/test/get/400',
  60. // method: 'get',
  61. // params: {
  62. // name: 'xxx',
  63. // age: 20
  64. // },
  65. // });
  66. },
  67. // post 请求, 200 响应码
  68. getMockDataMethodPost200 () {
  69. return Request().post('/test/post/200', {
  70. data: {
  71. name: 'xx',
  72. age: 20,
  73. }
  74. })
  75. },
  76. // download, 200 状态码
  77. getMockDataMethodDownload () {
  78. const r = Request();
  79. return r.get('/test/download')
  80. .then(res => {
  81. console.log('is then', res);
  82. return res.data.data.img;
  83. })
  84. .then(path => r.download(path, {
  85. params: {
  86. a: 'is a'
  87. },
  88. onProgressUpdate (res) {
  89. console.log(res)
  90. }
  91. }))
  92. },
  93. // upload, 200 状态码
  94. getMockDataMethodUpload (path) {
  95. let r = Request()
  96. let instance = r.upload('/test/upload', {
  97. name: 'file',
  98. filePath: path,
  99. header: {
  100. contentType: "multipart/form-data"
  101. },
  102. formData: {
  103. text: 'is upload file'
  104. },
  105. onProgressUpdate (res) {
  106. console.log(res)
  107. }
  108. });
  109. // 5s 之后还没上传完成就中断上传,会进入 catch 回调中
  110. setTimeout(() => {
  111. r.abort(instance)
  112. }, 5000)
  113. return instance;
  114. }
  115. }

7.其他

已经给 header 中 content-type 属性名进行适配

支持以下名称

  1. 'content-type', 'Content-type', 'Content-Type', contentType, ContentType

重新发送请求(刷新Token)

(v2.0.1) 新增
推荐下载demo查看

/api/token.js

  1. import Request from '../plugins/request/js/index';
  2. export default {
  3. // 获取 模拟token
  4. getMockToken (id) {
  5. return Request().get('/test/token/get?id=' + id);
  6. },
  7. // 验证 模拟token,id为 2460392754 则 success,code 200, 否则 fail code 为 401
  8. checkMockToken (token) {
  9. return Request().request({
  10. url: '/test/token/check',
  11. method: 'get',
  12. params: { name: 'xxx' },
  13. header: { token, uid: 'ux' },
  14. count: 0 // 用来记录 每个实例请求的 请求次数(可以用来判断 重新发送请求的次数)
  15. });
  16. }
  17. }

config.js 文件

  1. import Request from './index';
  2. import TokenApi from '../.././../api/token' // 重新请求token的api
  3. globalInterceptor.response.use((res, config) => {
  4. // 用code模拟http状态码
  5. const code = parseInt(res.data.code);
  6. if (200 <= code && code < 400) { // 20x ~ 30x
  7. return res;
  8. } else if (code == 401 && config.count === 0) { // token 验证失败, 并且这个实例是第一次重复请求
  9. config.count++;
  10. config.url = config.instanceURL;
  11. return getApiToken(2460392754).then(saveToken).then(() => Request().request(config))
  12. } else {
  13. return Promise.reject(res, config);
  14. }
  15. }, (err, config) => {
  16. console.error('is global response fail interceptor');
  17. console.error('err: ', err)
  18. console.error('config: ', config)
  19. const { errMsg, data } = err;
  20. return Promise.reject({ errMsg, data, config });
  21. });
  22. // 重新请求更新获取 token
  23. async function getApiToken (uid) {
  24. const res = await TokenApi.getMockToken(uid);
  25. const { token } = res.data;
  26. return token;
  27. }
  28. // 保存 token 到 localStorage
  29. function saveToken (token) {
  30. uni.setStorageSync('token', token);
  31. }

8.问题

重新发送请求

Q: 需要使用 “重新发送请求”的功能,并且同时也需要使用 “中断请求” Request().abort() ,会出现中断的请求不是 初次发送的,而是在正在运行的重复请求。
A: 暂时没法解决这个问题。如果可以就使用拦截器替代这个功能。

9.疑问

1.拦截器是什么、有什么用、怎么运行的

Q:是什么?
A:axios里功能的一部分,能够帮助解决一些重复性的问题

Q:有什么用?
A:

  • 请求拦截器的作用是在请求发送前进行一些操作,例如在每个请求体里加上token,统一做了处理如果以后要改也非常容易。
  • 响应拦截器的作用是在接收到响应后进行一些操作,例如在服务器返回登录状态失效,需要重新登录的时候,跳转到登录页、根据http状态码或者自定义code做出一些异常拦截并自定义错误信息、数据过滤等。

Q:怎么运行的?
A:查看运行流程图

2.为什么会发送options请求、为什么会跨域、什么是cors

header中设置其他特殊字段,或者content-type设置为json或其他类型,就会先发送 options 请求进行与服务器验证,验证失败就会出现跨域问题。

cors的全名叫做跨域资源共享,一般的简单请求是不会触发cors,设置了一写特殊字段就会先发送 options 请求了。在服务端配置一些数据就可以通过验证了。(怎么解决这个问题问后端,不行再google)

文档
mdn的options介绍
mdn的cors介绍

3.为什么没有添加cookie功能

注意非 H5 端不支持 cookie,服务器应避免验证 cookie。如果服务器无法修改,也可以使用一些模拟手段,比如这样的工具https://github.com/charleslo1/weapp-cookie 按照 W3C 规范,H5 端无法获取 response header 中 Set-Cookie、Set-Cookie2 这2个字段,对于跨域请求,允许获取的 response header 字段只限于“simple response header”和“Access-Control-Expose-Headers”(详情

这段话来自于uniapp官网
(主要我也不想再踩坑了)

4.为什么有些axios的功能或者属性没有实现

1.目前这个插件已经能够满足绝大部分了
2.axios的有些功能我也没有用过
3.uniapp的封装限制、或者运行环境限制

5.为什么不根据自己需求造一个轮子,而是要根据axios的源码进行重构

如果大家有时间,我还是比较推荐去阅读一下axios的源码。第一,可以了解axios的运行原理;第二,也可以提升自己。回到问题,我上个版本的插件其实已经到达难以维护和阅读的状态了,所以我想根据一些优秀的插件学习封装技巧和原理并且重构一份,这样也可以尽量降低其他开发者使用此插件的学习成本。

6.为什么要把一个插件拆分成多个文件

这样易于代码的维护和阅读

7.为什么不支持回调函数了,只支持Promise

1.js的es6出来到现在已经好几年了,需要与时俱进
2.多次嵌套使用回调函数真的很难维护和阅读代码!.

8.为什么demo能过正常运行,copy到自己的项目里却运行不了

1.可能是插件的引用路径问题
2.config.js 里有一些默认的demo没有删除或者注释掉
3.拼错单词
4.不知道该如何使用

9.为什么要用皮卡丘作为头像或者封面

因为可爱

最后

出现bug了?
遇到无法解决的问题?
直接加我q或者发邮件也行

email: 2460392754@qq.com
qq: 2460392754

欢迎交流或探讨问题!

喜欢插件的,可以给插件一个5⭐好评吗

土豪老板也可随意打赏

mm_reward_qrcode_1571378923544 _200.png