介绍
学习了** 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.目录结构
demo项目├── api api数据│ └── test.js├── components│ └── print.vue├── pages│ ├── index│ │ └── index.vue│ └── method 运行demo│ ├── download.vue│ ├── get_200.vue│ ├── get_400.vue│ ├── post_200.vue│ └── upload.vue├── plugins│ └── request│ ├── js│ │ ├── core│ │ │ ├── index.js 原型扩展│ │ │ ├── interceptor.js 拦截器│ │ │ ├── mergeConfig.js 数据合并│ │ │ └── network.js 请求封装│ │ ├── config.js 配置文件│ │ ├── index.js 主文件│ │ └── tools.js 工具类│ ├── ts│ └── todo.txt├── App.vue├── main.js 插件注册到全局├── manifest.json├── pages.json└── README.md
3.运行流程图
“猴子也能看懂的请求运行流程图”
没有配置局部拦截器就进入下一步
Promise的链式合并
Promise.resolve().then(global_Request).catch(global_Request).then(scoped_Request).catch(scoped_Request).then(发送请求).catch(请求错误、超时).then(global_Response).catch(global_Response).then(scoped_Response).catch(scoped_Response).then(获取请求的返回值).catch(拦截异常的返回值)
4.设置配置文件(config.js)
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| baseURL | String | 基地址 | ||
| header | Object | 请求头,会和请求对象中的header进行合并,请求对象中header的优先级大于全局 |
全局参数
/*** 全局配置* 只能配置 静态数据* `content-type` 默认为 application/json* header 中`content-type`设置特殊参数 或 配置其他会导致触发 跨域 问题,出现跨域会直接进入响应拦截器的catch函数中*/export const config = {baseURL: 'https://www.fastmock.site/mock/7f2e97ecf7a26f51479a4a08f6c49c8b',header: {// uid: 'xxxx',contentType: 'application/x-www-form-urlencoded'// 'Content-Type': 'application/json'},}
全局拦截器
/*** 全局 请求拦截器* 例如: 配置token** `return config` 继续发送请求* `return false` 会停止发送请求,不会进入错误数据拦截,也不会进入请求对象中的catch函数中* `return Promise.reject('xxxxx')` 停止发送请求, 会错误数据拦截,也会进入catch函数中** @param {Object} config 发送请求的配置数据*/globalInterceptor.request.use(config => {console.log('is global request interceptor 1');return config;// return false;// return Promise.reject('is error')}, err => {console.error('is global fail request interceptor: ', err);return false;});// 支持添加多个请求、响应拦截器globalInterceptor.request.use(config => {console.log('is global request interceptor 2');return config;}, err => {console.error('global request: ', err);return false;});/*** 全局 响应拦截器* 例如: 根据状态码选择性拦截、过滤转换数据** `return res` 继续返回数据* `return false` 停止返回数据,不会进入错误数据拦截,也不会进入catch函数中* `return Promise.reject('xxxxx')` 返回错误信息, 会错误数据拦截,也会进入catch函数中** @param {Object} res 请求返回的数据* @param {Object} config 发送请求的配置数据* @return {Object|Boolean|Promise<reject>}*/globalInterceptor.response.use((res, config) => {console.log('is global response interceptor');// 回传数据中没有携带 codeif (!(res.data && res.data.code)) {return res;}// 用code模拟http状态码const code = parseInt(res.data.code);// 20x ~ 30xif (200 <= code && code < 400) {return res;} else {return Promise.reject(res, config);}// return false;// return Promise.reject('is error')}, (err, config) => {console.error('is global response fail interceptor');console.error('err: ', err)console.error('config: ', config)const { errMsg, data } = err;return Promise.reject({ errMsg, data, config });});
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
import Request from './plugins/request/js/index'global.$http = Request();
global.$http.request({url: '/test/get/400',method: 'get',params: {name: 'xxx',age: 20},}).then(res=>{// do something...}).catch(err=>{// do something...})
2.挂载到 vue 的原型上
main.js
import Vue from 'vue'import Request from './plugins/request/js/index'Vue.prototype.$http = Request();
this.$http.request({url: '/test/get/400',method: 'get',params: {name: 'xxx',age: 20},}).then(res=>{// do something...}).catch(err=>{// do something...})
⭐把api抽离成单独的文件
/api/xxxx.js
import Request from '../plugins/request/js/index';export default {// get 请求, 200 响应码getMockDataMethodGet200 () {let r = Request();let reqId = r.interceptors.scoped.request.use(config => {console.log('is scoped request')return config;// return false;// return Promise.reject('xxx')}, err => {console.error('scoped request: ', err)// return Promise.reject(err)return false;});let repId = r.interceptors.scoped.response.use((res, config) => {console.log('is scoped response')return res;// return false;// return Promise.reject('xxx')}, err => {console.error('scoped response: ', err)return Promise.reject(err)});// 卸载 私有 请求 拦截器// instance.interceptors.scoped.request.eject(reqId)let instance = r.request({url: '/test/get/200',method: 'get',header: {// sid: 'xx',ContentType: "application/json"},params: {name: 'xxx',age: 20},data: {text: 'method type is get'}});// 超时1500ms就中断请求setTimeout(() => {r.abort(instance)}, 1500);return instance;},// get 请求, 400 响应码getMockDataMethodGet400 () {return global.$http.request({url: '/test/get/400',method: 'get',params: {name: 'xxx',age: 20},})// return Request().request({// url: '/test/get/400',// method: 'get',// params: {// name: 'xxx',// age: 20// },// });},// post 请求, 200 响应码getMockDataMethodPost200 () {return Request().post('/test/post/200', {data: {name: 'xx',age: 20,}})},// download, 200 状态码getMockDataMethodDownload () {const r = Request();return r.get('/test/download').then(res => {console.log('is then', res);return res.data.data.img;}).then(path => r.download(path, {params: {a: 'is a'},onProgressUpdate (res) {console.log(res)}}))},// upload, 200 状态码getMockDataMethodUpload (path) {let r = Request()let instance = r.upload('/test/upload', {name: 'file',filePath: path,header: {contentType: "multipart/form-data"},formData: {text: 'is upload file'},onProgressUpdate (res) {console.log(res)}});// 5s 之后还没上传完成就中断上传,会进入 catch 回调中setTimeout(() => {r.abort(instance)}, 5000)return instance;}}
7.其他
已经给 header 中 content-type 属性名进行适配
支持以下名称
'content-type', 'Content-type', 'Content-Type', contentType, ContentType
重新发送请求(刷新Token)
(v2.0.1) 新增
推荐下载demo查看
/api/token.js
import Request from '../plugins/request/js/index';export default {// 获取 模拟tokengetMockToken (id) {return Request().get('/test/token/get?id=' + id);},// 验证 模拟token,id为 2460392754 则 success,code 200, 否则 fail code 为 401checkMockToken (token) {return Request().request({url: '/test/token/check',method: 'get',params: { name: 'xxx' },header: { token, uid: 'ux' },count: 0 // 用来记录 每个实例请求的 请求次数(可以用来判断 重新发送请求的次数)});}}
config.js 文件
import Request from './index';import TokenApi from '../.././../api/token' // 重新请求token的apiglobalInterceptor.response.use((res, config) => {// 用code模拟http状态码const code = parseInt(res.data.code);if (200 <= code && code < 400) { // 20x ~ 30xreturn res;} else if (code == 401 && config.count === 0) { // token 验证失败, 并且这个实例是第一次重复请求config.count++;config.url = config.instanceURL;return getApiToken(2460392754).then(saveToken).then(() => Request().request(config))} else {return Promise.reject(res, config);}}, (err, config) => {console.error('is global response fail interceptor');console.error('err: ', err)console.error('config: ', config)const { errMsg, data } = err;return Promise.reject({ errMsg, data, config });});// 重新请求更新获取 tokenasync function getApiToken (uid) {const res = await TokenApi.getMockToken(uid);const { token } = res.data;return token;}// 保存 token 到 localStoragefunction saveToken (token) {uni.setStorageSync('token', token);}
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)
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
欢迎交流或探讨问题!
土豪老板也可随意打赏

