介绍
学习了** 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');
// 回传数据中没有携带 code
if (!(res.data && res.data.code)) {
return res;
}
// 用code模拟http状态码
const code = parseInt(res.data.code);
// 20x ~ 30x
if (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 {
// 获取 模拟token
getMockToken (id) {
return Request().get('/test/token/get?id=' + id);
},
// 验证 模拟token,id为 2460392754 则 success,code 200, 否则 fail code 为 401
checkMockToken (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的api
globalInterceptor.response.use((res, config) => {
// 用code模拟http状态码
const code = parseInt(res.data.code);
if (200 <= code && code < 400) { // 20x ~ 30x
return 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 });
});
// 重新请求更新获取 token
async function getApiToken (uid) {
const res = await TokenApi.getMockToken(uid);
const { token } = res.data;
return token;
}
// 保存 token 到 localStorage
function 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
欢迎交流或探讨问题!