网络请求封装
网络请求模块难度较大,如果学习起来感觉吃力,可以直接学习
[请求封装-使用 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/findBanner options.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/findBanner options.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: {}, // 设置请求的 header timeout: 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: {}, // 设置请求的 header timeout: 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 清除,然后隐藏 loading this.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, "在发送请求之前做些什么"); // 从本地获取 token if (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); // 清空token wx.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()// 获取小程序项目的 appIdconsole.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.jsimport http from '../utils/http'/** * @description 获取轮播图数据 * @returns Promise */export const reqBannerData = () => http.get('/index/findBanner')
这样做的有以下几点好处:
- 易于维护:一个文件就是一个模块,一个方法就是一个功能,清晰明了,查找方便
- 便于复用:哪里使用,哪里导入,可以在任何一个业务组件中导入需要的方法
- 团队合作:分工合作
落地代码:
// 导入封装的网络请求工具 http.jsimport http from '../utils/http'/** * @description 获取轮播图数据 * @returns Promise */export const reqSwiperData = () => http.get('/mall-api/index/findBanner')
// 导入接口 APIimport { reqSwiperData } from '../../api/index'Page({ // 页面数据 data: { swiperList: [] }, // 小程序页面加载时执行 onLoad () { // 调用获取首页数据的方法 getHomeList() } // 获取首页数据 async getHomeList() { // 获取轮播图数据 const res = await reqSwiperData() console.log(res) }})