网络请求封装
网络请求模块难度较大,如果学习起来感觉吃力,可以直接学习
[请求封装-使用 npm 包发送请求]
以后的模块
01. 为什么要封装 wx.request
小程序大多数 API 都是异步 API,如 wx.request(),wx.login() 等。这类 API 接口通常都接收一个 Object
对象类型的参数,参数中可以按需指定以下字段来接收接口调用结果:
参数名 | 类型 | 必填 | 说明 |
---|---|---|---|
success | function | 否 | 调用成功的回调函数 |
fail | function | 否 | 调用失败的回调函数 |
complete | function | 否 | 调用结束的回调函数(调用成功、失败都会执行) |
wx.request({// 接口调用成功的回调函数success() {wx.request({success() {wx.request({success() {wx.request({success() {wx.request({success() {wx.request({success() {wx.request({success() {wx.request({success() {}})}})}})}})}})}})}})},// 接口调用失败的回调函数fail() {},// 接口调用结束的回调函数(调用成功、失败都会执行)complete() {}
})
如果采用这种回调函数的方法接收返回的值,可能会出现多层 success
套用的情况,容易出现回调地狱问题,
为了解决这个问题,小程序基础库从 2.10.2 版本起,异步 API 支持 callback & promise 两种调用方式。
当接口参数 Object 对象中不包含 success/fail/complete 时,将默认返回 promise,否则仍按回调方式执行,无返回值。
但是部分接口如 downloadFile
, request
, uploadFile
等本身就有返回值,因此不支持 promise 调用方式,它们的 promisify 需要开发者自行封装。
Axios
是我们日常开发中常用的一个基于 promise 的网络请求库
我们可以参考 Axios
的 [使用方式] 来封装自己的网络请求模块,咱们看一下使用的方式:
网络请求模块封装
import WxRequest from 'mina-request'// 自定义配置新建一个实例
const instance = new WxRequest(({baseURL: 'https://some-domain.com/api/',timeout: 1000,headers: {'X-Custom-Header': 'foobar'}
})// 通过 instance.request(config) 方式发起网络请求
instance.requst({method: 'post',url: '/user/12345',data: {firstName: 'Fred',lastName: 'Flintstone'}
})// 通过 instance.get 方式发起网络请求
instance.get(url, data, config)// 通过 instance.delete 方式发起网络请求
instance.delete(url, data, config)// 通过 instance.post 方式发起网络请求
instance.post(url, data, config)// 通过 instance.put 方式发起网络请求
instance.put(url, data, config)// ----------------------------------------------// 添加请求拦截器
instance.interceptors.request = (config) => {// 在发送请求之前做些什么return config
}// 添加响应拦截器
instance.interceptors.response = (response) => {// response.isSuccess = true,代码执行了 wx.request 的 success 回调函数// response.isSuccess = false,代码执行了 wx.request 的 fail 回调函数// response.statusCode // http 响应状态码// response.config // 网络请求请求参数// response.data 服务器响应的真正数据// 对响应数据做点什么return response
}
封装后网络请求模块包含以下功能
- 包含 request 实例方法发送请求
- 包含 get、delete、put、post 等实例方法可以快捷的发送网络请求
- 包含 请求拦截器、响应拦截器
- 包含 uploadFile 将本地资源上传到服务器 API
- 包含 all 并发请求方法
- 同时优化了并发请求时 loading 显示效果
02. 请求封装-request 方法
思路分析:
在封装网络请求模块的时候,采用 Class
类来进行封装,采用类的方式封装代码更具可复用性,也方便地添加新的方法和属性,提高代码的扩展性
我们先创建一个 class 类,同时定义 constructor 构造函数
// 创建 WxRequest 类
class WxRequest {constructor() {}
}
我们在 WxRequest
类内部封装一个 request
实例方法
request
实例方法中需要使用 Promise
封装 wx.request,也就是使用 Promise
处理 wx.request
的返回结果
request
实例方法接收一个 options
对象作为形参,options
参数和调用 wx.request
时传递的请求配置项一致
- 接口调用成功时,通过
resolve
返回响应数据 - 接口调用失败时,通过
reject
返回错误原因
class WxRequest {// 定义 constructor 构造函数,用于创建和初始化类的属性和方法constructor() {}/*** @description 发起请求的方法* @param { Object} options 请求配置选项,同 wx.request 请求配置选项* @returns Promise*/request(options) {// 使用 Promise 封装异步请求return new Promise((resolve, reject) => {// 使用 wx.request 发起请求wx.request({...options,// 接口调用成功的回调函数success: (res) => {resolve(res)},// 接口调用失败的回调函数fail: (err) => {reject(err)}})})}
}
然后对 WxRequest
进行实例化,然后测试 request
实例方法是否封装成功!
注意:我们先将类 和 实例化的对象放到同一个文件中,这样方便进行调试,后面我们在拆分成两个文件
class WxRequest {// coding....
}// ----------------- 实例化 ----------------------// 对 WxRequest 进行实例化
const instance = new WxRequest()// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance
在其他模块中引入封装的文件后,我们期待通过 request()
方式发起请求,以 promise 的方式返回参数
// 导入创建的实例
import instance from '../../utils/wx-request'Page({// 点击按钮触发 handler 方法async handler() {// 通过实例调用 request 方法发送请求const res = await instance.request({url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',method: 'GET'})console.log(res)}
})
落地代码:
➡️ /utils/request.js
// 创建 WxRequest 类,采用类的方式进行封装会让方法更具有复用性,也可以方便进行添加新的属性和方法class WxRequest {// 定义 constructor 构造函数,用于创建和初始化类的属性和方法constructor() {}/*** @description 发起请求的方法* @param { Object} options 请求配置选项,同 wx.request 请求配置选项* @returns Promise*/request(options) {// 使用 Promise 封装异步请求return new Promise((resolve, reject) => {// 使用 wx.request 发起请求wx.request({...options,// 接口调用成功的回调函数success: (res) => {resolve(res)},// 接口调用失败的回调函数fail: (err) => {reject(err)}})})}
}// ----------------- 实例化 ----------------------// 对 WxRequest 进行实例化
const instance = new WxRequest()// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance
➡️ /pages/test/test.js
import instance from '../../utils/request'Page({// 点击按钮触发 handler 方法async handler() {// 第一种调用方式:通过 then 和 catch 接收返回的值// instance// .request({// url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',// method: 'GET'// })// .then((res) => {// console.log(res)// })// .catch((err) => {// console.log(err)// })// 第二种调用方式:通过 await 和 async 接收返回的值const res = await instance.request({url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',method: 'GET'})console.log(res)}})
03. 请求封装-设置请求参数
思路分析:
在发起网络请求时,需要配置一些请求参数,
其中有一些参数我们可以设置为默认参数,例如:请求方法、超时时长 等等,因此我们在封装时我们要定义一些默认的参数。
// 默认参数对象
defaults = {baseURL: '', // 请求基准地址url: '', // 开发者服务器接口地址data: null, // 请求参数method: 'GET',// 默认请求方法// 请求头header: {'Content-type': 'application/json' // 设置数据的交互格式},timeout: 60000 // 小程序默认超时时间是 60000,一分钟// 其他参数...
}
但是不同的项目,请求参数的设置是不同的,我们还需要允许在进行实例化的时候,传入参数,对默认的参数进行修改。例如:
// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api', // 请求基准地址timeout: 10000 // 微信小程序 timeout 默认值为 60000
})
在通过实例,调用 request
实例方法时也会传入相关的请求参数
const res = await instance.request({url: '/index/findBanner',method: 'GET'
})
从而得出结论:请求参数的设置有三种方式:
- 默认参数:在
WxRequest
类中添加defaults
实例属性来设置默认值 - 实例化时参数:在对
WxRequest
类进行实例化时传入相关的参数,需要在constructor
构造函数形参进行接收 - 调用实例方法时传入请求参数
默认参数和自定义参数的合并操作,通常会在constructor
中进行。
因此我们就在 constructor
中将开发者传入的相关参数和defaults
默认值进行合并,需要传入的配置项覆盖默认配置项
class WxRequest {+ // 默认参数对象
+ defaults = {
+ baseURL: '', // 请求基准地址
+ url: '', // 开发者服务器接口地址
+ data: null, // 请求参数
+ method: 'GET',// 默认请求方法
+ // 请求头
+ header: {
+ 'Content-type': 'application/json' // 设置数据的交互格式
+ },
+ timeout: 60000 // 小程序默认超时时间是 60000,一分钟
+ }/*** @description 定义 constructor 构造函数,用于创建和初始化类的属性和方法* @param {*} params 用户传入的请求配置项*/
+ constructor(params = {}) {
+ // 在实例化时传入的参数能够被 constructor 进行接收
+ console.log(params)+ // 使用 Object.assign 合并默认参数以及传递的请求参数
+ this.defaults = Object.assign({}, this.defaults, params)
+ }// coding....
}// ----------------- 实例化 ----------------------// 对 WxRequest 进行实例化
+ const instance = new WxRequest({
+ baseURL: 'https://gmall-prod.atguigu.cn/mall-api',
+ timeout: 15000
+ })// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance
在调用 request
实例时也会传入相关的参数,是发起请求真正的参数,
我们需要将调用 reqeust
实例方法时传入的参数,继续覆盖合并以后的参数,请求才能够发送成功
注意:让使用传入的参数覆盖默认的参数,同时拼接完整的请求地址。
// 创建 request 请求方法
request(options) {
+ // 拼接完整的请求地址
+ options.url = this.defaults.baseURL + options.url
+ // 合并请求参数
+ options = { ...this.defaults, ...options }return new Promise((resolve, reject) => {// coding...})}
落地代码:
➡️ utils/request.js
// 创建 Request 类,用于封装 wx.request() 方法
class WxRequest {+ // 默认参数对象
+ defaults = {
+ baseURL: '', // 请求基准地址
+ url: '', // 开发者服务器接口地址
+ data: null, // 请求参数
+ method: 'GET',// 默认请求方法
+ // 请求头
+ header: {
+ 'Content-type': 'application/json' // 设置数据的交互格式
+ },
+ timeout: 60000 // 小程序默认超时时间是 60000,一分钟
+ }+ /**
+ * @description 定义 constructor 构造函数,用于创建和初始化类的属性和方法
+ * @param {*} params 用户传入的请求配置项
+ */
+ constructor(params = {}) {
+ // 在实例化时传入的参数能够被 constructor 进行接收
+ console.log(params)+ // 使用 Object.assign 合并默认参数以及传递的请求参数
+ this.defaults = Object.assign({}, this.defaults, params)
+ }/*** @description 发起请求的方法* @param { Object} options 请求配置选项,同 wx.request 请求配置选项* @returns Promise*/request(options) {
+ // 拼接完整的请求地址
+ options.url = this.defaults.baseURL + options.url
+ // 合并请求参数
+ options = { ...this.defaults, ...options }// 方法返回一个 Promise 对象return new Promise((resolve, reject) => {// coding...})}
}// ----------------- 实例化 ----------------------// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance
04. 请求封装-封装请求快捷方法
思路分析:
目前已经完成了 request()
请求方法的封装,同时处理了请求参数。
每次发送请求时都使用 request()
方法即可,但是项目中的接口地址有很多,不是很简洁
const res = await instance.request({url: '/index/findBanner',method: 'GET'
})
所以我们在 request()
基础上封装一些快捷方法,简化 request()
的调用。
需要封装 4 个快捷方法,分别是 get
、delete
、post
、put
,他们的调用方式如下:
instance.get('请求地址', '请求参数', '请求配置')
instance.delete('请求地址', '请求参数', '请求配置')
instance.post('请求地址', '请求参数', '请求配置')
instance.put('请求地址', '请求参数', '请求配置')
这 4 个请求方法,都是通过实例化的方式进行调用,所以需要 Request
类中暴露出来 get
、delete
、post
、put
方法。每个方法接收三个参数,分别是:接口地址、请求参数以及其他参数。
这 4 个快捷方法,本质上其实还是调用 request
方法,我们只要在方法内部组织好参数,调用 request
发送请求即可
class WxRequest {// coding...+ // 封装 GET 实例方法
+ get(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'GET' }, config))
+ }+ // 封装 POST 实例方法
+ post(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'POST' }, config))
+ }+ // 封装 PUT 实例方法
+ put(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'PUT' }, config))
+ }+ // 封装 DELETE 实例方法
+ delete(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'DELETE' }, config))
+ }
}// ----------------- 实例化 ----------------------// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance
落地代码:
➡️ utils/request.js
class WxRequest {// coding...+ // 封装 GET 实例方法
+ get(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'GET' }, config))
+ }+ // 封装 POST 实例方法
+ post(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'POST' }, config))
+ }+ // 封装 PUT 实例方法
+ put(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'PUT' }, config))
+ }+ // 封装 DELETE 实例方法
+ delete(url, data = {}, config = {}) {
+ return this.request(Object.assign({ url, data, method: 'DELETE' }, config))
+ }
}// ----------------- 实例化 ----------------------// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance
➡️ /pages/test/test.js
// 导入创建的实例
import instance from '../../utils/wx-request'Page({async handler() {// 第一种调用方式:通过 then 和 catch 接收返回的值// instance// .request({// url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',// method: 'GET'// })// .then((res) => {// console.log(res)// })// .catch((err) => {// console.log(err)// })// 第二种调用方式// 通过实例调用 request 方法发送请求// const res = await instance.request({// url: '/index/findBanner',// method: 'GET'// })// console.log(res)// 第三种调用方式:通过调用快捷方式接收返回的值const res = await instance.get('/index/findBanner')console.log(res)}
})
05. 请求封装-wx.request 注意事项
知识点:
在使用 wx.request 发送网络请求时。
只要成功接收到服务器返回,无论statusCode
是多少,都会进入 success
回调
开发者根据业务逻辑对返回值进行判断。
什么时候会有 fail
回调函数 ?
一般只有网络出现异常、请求超时等时候,才会走 fail
回调
落地代码:
测试代码
request() {wx.request({url: 'https://gmall-prod.atguigu.cn/mall-api/index/findCategory',method: 'GET',// timeout: 100, 测试网络超时,需要调整网络success: (res) => {console.log('只要成功接收到服务器返回,不管状态是多少,都会进入 success 回调')console.log(res)},fail: (err) => {console.log(err)}})
}
06. 请求封装-定义请求/响应拦截器
思路分析:
为了方便统一处理请求参数以及服务器响应结果,为 WxRequest
添加拦截器功能,拦截器包括 请求拦截器 和 响应拦截器
请求拦截器本质上是在请求之前调用的函数,用来对请求参数进行新增和修改
响应拦截器本质上是在响应之后调用的函数,用来对响应数据做点什么
注意:不管成功响应还是失败响应,都会执行响应拦截器
拦截器的使用方式:
// 请求拦截器
instance.interceptors.request = (config) => {// 在发送请求之前做些什么return config
}// 响应拦截器
instance.interceptors.response = (response) => {// 对响应数据做点什么return response
}
通过使用方式,我们可以得出结论:
可以在 WxRequest
类内部定义 interceptors
实例属性,属性中需要包含 request
以及 response
方法
需要注意:在发送请求时,还需要区分是否通过实例调用了拦截器:
- 没有通过实例调用拦截器,需要定义默认拦截器,在默认拦截器中,需要将请求参数进行返回
- 通过实例调用拦截器,那么实例调用的拦截器会覆盖默认的拦截器方法,然后将新增或修改的请求参数进行返回
实现拦截器的思路:
- 在
WxRequest
类内部定义interceptors
实例属性,属性中需要包含request
以及response
方法 - 是否通过实例调用了拦截器
- 是:定义默认拦截器
- 否:实例调用的拦截器覆盖默认拦截器
- 在发送请求之前,调用请求拦截器
- 在服务器响应以后,调用响应拦截器
- 不管成功、失败响应,都需要调用响应拦截器
在 WxRequest
类内部定义 interceptors
实例属性,属性中需要包含 request
以及 response
方法。
没有使用拦截器,定义默认拦截器,需要将默认的请求参数进行返回。
如果使用了拦截器,那么使用者的拦截器会覆盖默认的拦截器方法
class WxRequest {// coding...+ // 定义拦截器对象,包含请求拦截器和响应拦截器方法,方便在请求或响应之前进行处理。
+ interceptors = {
+ // 请求拦截器
+ request: (config) => config,
+ // 响应拦截器
+ response: (response) => response
+ }// 用于创建和初始化类的属性以及方法// 在实例化时传入的参数,会被 constructor 形参进行接收constructor(options = {}) {// coding...}
}
// ----------------- 以下是实例化的代码 --------------------
// 目前写到同一个文件中,是为了方便进行测试,以后会提取成多个文件// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})+ // 配置请求拦截器
+ instance.interceptors.request = (config) => {
+ // 在发送请求之前做些什么
+ return config
+ }+ // 响应拦截器
+ instance.interceptors.response = (response) => {
+ // 对响应数据做点什么
+ return response
+ }// 将 WxRequest 实例进行暴露出去,方便在其他文件中进行使用
export default instance
在发送请求之前,调用请求拦截器,在服务器响应以后,调用响应拦截器
不管成功、失败,都需要调用响应拦截器
class WxRequest {// coding...// request 实例方法接收一个对象类型的参数// 属性值和 wx.request 方法调用时传递的参数保持一致request(options) {// 注意:需要先合并完整的请求地址 (baseURL + url)// https://gmall-prod.atguigu.cn/mall-api/index/findBanneroptions.url = this.defaults.baseURL + options.url// 合并请求参数options = { ...this.defaults, ...options }+ // 在发送请求之前调用请求拦截器
+ options = this.interceptors.request(options)// 需要使用 Promise 封装 wx.request,处理异步请求return new Promise((resolve, reject) => {wx.request({...options,// 当接口调用成功时会触发 success 回调函数success: (res) => {
+ // 不管接口成功还是失败,都需要调用响应拦截器
+ // 第一个参数:需要合并的目标对象
+ // 第二个参数:服务器响应的数据
+ // 第三个参数:请求配置以及自定义的属性
+ const mergetRes = Object.assign({}, res, { config: options })
+ resolve(this.interceptors.response(mergetRes))},// 当接口调用失败时会触发 fail 回调函数fail: (err) => {
+ // 不管接口成功还是失败,都需要调用响应拦截器
+ const mergetErr = Object.assign({}, err, { config: options })
+ reject(this.interceptors.response(mergetErr))}})})}// coding...
}
落地代码:
➡️ utils/request.js
// 创建 WxRequest 类
// 通过类的方式来进行封装,会让代码更加具有复用性
// 也可以方便添加新的属性和方法class WxRequest {// 定义实例属性,用来设置默认请求参数defaults = {baseURL: '', // 请求基准地址url: '', // 接口的请求路径data: null, // 请求参数method: 'GET', // 默认的请求方法// 请求头header: {'Content-type': 'application/json' // 设置数据的交互格式},timeout: 60000 // 默认的超时时长,小程序默认的超时时长是 1 分钟}+ // 定义拦截器对象,包含请求拦截器和响应拦截器方法,方便在请求或响应之前进行处理。
+ interceptors = {
+ // 请求拦截器
+ request: (config) => config,
+ // 响应拦截器
+ response: (response) => response
+ }// 用于创建和初始化类的属性以及方法// 在实例化时传入的参数,会被 constructor 形参进行接收constructor(params = {}) {// 通过 Object.assign 方法合并请求参数// 注意:需要传入的参数,覆盖默认的参数,因此传入的参数需要放到最后this.defaults = Object.assign({}, this.defaults, params)}// request 实例方法接收一个对象类型的参数// 属性值和 wx.request 方法调用时传递的参数保持一致request(options) {// 注意:需要先合并完整的请求地址 (baseURL + url)// https://gmall-prod.atguigu.cn/mall-api/index/findBanneroptions.url = this.defaults.baseURL + options.url// 合并请求参数options = { ...this.defaults, ...options }+ // 在发送请求之前调用请求拦截器
+ options = this.interceptors.request(options)// 需要使用 Promise 封装 wx.request,处理异步请求return new Promise((resolve, reject) => {wx.request({...options,// 当接口调用成功时会触发 success 回调函数success: (res) => {
+ // 不管接口成功还是失败,都需要调用响应拦截器
+ // 第一个参数:需要合并的目标对象
+ // 第二个参数:服务器响应的数据
+ // 第三个参数:请求配置以及自定义的属性
+ const mergeRes = Object.assign({}, res, { config: options })
+ resolve(this.interceptors.response(mergeRes))},// 当接口调用失败时会触发 fail 回调函数fail: (err) => {
+ // 不管接口成功还是失败,都需要调用响应拦截器
+ const mergeErr = Object.assign({}, err, { iconfig: options })
+ // 不管接口成功还是失败,都需要调用响应拦截器
+ err = this.interceptors.response(mergeErr)
+ reject(err)}})})}// 封装 GET 实例方法get(url, data = {}, config = {}) {// 需要调用 request 请求方法发送请求,只需要组织好参数,传递给 request 请求方法即可// 当调用 get 方法时,需要将 request 方法的返回值 return 出去return this.request(Object.assign({ url, data, method: 'GET' }, config))}// 封装 DELETE 实例方法delete(url, data = {}, config = {}) {return this.request(Object.assign({ url, data, method: 'DELETE' }, config))}// 封装 POST 实例方法post(url, data = {}, config = {}) {return this.request(Object.assign({ url, data, method: 'POST' }, config))}// 封装 PUT 实例方法put(url, data = {}, config = {}) {return this.request(Object.assign({ url, data, method: 'PUT' }, config))}
}// ----------------- 以下是实例化的代码 --------------------
// 目前写到同一个文件中,是为了方便进行测试,以后会提取成多个文件// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 15000
})+ // 配置请求拦截器
+ instance.interceptors.request = (config) => {
+ // 在发送请求之前做些什么
+ return config
+ }+ // 响应拦截器
+ instance.interceptors.response = (response) => {
+
+ // 对响应数据做点什么
+ return response.data
+ }// 将 WxRequest 实例进行暴露出去,方便在其他文件中进行使用
export default instance
07. 请求封装-完善请求/响应拦截器
思路分析:
在响应拦截器,我们需要判断是请求成功,还是请求失败,然后进行不同的业务逻辑处理。
例如:请求成功以后将数据简化返回,网络出现异常则给用户进行网络异常提示。
目前不管请求成功 (success),还是请求失败(fail),都会执行响应拦截器
那么怎么判断是请求成功,还是请求失败呢 ?
封装需求:
- 如果请求成功,将响应成功的数据传递给响应拦截器,同时在传递的数据中新增
isSuccess: true
字段,表示请求成功 - 如果请求失败,将响应失败的数据传递给响应拦截器,同时在传递的数据中新增
isSuccess: false
字段,表示请求失败
在实例调用的响应拦截中,根据传递的数据进行以下的处理:
- 如果
isSuccess: true
表示服务器响应了结果,我们可以将服务器响应的数据简化以后进行返回 - 如果
isSuccess: false
表示是网络超时或其他网络问题,提示网络异常
,同时将返回即可
落地代码:
➡️ utils/request.js
class WxRequest {// coding....request(options) {// coding....// 使用 Promise 封装异步请求return new Promise((resolve, reject) => {// 使用 wx.request 发起请求wx.request({...options,// 接口调用成功的回调函数success: (res) => {// 响应成功以后触发响应拦截器if (this.interceptors.response) {
+ // 调用响应拦截器方法,获取到响应拦截器内部返回数据
+ // success: true 表示服务器成功响应了结果,我们需要对业务状态码进行判断
+ res = this.interceptors.response({ response: res, isSuccess: true })}// 将数据通过 resolve 进行返回即可resolve(res)},// 接口调用失败的回调函数fail: (err) => {// 响应失败以后也要执行响应拦截器if (this.interceptors.response) {
+ // isSuccess: false 表示是网络超时或其他问题
+ err = this.interceptors.response({ response: err, isSuccess: true })}// 当请求失败以后,通过 reject 返回错误原因reject(err)}})})}// coding......
}// -----------------------------------------------------// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api'
})// 设置请求拦截器
instance.setRequestInterceptor((config) => {console.log('执行请求拦截器')return config
})// 设置响应拦截器
+ instance.setResponseInterceptor((response) => {
+ const { response: res, isSuccess } = response+ // isSuccess: false 表示是网络超时或其他问题,提示 网络异常,同时将返回即可
+ if (!isSuccess) {
+ wx.toast('网络异常,请稍后重试~')
+ // 如果请求错误,将错误的结果返回出去
+ return res
+ }+ // 简化数据
+ return response.data
})// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance
08. 请求封装-使用请求/响应拦截器
思路分析:
使用请求拦截器:
在发送请求时,购物车列表、收货地址、更新头像等接口,都需要进行权限验证,因此我们需要在请求拦截器中判断本地是否存在访问令牌 token
,如果存在就需要在请求头中添加 token
字段。
使用响应拦截器:
在使用 wx.request 发送网络请求时。只要成功接收到服务器返回,无论statusCode
是多少,都会进入 success
回调。
因此开发者根据业务逻辑对返回值进行判断。
后端返回的业务状态码如下:
- 业务状态码 === 200, 说明接口请求成功,服务器成功返回了数据
- 业务状态码 === 208, 说明没有 token 或者 token 过期失效,需要登录或者重新登录
- 业务状态码 === 其他,说明请求或者响应出现了异常
其他测试接口:/cart/getCartList
落地代码:
➡️ utils/request.js
// 创建 WxRequest 类,采用类的方式进行封装会让方法更具有复用性,也可以方便进行添加新的属性和方法class WxRequest {// coding...
}// -----------------------------------------------------// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api',timeout: 5000
})// 设置请求拦截器
instance.setRequestInterceptor((config) => {
+ // 从本地获取 token
+ if (wx.getStorageSync('token')) {
+ // 如果存在 token ,则添加请求头
+ config.header['token'] = wx.getStorageSync('token')
+ }
+
+ // 返回请求参数
+ return config
})// 设置响应拦截器
instance.setResponseInterceptor(async (response) => {
+ const { response: res, isSuccess } = response+ // isSuccess: false 表示是网络超时或其他问题,提示 网络异常,同时将返回即可
+ if (!isSuccess) {
+ wx.toast('网络异常,请稍后重试~')
+ // 如果请求错误,将错误的结果返回出去
+ return res
+ }+ switch (res.data.code) {
+ case 200:
+ return res.data+ case 208:
+ // 判断用户是否点击了确定
+ const modalStatus = await wx.modal({
+ title: '提示',
+ content: '登录授权过期,请重新授权'
+ })+ // 如果点击了确定,先清空本地的 token,然后跳转到登录页面
+ if (modalStatus) {
+ wx.clearStorageSync()
+ wx.navigateTo({
+ url: '/pages/login/login'
+ })
+ }
+ return+ default:
+ wx.showToast({
+ title: '接口调用失败~~~~',
+ icon: 'none'
+ })+ // 将错误继续向下传递
+ return Promise.reject(response)
+ }
})// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance
09. 请求封装-添加并发请求
思路分析:
前端并发请求是指在前端页面同时向后端发起多个请求的情况。当一个页面需要请求多个接口获取数据时,为了提高页面的加载速度和用户体验,可以同时发起多个请求,这些请求之间就是并发的关系。
我们通过两种方式演示发起多个请求:
- 使用
async
和await
方式 - 使用
Promise.all()
方式
首先使用async
和 await
方式发送请求,使用 async
和 await
能够控制异步任务以同步的流程执行,代码如下,这时候就会产生一个问题,当第一个请求执行完以后,才能执行第二个请求,这样就会造成请求的阻塞,影响渲染的速度,如下图
这时候我们需要使用 Promise.all()
方式同时发起多个异步请求,并在所有请求完成后再进行数据处理和渲染。使用Promise.all()
能够将多个请求同时发出,不会造成请求的阻塞。
通过两种方式演示,我们能够知道封装并发请求的必要性。在 WxRequest 实例中封装 all
方法,使用展开运算符将传入的参数转成数组,方法的内部,使用 Promise.all()
接收传递的多个异步请求,将处理的结果返回即可。
class WxRequest {// coding...+ // 封装处理并发请求的 all 方法
+ all(...promise) {
+ return Promise.all(promise)
+ }// coding...
}// coding...
落地代码:
➡️ utils/request.js
class WxRequest {// coding...+ // 封装处理并发请求的 all 方法
+ all(...promise) {
+ return Promise.all(promise)
+ }// coding...
}// coding...
➡️ /pages/test/test.js
import instance from '../../utils/http'Page({async getData() {// 使用 Promise.all 同时处理多个异步请求const [res1, res2] = await instance.all([instance.get('/mall-api/index/findBanner'),instance.get('/mall-api/index/findCategory1')])console.log(res1)console.log(res2)}
})
10. 请求封装-添加 loading
思路分析:
在封装时添加 loading
效果,从而提高用户使用体验
-
在请求发送之前,需要通过
wx.showLoading
展示loading
效果 -
当服务器响应数据以后,需要调用
wx.hideLoading
隐藏loading
效果
要不要加 loading 添加到 WxRequest 内部 ?
在类内部进行添加,方便多个项目直接使用类提供的 loading 效果,也方便统一优化 wx.showLoading 使用体验。
但是不方便自己来进行 loading 个性化定制。
如果想自己来控制 loading 效果,带来更丰富的交互体验,就不需要将 loading 封装到类内部,但是需要开发者自己来优化 wx.showLoading 使用体验,每个项目都要写一份。
大伙可以按照自己的业务需求进行封装,
在项目中我们会选择第一种方式。折中
不过也会通过属性控制是否展示 loading,从而方便类使用者自己控制 loading 显示
落地代码:
➡️ utils/request.js
class WxRequest {// coding...constructor(options = {}) {// coding...}// 创建 request 请求方法request(options) {// 拼接完整的请求地址options.url = this.defaults.baseURL + options.url// 合并请求参数options = { ...this.defaults, ...options }+ // 发送请求之前添加 loding
+ wx.showLoading()// 如果存在请求拦截器,我们则调用请求拦截器if (this.interceptors.request) {// 请求之前,触发请求拦截器options = this.interceptors.request(options)}// 方法返回一个 Promise 对象return new Promise((resolve, reject) => {wx.request({...options,success: (res) => {// coding...},fail: (err) => {// coding...},
+ complete: () => {
+ // 接口调用完成后隐藏 loding
+ wx.hideLoading()
+ }})})}// coding...
}
11. 请求封装-完善 loading
思路分析:
目前在发送请求时,请求发送之前会展示 loading
,响应以后会隐藏 loading
。
但是 loading 的展示和隐藏会存在以下问题:
- 每次请求都会执行
wx.showLoading()
,但是页面中只会显示一个,后面的loading
会将前面的覆盖 - 同时发起多次请求,只要有一个请求成功响应就会调用
wx.hideLoading
,导致其他请求还没完成,也不会loading
- 请求过快 或 一个请求在另一个请求后立即触发,这时候会出现
loading
闪烁问题
我们通过 队列 的方式解决这三个问题:首先在类中新增一个实例属性 queue
,初始值是一个空数组
- 发起请求之前,判断
queue
如果是空数组则显示loading
,然后立即向queue
新增请求标识 - 在
complete
中每次请求成功结束,从queue
中移除一个请求标识,queue
为空时隐藏loading
- 为了解决网络请求过快产生
loading
闪烁问题,可以使用定时器来做判断即可
落地代码:
➡️ utils/request.js
class WxRequest {// coding...constructor(options = {}) {// 使用 Object.assign 合并默认参数以及传递的请求参数this.defaults = Object.assign({}, this.defaults, options)// 定义拦截器对象,包含请求拦截器和响应拦截器方法,方便在请求或响应之前进行处理。this.interceptors = {// 请求拦截器request: null,// 响应拦截器response: null}+ // 初始化 queue 数组,用于存储请求队列
+ this.queue = []}// 创建 request 请求方法request(options) {
+ // 如果有新的请求,则清空上一次的定时器
+ this.timerId && clearTimeout(this.timerId)// 拼接完整的请求地址options.url = this.defaults.baseURL + options.url// 合并请求参数options = { ...this.defaults, ...options }// 如果存在请求拦截器,我们则调用请求拦截器if (this.interceptors.request) {// 请求之前,触发请求拦截器options = this.interceptors.request(options)}+ // 发送请求之前添加 loding
+ this.queue.length === 0 && wx.showLoading()
+ // 然后想队列中添加 request 标识,代表需要发送一次新请求
+ this.queue.push('request')// 方法返回一个 Promise 对象return new Promise((resolve, reject) => {wx.request({...options,success: (res) => {// coding...},fail: (err) => {// coding...},complete: () => {// 接口调用完成后隐藏 loding// wx.hideLoading()+ // 每次请求结束后,从队列中删除一个请求标识
+ this.queue.pop()
+
+ // 如果队列已经清空,在往队列中添加一个标识
+ this.queue.length === 0 && this.queue.push('request')+ // 等所有的任务执行完以后,经过 100 毫秒
+ // 将最后一个 request 清除,然后隐藏 loading
+ this.timerId = setTimeout(() => {
+ this.queue.pop()
+ this.queue.length === 0 && wx.hideLoading()
+ }, 100)}})})}// 封装快捷请求方法// coding...// 封装拦截器// coding...
}// coding...export default instance
12. 请求封装-控制 loading 显示
思路分析:
在我们封装的网络请求文件中,通过 wx.showLoading
默认显示了 loading
效果
但是在实际开发中,有的接口可能不需要显示 loading
效果,或者开发者希望自己来控制 loading
的样式与交互,那么就需要关闭默认 loading
效果。
这时候我们就需要一个开关来控制 loading
显示。
- 类内部设置默认请求参数
isLoading
属性,默认值是true
,在类内部根据isLoading
属性做判断即可 - 某个接口不需要显示
loading
效果,可以在发送请求的时候,可以新增请求配置isLoading
设置为false
- 整个项目都不需要显示
loading
效果,可以在实例化的时候,传入isLoading
配置为false
实现步骤:
-
在 WxRequest 类的默认请求配置项中,设置 isLoading 默认值为 true,显示 loading
class WxRequest {// 初始化默认的请求属性defaults = {url: '', // 开发者服务器接口地址data: null, // 请求参数header: {}, // 设置请求的 headertimeout: 60000, // 超时时间method: 'GET', // 请求方式 + isLoading: true // 是否显示 loading 提示框}// code... }
-
在进行实例化的时候,可以配置 isLoading 配置为 false,隐藏 loading
// 对 WxRequest 进行实例化 const instance = new WxRequest({baseURL: 'https://gmall-prod.atguigu.cn/mall-api', + isLoading: false // 隐藏 loading })
-
在发送网络请求时候,传入请求配置 isLoading 配置为 false,隐藏 loading
async func() { + // 请求配置 isLoading 配置为 false,隐藏 loading + await instance.get('/index/findCategory1', null, { isLoading: true }) }
-
wx-request 内部代码实现
// 创建 WxRequest 类,采用类的方式进行封装会让方法更具有复用性,也可以方便进行添加新的属性和方法class WxRequest {// 初始化默认的请求属性defaults = {url: '', // 开发者服务器接口地址data: null, // 请求参数header: {}, // 设置请求的 headertimeout: 60000, // 超时时间method: 'GET', // 请求方式 + isLoading: true // 是否显示 loading 提示框}constructor(params = {}) {// coding...}request(options) {// coding...+ // 发送请求之前添加 loding + if (options.isLoading) { + this.queue.length === 0 && wx.showLoading() + // 然后想队列中添加 request 标识,代表需要发送一次新请求 + this.queue.push('request') + }// 请求之前,触发请求拦截器// 如果存在请求拦截器,则触发请求拦截器if (this.interceptors.request) {options = this.interceptors.request(options)}// 使用 Promise 封装异步请求return new Promise((resolve, reject) => {// 使用 wx.request 发起请求wx.request({...options,// 接口调用成功的回调函数success: (res) => {// coding...},// 接口调用失败的回调函数fail: (err) => {// coding...},complete: () => {// 接口调用完成后隐藏 loding// wx.hideLoading()+ if (!options.isLoading) return// 每次请求结束后,从队列中删除一个请求标识this.queue.pop()// 如果队列已经清空,在往队列中添加一个标识this.queue.length === 0 && this.queue.push('request')// 等所有的任务执行完以后,经过 100 毫秒// 将最后一个 request 清除,然后隐藏 loadingthis.timerId = setTimeout(() => {this.queue.pop()this.queue.length === 0 && wx.hideLoading()}, 100)}})})}// coding... }
13. 请求封装-封装 uploadFile
思路分析:
wx.uploadFile
也是我们在开发中常用的一个 API
,用来将本地资源上传到服务器。
例如:在获取到微信头像以后,将微信头像上传到公司服务器。
wx.uploadFile({url: '', // 必填项,开发者服务器地址filePath: '', // 必填项,要上传文件资源的路径 (本地路径)name: '' // 必填项,文件对应的 key,开发者在服务端可以通过这个 key 获取文件的二进制内容
})
在了解了 API 以后,我们直接对 wx.uploadFile
进行封装即可。
首先在 WxRequest
类内部创建 upload
实例方法,实例方法接收四个属性:
/**
* @description 文件上传接口封装
* @param { string } url 文件上传地址
* @param { string } filePath 要上传文件资源的路径
* @param { string } name 文件对应的 key
* @param { string } config 其他配置项
* @returns
*/
upload(url, filePath, name, config = {}) {return this.request(Object.assign({ url, filePath, name, method: 'UPLOAD' }, config))
}
这时候我们需要在 request
实例方法中,对 method
进行判断,如果是 UPLOAD
,则调用 wx.uploadFile
上传API
// request 实例方法接收一个对象类型的参数
// 属性值和 wx.request 方法调用时传递的参数保持一致
request(options) {// coding...// 需要使用 Promise 封装 wx.request,处理异步请求return new Promise((resolve, reject) => {
+ if (options.method === 'UPLOAD') {
+ wx.uploadFile({
+ ...options,
+
+ success: (res) => {
+ // 将服务器响应的数据通过 JSON.parse 转换为 JS 对象
+ res.data = JSON.parse(res.data)
+
+ const mergeRes = Object.assign({}, res, {
+ config: options,
+ isSuccess: true
+ })
+
+ resolve(this.interceptors.response(mergeRes))
+ },
+
+ fail: (err) => {
+ const mergeErr = Object.assign({}, err, {
+ config: options,
+ isSuccess: true
+ })
+
+ reject(this.interceptors.response(mergeErr))
+ },
+
+ complete: () => {
+ this.queue.pop()
+
+ this.queue.length === 0 && wx.hideLoading()
+ }
+ })} else {wx.request({// coding...})}})
}
落地代码:
➡️ utils/request.js
// request 实例方法接收一个对象类型的参数
// 属性值和 wx.request 方法调用时传递的参数保持一致
request(options) {// coding...// 需要使用 Promise 封装 wx.request,处理异步请求return new Promise((resolve, reject) => {
+ if (options.method === 'UPLOAD') {
+ wx.uploadFile({
+ ...options,
+
+ success: (res) => {
+ // 将服务器响应的数据通过 JSON.parse 转换为 JS 对象
+ res.data = JSON.parse(res.data)
+
+ const mergeRes = Object.assign({}, res, {
+ config: options,
+ isSuccess: true
+ })
+
+ resolve(this.interceptors.response(mergeRes))
+ },
+
+ fail: (err) => {
+ const mergeErr = Object.assign({}, err, {
+ config: options,
+ isSuccess: true
+ })
+
+ reject(this.interceptors.response(mergeErr))
+ },
+
+ complete: () => {
+ this.queue.pop()
+
+ this.queue.length === 0 && wx.hideLoading()
+ }
+ })} else {wx.request({// coding...})}})
}
test/test.js
Page({/*** 页面的初始数据*/data: {avatarUrl: '../../assets/Jerry.png'},// 获取微信头像async chooseavatar(event) {// 目前获取的微信头像是临时路径// 临时路径是有失效时间的,在实际开发中,需要将临时路径上传到公司的服务器const { avatarUrl } = event.detail// 调用 upload 方法发送请求,将临时路径上传到公司的服务器const res = await instance.upload('/fileUpload',event.detail.avatarUrl,'file')// 将返回的数据赋值给 data 中的数据this.setData({avatarUrl: res.data})},// coding...
}
14. 请求封装-使用 npm 包发送请求
思路分析:
封装的网络请求模块发布到了 npm
,如果你在学习网络请求模块封装时感觉比较吃力,可以先使用 npm 包实现功能。
npm install mina-request
📌 构建 npm:
安装包后,需要在微信开发者工具中进行 npm 构建,点击
工具
➡️构建 npm
其余步骤参考文档进行开发即可:
mina-request 地址
落地代码:
import WxRequest from "./request";
import { env } from "./env ";
// 是否显示重新登录
let isRelogin = { show: false };
// ----------------- 实例化 ----------------------
// 对 WxRequest 进行实例化
const instance = new WxRequest({baseURL: env.baseURL,timeout: 15000,
});// 配置请求拦截器
instance.interceptors.request = (config) => {// 在发送请求之前做些什么console.log(config, "在发送请求之前做些什么");// 从本地获取 tokenif (wx.getStorageSync("token")) {// 如果存在 token ,则添加请求头config.header["token"] = wx.getStorageSync("token");}// 返回请求参数return config;
};// 响应拦截器
instance.interceptors.response = (response) => {console.log(response, "响应拦截器");const { isSuccess, data } = response;// isSuccess: false 表示是网络超时或其他问题,提示 网络异常,同时将返回即可if (!isSuccess) {wx.showToast({title: '"网络异常,请稍后重试~"',icon: "error",});// 如果请求错误,将错误的结果返回出去return response;}switch (data.code) {case 200:return data;case 208:// 控制多个接口触发,弹框只出现一次if (!isRelogin.show) {isRelogin.show = true;wx.showModal({showCancel: false,title: "提示",content: "登录授权过期,请重新授权",complete: (res) => {console.log(res);// 清空tokenwx.removeStorageSync("token");// 返回首页wx.reLaunch({url: "/pages/login/login",});// 点击确认后恢复状态isRelogin.show = false;},});}// 将错误继续向下传递return Promise.reject(response);default:wx.showToast({title: "接口调用失败~~~~",icon: "none",});// 将错误继续向下传递return Promise.reject(response);}
};// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance;
15. 环境变量-小程序设置环境变量
知识点:
在实际开发中,不同的开发环境,调用的接口地址是不一样的。
例如:开发环境需要调用开发版的接口地址,生产环境需要调用正式版的接口地址
这时候,我们就可以使用小程序提供了 wx.getAccountInfoSync()
接口,用来获取当前账号信息,在账号信息中包含着 小程序 当前环境版本。
环境版本 | 合法值 |
---|---|
开发版 | develop |
体验版 | trial |
正式版 | release |
落地代码:
// 获取当前帐号信息
const accountInfo = wx.getAccountInfoSync()// 获取小程序项目的 appId
console.log(accountInfo.miniProgram.appId)
// 获取小程序 当前环境版本
console.log(accountInfo.miniProgram.envVersion)
根据环境的不同,我们给 env 变量设置不同的请求基准路径 baseURL
然后将 env
环境变量导出
// 获取 小程序帐号信息
const { miniProgram } = wx.getAccountInfoSync();// 获取小程序当前开发环境
// develop 开发版, trial 体验版, release 正式版
const { envVersion } = miniProgram;let env = {baseURL: "https://gmall-prod.atguigu.cn/mall-api",
};switch (envVersion) {case "develop":env.baseURL = "https://gmall-prod.atguigu.cn/mall-api";break;case "trial":env.baseURL = "https://gmall-prod.atguigu.cn/mall-api";break;case "release":env.baseURL = "https://gmall-prod.atguigu.cn/mall-api";break;default:console.log("当前环境异常");env.baseURL = "https://gmall-prod.atguigu.cn/mall-api";
}export { env };
16. 接口调用方式说明
思路分析:
在开发中,我们会将所有的网络请求方法放置在 api 目录下统一管理,然后按照模块功能来划分成对应的文件,在文件中将接口封装成一个个方法单独导出,例如:
// 导入封装的网络请求工具 http.js
import http from '../utils/http'/*** @description 获取轮播图数据* @returns Promise*/
export const reqBannerData = () => http.get('/index/findBanner')
这样做的有以下几点好处:
- 易于维护:一个文件就是一个模块,一个方法就是一个功能,清晰明了,查找方便
- 便于复用:哪里使用,哪里导入,可以在任何一个业务组件中导入需要的方法
- 团队合作:分工合作
落地代码:
// 导入封装的网络请求工具 http.js
import http from '../utils/http'/*** @description 获取轮播图数据* @returns Promise*/
export const reqSwiperData = () => http.get('/mall-api/index/findBanner')
// 导入接口 API
import { reqSwiperData } from '../../api/index'Page({// 页面数据data: {swiperList: []},// 小程序页面加载时执行onLoad () {// 调用获取首页数据的方法getHomeList()}// 获取首页数据async getHomeList() {// 获取轮播图数据const res = await reqSwiperData()console.log(res)}
})