# JS 面向对象,代码分层架构设计实践 与 网络请求封装

TIP

通过上一章节的学习,我们使用微信小程序自定义组件的能力,封装了一个 tabs 组件,经过这个过程我们解决了一个工程隐患,即代码重复实现的问题。

经历了发现问题,逐步解决问题的方式,学习到了微信小程序小程序项目开发中的实践技巧和解决方案。但我们项目中还没有实际的动态业务数据。

因此本节内容我们开始来实践如何获取业务数据 并 进行渲染的综合实践。从而衍生到复杂项目中应用 JavaScript 的面向对象 与 代码分层、架构设计等。同时会对 wx.request 进行二次封装实现统一响应、异常处理 以及 async/await 与 同步/异步编程等。再次升级微信小程序自定义组件的封装难度,进行新的自定义组件的封装实践。

搞清楚这些核心实践在实际的项目该如何应用,它的由来、既解决工程问题又能搞定面试相关

JavaScript 面向对象 与 代码分层架构设计

  • 常规的实现方式
  • 什么模型
  • 模型的意义
  • 软件工程最佳实践
  • 分层设计的好处

wx.request 二次封装,实现统一响应和异常处理

  • wx.request 封装
  • 全局统一响应、异常处理
  • 解决方案一:回调函数
  • 回调地狱解决方案演进脉络
  • 解决方案二:Promise 演进
  • 小程序 API 的 Promise 化
  • 终极解决方案:async/await

# 一、JavaScript 面向对象 与 代码分层架构设计

TIP

前面我们实现了内容标签页 tabs 自定义组件,接下来实现在内容标签的基础上加载不同的数据内容。

深入浅出面向对象在 JavaScript 中的实践,利用代码分层设计提高代码的可维护性 和 扩展性。

# 1、常规的实现方式

TIP

我们前面的学习中,是通过以下方式,发起网络请求来获取页面初始化的数据的

pages/index/index.js 页面逻辑中

// pages/index/index.js
Page({
  // 页面的初始数据
  data: {},

  // 生命周期函数--监听页面加载
  onLoad: function (options) {
    // 初始化课程列表
    this._getCourseList();

    // 执行其他函数 ......
  },

  // 获取课程列表(以 _ 开头,表示页面私有函数)
  _getCourseList() {
    // 发起网络请求,获取课程列表数据
    wx.request({
      // ......
    });
  },

  // 部分省略 ......
});

在企业项目开发中的最佳实践

  • 页面数据初始化时,onLoad 生命周期函数作为一个总的入口,进入后会分别执行各种函数,不要在其中写任何业务逻辑。这样做对于项目维护性、代码的可读性都会非常高
  • 对于函数的封装也有利于在其他函数中复用,因此不会在 onLoad 中直接发起网络请求
  • 会重新定义一个页面的私有函数,并以 _开头,然后再 onLoad 中调用该函数
  • _getCourseList() 函数中同样也不可以直接发起网络请求 wx.request() ,同样是也是为了代码的可维护性、复用性的问题

我们前面都是通过以上代码的方式做了很多实践,就会发现在其他页面 或 方法中都会用到 wx.request() 并还需要传递很多参数,每次都会重复的书写。这样的方式会带来很多隐患

  • 其中的 url 在多个方法中都会用到,一旦服务端 API 接口域名地址改变了,就需要把项目中所有的接口地址更新,是非常危险的,后期也很难维护
  • 因此,需要对 wx.request() 这个部分进行再次封装,从而提高项目的可维护性

接下来我们就会通过建立模型方式,增加一层来实现。如下

# 2、什么是 模型

TIP

  • 对某个业务或数据进行归纳总结,最终对外提供若干函数方法
  • 每个对外提供的函数方法都有各自独立的作用

我们需要定义一个通用的模型,后续只要其他页面有需求时,直接调用某个方法,就能拿到我想要的数据

在项目的根目录中创建文件夹 model

icoding-com-course
├─ model
│ ├─ course.js

/mode/course.js 中定义一个 Course

/**
 * @author arry老师
 * @description 课程相关
 */
class Course {
  /**
   * 分页获取课程列表
   * @param {Number} page 页码
   * @param {Number} count 每页数量
   * @param {Number} categoryId 分类 ID(可为空)
   * @param {Number} type 课程类型(可为空)
   */
  getCourseList(page, count, categoryId = null, type = null) {
    console.log("获取课程列表");
    // 发起网络请求,获取数据
  }
}

export default Course;

pages/index/index.js 页面逻辑 _getCourseList() 方法中调用模型层中的方法

// pages/index/index.js

// 导入 Course 类
import Course from "../../mode/course";
// 实例化 Course
const course = new Course();

Page({
  // 页面的初始数据
  data: {},

  // 生命周期函数--监听页面加载
  onLoad: function (options) {
    // 初始化课程列表
    this._getCourseList();

    // 执行其他函数 ......
  },

  // 获取课程列表(以 _ 开头,表示页面私有函数)
  _getCourseList() {
    // 调用模型层中的方法
    course.getCourseList(1, 10);
  },

  // 部分省略 ......
});

当页面加载时,就会成功调用模型层中的 getCourseList() 方法

# 3、模型的意义

TIP

用来分离调用 与 内部实现,实现功能解耦

# 3.1、传统的方式

image-20230424193857074

注:

按照传统方式,我们直接在 JS 中发起 wx.request 网络请求 -> 获取到数据 -> 返回到 JS 中 -> 完成数据绑定,渲染到页面

# 3.2、改变调用链路

TIP

新增一个 Model 模型类(增加一层)

image-20230424194136124

注:

在 JS 中调用 Model 模型类中的方法 -> 在模型类中再发起 wx.request 网络请求 -> 将获取到的数据返回给 Model 模型 -> 再从模型返回给 JS,同时还可有多个 JS (页面)调用模型层

这么做的目的就是为了解决上边提到的几个问题:

  • 当接口发生改变时,如果有多个页面 JS 中使用了就都需要修改,通过增加一个 模型层就能很好的解决这个问题
  • 将具体的请求封装到模型层中,JS 只跟模型做交互,我们还可以有多个 JS(页面)同时调用模型,如果一旦接口发生了改变,我们只需要修改模型层中的代码即可,模型暴露的通常都是模型的方法

也就是说,无论我们的接口怎么变,只需要在模型中做好适配即可,所有调用模型类的页面全部都不用做改动。这就叫做 “调用与实现的分离”,将可能存在变化的部分隔离起来,集中在一个地方。这与我们前面在自定义组件封装时提到的编程思想 “高类聚,低耦合” 类似。

把功能与功能之间的耦合度尽量的降低,又可以实现易于维护、并易于扩展的代码。这种在调用 与 实现中加一层的思想在软件工程中的实践是非常广泛的。

# 4、软件工程最佳实践

TIP

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。

被用于各个项目工程中,如 Java 工程

  • DO(Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
  • BO(Business Object):业务对象,由 Service 层输出的封装业务逻辑的对象。
  • AO(Application Object):应用对象,在 Web 层与 Service 层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
  • VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
  • Countroller:业务控制层,负责接收数据和请求,并且调用 Service 层实现这个业务逻辑。
  • Service:服务层或业务层,封装 Dao 层的操作,使一个方法对外表现为实现一种功能
  • Model:模型层

不同的项目分层设计都会不一样,尤其是在中大型项目中优势会体现的更明显,还会有更多其他的分层。做这么分层的目的和我们前面讲的都一样,就是为了 分离调用 与 内部实现,实现功能与功能之间的解耦。

注:

项目是否分层不是编程语言决定的,而是项目复杂度决定的。

# 5、分层设计的好处

TIP

  • 高内聚:分层的设计可以简化系统设计,让不同的层专注做某一模块的事
  • 低耦合:层与层之间通过接口 或 API 来交互,依赖方不用知道被依赖方的细节
  • 复用:分层之后可以做到很高的复用
  • 扩展性:分层架构可以让我们更容易做横向扩展

如果系统没有分层,当业务规模增加或流量增大时我们只能针对整体系统来做扩展。分层之后可以很方便的把一些模块抽离出来,独立成一个系统。

# 6、发起网络请求

TIP

通过上边的学习对于模型在工程实践中的意义有一些了解了。接下来就需要实现发起网络请求

/mode/course.js 模型层的 getCourseList 方法中发起网络请求

/**
 * @author arry老师
 * @description 课程相关
 */
class Course {
  /**
   * 分页获取课程列表
   * @param {Number} page 页码
   * @param {Number} count 每页数量
   * @param {Number} categoryId 分类 ID(可为空)
   * @param {Number} type 课程类型(可为空)
   */
  getCourseList(page, count, categoryId = null, type = null) {
    console.log("获取课程列表");
    // 发起网络请求,获取数据
    wx.request({
      url: "url",
    });
  }
}

export default Course;

以上代码

我们设计了一个模型类确实解决了模型方法的复用问题,当如果我们在模型方法中直接发起一个 wx.request 请求的话就会产生另外一些工程上的问题

  • ①、容易犯错:在工程实践中要 尽量避免提供犯错的机会。因为我们需要 request 中配置大量的参数,直接调用 wx.request 的方式对开发者不友好,很容易犯错。因此,需要将 wx.request 这个 API 做一层封装。
  • ②、请求响应 和 异常处理,如果请求成功了还好,如果失败了就需要每次单独处理和判断(根据不同状态码)。无法做到 统一的响应和处理,如果封装后就可以集中处理

wx.requestAPI 的封装,再一次体现了调用与实现分离的思想,这网络请求这个部分进一步做了解耦。

# 二、wx.request 二次封装,实现统一响应和异常处理

TIP

我们前面封装了一个 Model 模型类,但发现如果直接在模型类中直接发起请求获取数据,后期会造成大量的重复代码,在真实的开发场景下,一般都会网络请求做二次封装。

通过封装可以把统一的响应和异常处理集中在一个函数中,可以大大提高使用效率以及降低维护成本。

  • wx.request 二次封装,简化调用
  • 全局统一响应、异常处理

# 1、wx.request 封装

在项目根目录 utils 文件夹中新建 http.js 文件用于封装 wx.request

icoding-com-course
├─ utils
│ ├─ http.js

/utils/http.js

// 导入 API 接口根地址
import APIConfig from "../config/api";

/**
 * @author arry老师
 * @description 网络请求
 */
class Http {
  // 静态方法:
  // 当方法不需要使用到类的属性
  // 调用静态方法不需要实例化,直接使用 “类名.方法名” 即可直接调用方法了

  /**
   * 发起网络请求
   * @param {String} url 接口地址
   * @param {Object} data 服务器请求,需要传递参数
   * @param {String} method HTTP 请求方法 (默认值 GET)
   */
  static request(url, data, method = "GET") {
    wx.request({
      url: APIConfig.baseUrl + url, // 将接口根地址单独管理配置,方便统一修改
      data,
      method,
      success: (res) => {
        console.log(res);
        // 全局的统一响应 和 异常处理

        // 请求成功

        // 请求失败
      },
    });
  }
}

export default Http;

在项目根目录中新建 config 文件夹,并创建 api.js 文件

icoding-com-course
├─ config
│ ├─ api.js

/config/api.js

/**
 * @author arry老师
 * @description 服务器接口根地址(域名)
 */
const APIConfig = {
  baseUrl:
    "https://www.fastmock.site/mock/3f688708823217b086a3a3f316e13307/icoding-course",
};

export default APIConfig;

在模型层 /mode/course.js 中调用测试网络请求

import Http from "../utils/http";

/**
 * @author arry老师
 * @description 课程相关
 */
class Course {
  /**
   * 分页获取课程列表
   * @param {Number} page 页码
   * @param {Number} count 每页数量
   * @param {Number} categoryId 分类 ID(可为空)
   * @param {Number} type 课程类型(可为空)
   */
  getCourseList(page, count, categoryId = null, type = null) {
    console.log("获取课程列表");
    // 发起网络请求,获取数据
    Http.request({ url: "/api/course/list", data: { page, count } });
  }
}

export default Course;

网络请求数据获取成功

image-20230423045219701

通过服务端返回的 statusCode 状态码来判断请求是成功 还是 出现异常了

# 2、全局统一响应、异常处理

TIP

通过返回结果中的 statusCode 判断状态码(请求成功 OR 失败)

  • 请求成功:状态码正常是 200 ,其他如接口中返回有特殊的定义,按接口来即可
  • 请求失败:根据接口文档中定义的状态码,来进行判断即可
  • 其他公共的错误信息处理:接口错误信息一定要看清楚文档,哪些适合直接给用户展示,哪些不适合展示(仅开发者自己看的),一般情况会单独定义一个函数来处理。

如微信官方相关错误码,点击查看接口文档 - 错误码字典 (opens new window)

其他公共的错误信息处理思路如下

  • 在本地定义字典,将不适合对外展示的列举出来,当接口提示了对应的错误信息时,将接口返回的错误码和字典作比对
  • 如果找到了,就不展示这条错误信息,而是用我们另外定义的错误信息作展示;
  • 如果没有找到,就直接把接口返回的错误信息作展示

/utils/http.js 中定义

// 导入 API 接口根地址
import APIConfig from "../config/api";
import exceptionMessage from "../config/exception-message";

/**
 * @author arry老师
 * @description 网络请求
 */
class Http {
  /**
   * 发起网络请求
   * @param {String} url 接口地址
   * @param {Object} data 服务器请求,需要传递参数
   * @param {String} method HTTP 请求方法 (默认值 GET)
   */
  static request({ url, data, method = "GET" }) {
    wx.request({
      url: APIConfig.baseUrl + url, // 将接口根地址单独管理配置,方便统一修改
      data,
      method,
      success: (res) => {
        console.log(res);
        // 全局的统一响应 和 异常处理

        // 请求成功(测试异常处理时,先将其注释,由于我们该接口中没有返回大于 400 的状态码)
        if (res.statusCode < 400) {
          return res.data.data;
        }

        // 请求失败(将接口中定义的相关失败的状态码,进行判断)
        if (res.statusCode === 401) {
          return;
        }

        // 接口错误信息,一定要看清楚文档,哪些适合直接给用户展示,哪些不适合展示(仅开发者自己看的)
        // 正规的接口的文档都有会 错误码字典

        // 实现思路:
        // 在本地定义字典,将不适合对外展示的列举出来,当接口提示了对应的错误信息时,将接口返回的错误码和字典作比对。
        // 如果找到了,就不展示这条错误信息,而是用我们另外定义的错误信息作展示;
        // 如果没有找到,就直接把接口返回的错误信息作展示

        Http._showError(res.data.code, res.data.desc);
      },
    });
  }

  /**
   * 错误信息比对校验
   * @param {String} errorCode 接口返回的错误码
   * @param {String} message 接口返回的错误信息内容
   * @desc 演示时,需要在模型层 course.js 中,制造错误才可看到效果
   */
  static _showError(errorCode, message) {
    console.log(errorCode);
    // 弹窗展示给用户的信息
    let title = "";
    // 在对象中通过 [] 的方式进行匹配,如果匹配成功直接返回对应的值,如失败 返回 undefined
    const errorMessage = exceptionMessage[errorCode];
    console.log(errorMessage);

    // 判断如果返回的不是一个有效的字符串(undefined),就需要取其他的展示信息
    // 优先取字典中的内容,如果没有取原始接口中返回的内容,还没有就直接 “未知异常”
    title = errorMessage || message || "未知异常";

    // desc: {page: "page 不能为空",count: "count 不能为空"}
    // 如果有多个参数都异常的话,会返回一个对象
    // Object.values(title) 在对象上找到的可枚举属性值,返回一个数组
    // .join(';') 把数组中的所有元素转换一个字符串,通过指定的分隔符进行分隔的
    title = typeof title === "object" ? Object.values(title).join(";") : title;

    // 在页面中展示信息
    wx.showToast({
      title,
      icon: "none",
      duration: 3000,
    });
  }
}

export default Http;

在项目根目录中新建错误码字典

icoding-com-course
├─ config
│ ├─ exception-message.js

/config/exception-message.js 中定义字典,将不适合对外展示的列举出来

/**
 * @author arry老师
 * @description 不适合对外展示的错误码字典(在本地定义字典,将不适合对外展示的列举出来)
 */
const exceptionMessage = {
  "0002": "这是测试信息",
};

export default exceptionMessage;

注:

演示异常处理时,需要在模型层 course.js 的网络请求中故意制造错误才可看到效果。如:将 url 中的路径修改错误,再测试

# 3、在模型类中调用封装好的 request 请求

TIP

在模型类 mode/course.js 中调用 Http 类中封装好的 wx.request 请求,并接收返回值在控制台打印,输出请求返回结果

import Http from "../utils/http";

/**
 * @author arry老师
 * @description 课程相关
 */
class Course {
  /**
   * 分页获取课程列表
   * @param {Number} page 页码
   * @param {Number} count 每页数量
   * @param {Number} categoryId 分类 ID(可为空)
   * @param {Number} type 课程类型(可为空)
   */
  getCourseList(page, count, categoryId = null, type = null) {
    console.log("获取课程列表");
    // 发起网络请求,获取数据
    const res = Http.request({
      url: "/api/course/list",
      data: { page, count },
    });
    // 输出结果 为 undefined
    console.log(res);
  }
}

export default Course;

注:

当我们在模型类中打印输出返回结果为 undefined 这是为什么呢 ?

但我们在 Http 类中打印输出 res 请求结果时是有正确返回请求结果的。却在模型类 Course 中没有拿到接口返回的数据,而输出了 undefined

原因是:

Http 类的 request 方法中 success 返回的请求结果是在一个回调函数中,当请求成功时不能通过 return 取到值的。

那怎么才能拿到值呢 ?否则封装网络请求就么有意义了 !

# 5、解决方法 一 :回调函数

TIP

刚刚我们通过网络请求进行简单的封装,但我们测试时发现根本无法拿到接口返回的数据 !我们接下来就来解决这个问题

我们将最终通过之前学过的 JavaScript 中非常重要的一个机制 async/await 来解决此类问题,这其中就会涉及同步编程和异步编程的概念。

一起来看下从 回调函数 -> Promise -> async/await 整个演进过程,从而来了解如何告别回调地狱的问题

# 5.1、什么是回调地狱

TIP

我们可以看到代码中通过 wx.request 请求接口,请求成功后 success 中是通过回调函数的方式来接收请求的结果。因为 wx.request 本身就是微信小程序提供的一个异步 API ,当我们要去接收一个 异步 API 结果时,只能用另外一个函数传递给它,并在这个 API 的内部去调用函数来获取这种结果的目的。

这种回调函数 调用 回调函数的方式 就会形成回调地狱

// 导入 API 接口根地址
import APIConfig from "../config/api"
import exceptionMessage from "../config/exception-message"

/**
 * @author arry老师
 * @description 网络请求
 */
class Http {

    /**
     * 发起网络请求
     * @param {String} url 接口地址
     * @param {Object} data 服务器请求,需要传递参数
     * @param {String} method HTTP 请求方法 (默认值 GET)
     */
    static request({url,data,method='GET'}){

        // wx.request 本身就是微信小程序提供的一个异步API ,当我们要去接收一个 异步 API 结果时,只能用另外一个函数传递给它,并在这个API 的内部去调用函数来获取这种结果的目的
        wx.request({
          url: APIConfig.baseUrl + url,
          data,
          method,
          success: (res) => {
              // console.log(res)

              // 请求成功
              if(res.statusCode < 400
                  // 当请求成功时不能通过 return 取到值的
                  return res.data.data
                  console.log(res)
              }

              // 请求失败
              if(res.statusCode === 401){
                  return
              }

              // 错误信息比对校验
              Http._showError(res.data.code,res.data.desc)
          }
        })
    }

	// 省略部分 ......

}

export default Http

# 5.2、同步编程

TIP

同步编程的代码是按顺序执行的,下一行代码是建立在上一行代码执行完成的基础上才会执行,这就是同步编程。

如果我们的应用程序中所有的代码都是同步执行的就会有问题,一旦上一行代码的执行时间很长(如:20s) 就意味着在这条代码执行完毕之前,后边的代码永远都不会执行。这样就会大大的降低程序的执行效率 !因此就有了异步编程的概念 。

# 5.3、异步编程

TIP

如果我们要执行三条编程语句:①、②、③

在异步编程中它的执行顺序就改变了,当执行了第 ① 条语句后,第 ② 语句并不会等待第 ① 条语句执行完毕后才执行。而是执行了第 ① 条语句,就立马执行第 ② 条语句,同样第 ③ 条也同样

它们是并发执行的,通过这样的执行方式,我们的应用程序就可以提高工作效率了。问题来了,它异步执行后结果去哪里了 !我们还能够像原来一样用同步的方式把它的结果拿到吗 ?当然不行 !

这也是为什么我们上边的代码中,直接通过 return res.data.data 在模型类中是取不到值的

success: (res) => {
  if (res.statusCode < 400) {
    // 当请求成功时不能通过 return 取到值的
    return res.data.data;
  }
};

通过一张图来分析一下,整个异步执行的结果

image-20230424194723482

执行过程分析

  • 首先调用了一个微信的 wx.requesst API ->
  • 传递一个回调函数接收 API 返回的结果 ->
  • 接下来执行一个打印操作 console.log(res) 执行打印结果,这里肯定是拿不到结果的,因为它是分叉出去的 。

在这种异步编程中,如何才能实现在原来代码中去拿到异步函数的执行结果呢

image-20230424195120892

执行过程分析

  • 调用 微信的 wx.requesst API ->
  • 定义一个回调函数 A 接收请求的结果 -> 再定义一个回调函数 B
  • 然后在 回调函数 A 中写一行逻辑,即:当我们的结果拿到后,反过来去调用回调函数 B,同时把回调函数 A 之前拿到的结果传递给回调函数 B,这样就可以实现拿到回调函数 A 的请求结果了

这样的思路,就是用回调函数调用回调函数的方式,当然是可以取到值的。代码实现如下

在模型层 mode/course.jsHttp.request() 中增加一个参数

import Http from "../utils/http";

class Course {
  getCourseList(page, count, categoryId = null, type = null) {
    console.log("获取课程列表");

    // 在调用 request 方法时,在增加一个参数(函数式的参数)
    // 函数中接收参数 res 表示,异步回调中的结果,我们要在 Http.request 类库中反过来调用传递进来的函数
    const res = Http.request(
      { url: "/api/course/list", data: { page, count } },
      function (res) {
        console.log(res);
      }
    );
    // console.log(res)
  }
}

export default Course;

/utils/http.js 函数 request() 方法中增加 参数

// 导入 API 接口根地址
import APIConfig from "../config/api";
import exceptionMessage from "../config/exception-message";

class Http {
  // 增加接收回调函数的参数 callback 回调函数
  static request({ url, data, method = "GET" }, callback) {
    wx.request({
      url: APIConfig.baseUrl + url,
      data,
      method,
      success: (res) => {
        // 请求成功
        if (res.statusCode < 400) {
          // 调用 callback 回调函数,并将返回结果 res.data.data 传入
          callback(res.data.data);
          //   return res.data.data
        }

        // 请求失败
        if (res.statusCode === 401) {
          return;
        }

        Http._showError(res.data.code, res.data.desc);
      },
    });
  }

  // 省略部分 ......
}

export default Http;

以上代码中

可以看到在模型类中确实打印出了请求结果,我们通过让原本一个异步 API 的回调函数中,反过来在调用我们传递进去的参数(函数),在这里函数中就可以拿到异步的结果 !

就有一种魔法打败魔法的感觉了,这种回调函数 调用 回调函数的方式也是最原始解决异步编程计算结果的一种解决方案。

# 6、回调地狱解决方案演进脉络

TIP

以上我们通过回调函数 调用 回调函数的解决方案的问题就是 回调地狱。我们上边实现的代码仅仅只是嵌套了一层还好问题不大,如果需求更复杂了,嵌套层级多了就是真正变成了回调地狱,我们在 JavaScript 部分已经详细学过了。这里就不再赘述 !

这也是为什么随着前端应用越来多、越来越广泛之后,回调地狱的问题就被 JavaScript 社区摆上了台面。因为这个问题已经严重影响了我们在编写一些复杂项目时候的开发和维护效率。

针对回调地狱,社区首先提出了 Promise ,也是 JavaScript 中非常重要的知识点、也是应用非常广泛点。虽然它并没有完全解决我们的回调地狱问题,但它是一个非常有用的东西。包括我们后边会讲到的最终的解决方案,也是建立在 Promise 基础上的,它是实现的一个很重要的前提。

image-20230424195318441

# 6.1、使用 Promise 解决地狱回调

TIP

在 ES6 中推出了一种新的机制 Promise ,它的初衷就是为了解决回调地狱的问题。

/utils/http.js 中 我们不希望像原来,给 success 属性传递一个函数的方式来接收值,更新希望通过下边的方式来实现,定义个变量 res 直接就能拿到结果,Promise 的出现让这个愿景成为了可能

不过它本身对这个的解决就不是特别的彻底,后边还是需要 .then 还需要接收一个函数的方式(还是嵌套了函数)

// 伪代码
const res = wx
  .request()
  .then((res) => {})
  .then()
  .then()
  .then();

// ...... 一路 .then 下去将原来回调函数的方式 变成了 链式调用的方式,比原来的嵌套方式的回调地狱得到了很好的优化

注:

Promise 的链式调用从客观来看,确实是有很大的帮助和提升的。但这种解决方案并不完美,原因也是出在链式调用的方式上。

虽然 Promise 的出现确实让我们原来的嵌套方式调用变得相对扁平了,但还需要我们去维护这个调用链的。如果调用成功还好,我们知道 Promise 是有两种状态结果的(成功和异常)以上的代码是成功的状态。

异常如下

// 伪代码
const res = wx
  .request()
  .then((res) => {})
  .catch()
  .then()
  .catch()
  .then()
  .catch()

  // 出现异常后
  .catch();

// 每一个都捕获异常的话,就会变成这样的链式调用

注:

如果代码一旦复杂了,依然是非常丑陋的,并且难以维护这个调用链。因此就有最终的解决方案。

虽然,我们在项目中不会直接使用 Promise 的链式调用来开发,但 毕竟 Promise 是基础,我们接下来先将 wx.request 请求 Promise 化

# 6.2、小程序 API 的 Promise 化

TIP

回调地狱确实在实际工程中是一个大问题,在项目开发中就需要提前考虑到,面试题也爱考。

在小程序早期,很多 API 都是异步的 API ,它并没有提供原生支持 Promise 的 API,那时候我们都是自己封装一个函数 或 第三方库来转换一下,将小程序原生的异步 API 转成一个 Promise 对象,最后再使用 async/await 最终解决方案(后边会讲)来实现一个以同步代码编写的方式调用异步的函数,这样的最终实现结果。

这样的方式,维持了很长的一段时间,直到后来小程序在某一版本把官方提供的 API 中,绝大多数的异步 API 都提供了一种原生 Promise 的 支持。这样就大大简化了我们的开发工作,而且变得更友好了。

但,很不幸的是由于底层的实现机制问题,小程序的原生 API 中是有几个不支持 Promise 化的,还需要我们开发者自己手动转化,其中一个就是 网络请求 wx.request

详细查阅,微信小程序官方文档 - 异步 API 返回 Promise (opens new window)

在项目根目录 utils 文件夹中,新建一个 wx.js 文件,封装一个函数用来处理原生不支持 Promise 的官方 API

/utils/wx.js

/**
 * 转换原生不支持 Promise 的官方API
 * @param {String} method 需要调用的小程序官方 API
 * @param {Object} options 调用时需要传递的参数
 */
export default function wxToPromise(method, options = {}) {
  return new Promise((resolve, reject) => {
    options.success = resolve;
    options.fail = (err) => {
      reject(err);
    };
    wx[method](options);
  });
}

/utils/http.js 中使用封装好的 wxToPromise 函数

// 导入 API 接口根地址
import APIConfig from "../config/api";
import exceptionMessage from "../config/exception-message";
import wxToPromise from "./wx";

/**
 * @author arry老师
 * @description 网络请求
 */
class Http {
  /**
   * 发起网络请求
   * @param {String} url 接口地址
   * @param {Object} data 服务器请求,需要传递参数
   * @param {String} method HTTP 请求方法 (默认值 GET)
   */
  static request({ url, data, method = "GET" }, callback) {
    // 调用封装好的 wxToPromise 函数,使用 Promise 方式返回结果
    const res = wxToPromise("request", {
      url: APIConfig.baseUrl + url,
      data,
      method,
    });

    res.then((res) => {
      console.log(res);
    });

    // wx.request({
    //   url: APIConfig.baseUrl + url, // 将接口根地址单独管理配置,方便统一修改
    //   data,
    //   method,
    //   success: (res) => {
    //     //   console.log(res)
    //       // 全局的统一响应 和 异常处理

    //       // 请求成功(测试异常处理时,先将其注释,由于我们该接口中没有返回大于 400 的状态码)
    //       if(res.statusCode < 400){
    //           // 调用 callback 回调函数,并将返回结果 res.data.data 传入
    //           callback(res.data.data)
    //         //   return res.data.data
    //       }

    //       // 请求失败(将接口中定义的相关失败的状态码,进行判断)
    //       if(res.statusCode === 401){
    //           return
    //       }

    //       // 接口错误信息,一定要看清楚文档,哪些适合直接给用户展示,哪些不适合展示(仅开发者自己看的)
    //       // 正规的接口的文档都有会 错误码字典

    //       // 实现思路:
    //       // 在本地定义字典,将不适合对外展示的列举出来,当接口提示了对应的错误信息时,将接口返回的错误码和字典作比对。
    //       // 如果找到了,就不展示这条错误信息,而是用我们另外定义的错误信息作展示;
    //       // 如果没有找到,就直接把接口返回的错误信息作展示

    //       Http._showError(res.data.code,res.data.desc)
    //   }
    // })
  }

  // 部分省略 ......
}

export default Http;

通过调用 wxToPromise 函数,以上代码,测试控制台打印信息,成功返回值

# 6.3、改造完善 Http 类中的方法

TIP

现在就可以在 res.then() 中直接 return ,这个值也会被包装成一个 Promise 对象

/utils/http.js

// 导入 API 接口根地址
import APIConfig from "../config/api";
import exceptionMessage from "../config/exception-message";
import wxToPromise from "./wx";

/**
 * @author arry老师
 * @description 网络请求
 */
class Http {
  /**
   * 发起网络请求
   * @param {String} url 接口地址
   * @param {Object} data 服务器请求,需要传递参数
   * @param {String} method HTTP 请求方法 (默认值 GET)
   */
  static request({ url, data, method = "GET" }) {
    // 去掉 callback 参数

    // 调用封装好的 wxToPromise 函数,使用 Promise 方式返回结果
    const res = wxToPromise("request", {
      url: APIConfig.baseUrl + url,
      data,
      method,
    });

    // 同时,需要将整个成功的对象返回出去
    return res.then((res) => {
      if (res.statusCode < 400) {
        // callback(res.data.data)
        // 直接 return 也会被包装成一个 Promise 的对象,并且是一个成功的状态,外界调用时就能获取到值
        return res.data.data;
      }
      // 请求失败(将接口中定义的相关失败的状态码,进行判断)
      if (res.statusCode === 401) {
        return;
      }
      // 错误信息比对校验
      Http._showError(res.data.code, res.data.desc);
    });
  }

  // 省略部分 .......
}

export default Http;

在模型类mode/course.js中调用封装好的 request 方法,即可返回一个 Promise 对象

import Http from "../utils/http";

/**
 * @author arry老师
 * @description 课程相关
 */
class Course {
  /**
   * 分页获取课程列表
   * @param {Number} page 页码
   * @param {Number} count 每页数量
   * @param {Number} categoryId 分类 ID(可为空)
   * @param {Number} type 课程类型(可为空)
   */
  getCourseList(page, count, categoryId = null, type = null) {
    console.log("获取课程列表");

    // 发起网络请求,获取数据
    // 去掉原来的函数式的参数,直接输出就能返回 Promise 对象
    const result = Http.request({
      url: "/api/course/list",
      data: { page, count },
    });

    // 获取 Promise 对象
    console.log(result);

    // 获取 Promise 成功的值
    result.then((res) => {
      console.log(res);
    });
    // .catch(error=>{ // 失败
    //   console.log(error)
    // })

    // 返回到页面中,做数据绑定即可,但依然非常的麻烦
    // 需要再次以 Promise 对象的方式返回,一路 .then 的方式取值
    // return result.then()
  }
}

export default Course;

注:

以上代码中,我们发现返回 Promise 对象,但依然非常的麻烦,效率非常低下

# 7、async/await 终极解决方案

TIP

后来社区又出了一个方法 Generator 生成器一个全新的机制,可以在函数中实现暂停,且暂停的控制是交给调用者的。也就是说我们在Generator 函数中定义很多方法(如我们刚刚写的异步函数),通过暂停拿到值, 继续下一步 !就很好的解决我们的问题,同时在书写是也可以像写同步方法一样去调用异步函数。

但很遗憾有两点:

  • 微信小程序不支持 Generator
  • Generator 生成器在实际的项目工程中,最后演变成了一般只会在偏底层的组件、函数类库、框架中才会使用这个机制,在应用层面的开发很少会使用 Generator

一方面 Generator 在使用还是有开发成本的,很多概念不是很好理解,如果在应用层使用对开发者的要求就非常高了,这也是为什么小程序不支持 Generator 生成器的机制也是非常明智的。

# 7.1、async/await 的本质

TIP

在经历了各种方案之间的斗争之后,终于产生了终极的解决方案 async/await ,它本身不是一个全新的技术,也没有新的知识点。

它的本质就是 Promise + Generator 这两个解决方案的结合体,它是一个语法糖。

也就是说: 在实际的开发中,只需要通过简单的 两个关键字(async/await)就能实现 Promise + Generator 这两个解决方案的结合。从而完美的实现异步调用的问题。

image-20230424195638577

注:

有了 async/await 后,接下来只需要在包含有异步操作的函数或方法上,声明 async 关键字,通过这个声明就说明,这个函数或方法中是存在异步调用的

然后,在每一个异步调用的前面加上 await ,这个 await 顾名思义就是等待,当我们执行当前方法时,需要先等待它的结果返回(之前我们执行完该方法就会执行下面的代码,即异步执行)因此就不需要后面的 .then 了,就可以像写同步代码一样,对于返回值就可以直接拿来用。

# 7.2、通过 async/await 改造完善 Http 类中的方法

/utils/http.js

// 导入 API 接口根地址
import APIConfig from "../config/api";
import exceptionMessage from "../config/exception-message";
import wxToPromise from "./wx";

/**
 * @author arry老师
 * @description 网络请求
 */
class Http {
  // 在包含有异步操作的函数上,声明 async 关键词,说明这个函数或方法中是存在异步调用的
  static async request({ url, data, method = "GET" }) {
    // 异步调用的前面加上 await 等待它的结果返回
    const res = await wxToPromise("request", {
      url: APIConfig.baseUrl + url,
      data,
      method,
    });

    // 上边通过 await 关键字等待后,直接返回结果,下边就可以直接使用了,就跟同步代码一样的用法
    // return res.then((res)=>{

    if (res.statusCode < 400) {
      // 直接 return 也会被包装成一个 Promise 的对象
      return res.data.data;
    }
    // 请求失败(将接口中定义的相关失败的状态码,进行判断)
    if (res.statusCode === 401) {
      return;
    }
    // 错误信息比对校验
    Http._showError(res.data.code, res.data.desc);

    // })
  }

  // 省略部分 ......
}

export default Http;

在模型类 mode/course.js 中,添加 async/await

如果我们在模型类中需要调用该方法,并对结果进行计算或运算时,同样是需要在调用的方法上加上 async 关键字声明,同样 await 等待异步函数的执行结果

import Http from "../utils/http";

/**
 * @author arry老师
 * @description 课程相关
 */
class Course {
  // 在使用异步函数时,需要在调用的方法上加上 async 关键字声明
  async getCourseList(page, count, categoryId = null, type = null) {
    console.log("获取课程列表");

    // 同样需要添加,await 关键字,等待异步函数的执行结果
    const result = await Http.request({
      url: "/api/course/list",
      data: { page, count },
    });

    // 同样可以取到值
    console.log(result);
    // 再返回到页面中,做数据绑定即可
    return result;
  }
}

export default Course;

# 8、在页面逻辑中调用模型类中的方法

/pages/index/index.js

// pages/index/index.js

Page({
  // 页面的初始数据
  data: {},

  // 省略部分 .......

  // 生命周期函数--监听页面加载
  onLoad: function (options) {
    // 初始化课程列表
    this._getCourseList();

    // 执行其他函数 ......
  },

  // 添加 async 关键字
  async _getCourseList() {
    // 添加 await 关键字
    const courseList = await course.getCourseList(1, 10);
    // 打印验证
    console.log(courseList);
  },
});

以上代码

实现了通过 async、await 机制解决回调地狱的问题,这个机制是在我们真实开发场景中必备的能力,一定要掌握。

同时,从理论层面理解回调地狱解决方案的演进过程也是非常重要的,在面试中也非常喜欢问这个意义是什么 ? 本质上 async/await 就是为了解决原有的 回调函数、Promise、Generator 这几种方案的不完美,它本身是一种语法糖(结合了 Promise + Generator 这两种机制)

因此,我们在实际开发中,需要重点关注的是:回调函数、Promise、async/await 这三个,Generator 在应用层面会用的比较少。

当然我们上边封装的这些请求库还不是很完满,会根据项目需求再持续迭代优化

上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X