Skip to content

小程序慕尚花坊项目1

一、项目介绍

01. 项目概述

[慕尚花坊] 是一款 同城鲜花订购 的小程序,专业提供各地鲜花速递、鲜花预定、网上订花、包月鲜花等服务。最快3小时送花上门,保证花材新鲜和鲜花质量,可先送花后付款,专业花艺师傅精美包扎,品质保证,至诚服务。

02. 项目演示

[慕尚花坊] 项目涵盖电商项目常见功能模块,包含:

  1. 项目首页
  2. 商品分类
  3. 商品列表
  4. 商品详情
  5. 用户管理
  6. 收货地址
  7. 购物车
  8. 结算支付
  9. 订单管理
  10. 等……

03. 项目技术栈

[慕尚花坊] 项目使用原生小程序进行搭建开发,项目涵盖小程序开发所有常用的知识点

  1. 小程序内置组件:采用小程序内置组件 结合 Vant 组件库实现页面结构的搭建

  2. 项目中使用了 css 拓展语言 Scss 绘制页面的结构

  3. 小程序内置API:交互、支付、文件上传、地图定位、网络请求、预览图片、本地存储等

  4. 小程序分包加载:降低小程序的启动时间、包的体积,提升用户体验度

  5. 小程序组件开发:将页面内的功能模块抽象成自定义组件,实现代码的复用

  6. 网络请求封装:wx.request 方法封装、快捷方式封装、响应拦截器、请求拦截器

  7. 骨架屏组件:利用开发者工具提供了自动生成骨架屏代码的能力,提高了整体使用体验和用户满意度。

  8. UI 组件库:使用 Vant 组件库实现小程序 结构的绘制

  9. LBS:使用腾讯地图服务进行 LBS逆地址解析,实现选择收货地址功能

  10. miniprogram-licia:使用 licia 进行函数的防抖节流

  11. async-validator:使用 async-validator 实现表单验证

  12. miniprogram-computed: 使用 miniprogram-computed 进行计算属性功能

  13. mobx-miniprogram:使用 mobx-miniprogram 进行项目状态的管理

04. 接口文档

慕尚花坊系统-API文档

二、申请开发权限

在开始开发一个小程序项目之前,需要先申请开发权限。

需要将自己的微信号发送给对应小程序账号的管理员,在小程序微信公众后台添加我们自己为开发者。

📌:注意事项:

  1. 需要将自己的 微信号 发送给对应小程序账号的管理员

  2. 手机号不是微信号 (除非将手机号设置为了微信号)

如何查看自己的微信号:

在将微信号发送给管理员以后,管理员会登录微信公众后台,进行添加:

在管理员将自己设置为项目成员以后,开发者自己也可以登录当前小程序管理后台,获取 AppId

在获取到小程序 AppId 以后,就可以使用 AppId 新建小程序项目 或者 切换小程序项目的 AppId

三、项目初始化

01. 创建项目与项目初始化

创建项目

在微信开发者工具的开始界面左侧检查项目类型,需要为 [小程序]

然后在右侧点击 [+] 开始新建项目

最后在弹出的界面中输入项目相关的信息,点击确定即可

📌 注意

在新建项目的时候,[填写的 AppId 需要是自己的 AppId]

项目初始化

  1. 重置 app.js 中的代码
  2. 删除 app.jsonpages 下的 "pages/logs/logs" 路径,同时删除 pages/logs 文件夹
  3. 删除 app.jsonpages 下的 "rendererOptions" 以及 "componentFramework" 字段(不使用最新的渲染引擎)
  4. 重置 app.wxss 中的代码
  5. 删除 components 中的自定义组件
  6. 重置 pages/index 文件夹下的 index.jsindex.wxssindex.html 以及 index.json 文件
  7. 更新 utilsutil.js 的文件名为 formatTime.js

02. 自定义构建 npm + 集成Sass

随着项目的功能越来越多、项目越来越复杂,文件目录也变的很繁琐,为了方便进行项目的开发,开发人员通常会对目录结构进行调整优化,在慕尚花坊项目中,我们就需要将小程序源码放到 miniprogram 目录下

自定义构建

  1. 首先在项目根目录创建 miniprogram文件夹,将项目的核心文件移动到该目录下

  2. project.config.json 配置 miniprogramRoot 选项,指定小程序源码的目录

  3. 然后配置 project.config.jsonsetting.packNpmManuallytrue,开启自定义 node_modules 和 miniprogram_npm 位置的构建 npm 方式

    1. 最后配置 project.config.json 的 setting.packNpmRelationList 项,指定 packageJsonPathminiprogramNpmDistDir 的位置
    2. packageJsonPath 表示 node_modules 源对应的 package.json
    3. miniprogramNpmDistDir 表示 node_modules 的构建结果目标位置
  4. 安装 vant ,然后进行 npm 构建,测试是否能够正常 vant 构建成功

    npm i @vant/weapp

📌 注意

​ 配置后如果没有生效,需要 [ 重启微信开发者工具 ] ❗ ❗

集成 Sass

project.config.json 文件中,修改 setting 下的 useCompilerPlugins 字段为 ["sass"],即可开启工具内置的 sass 编译插件。

json
{
+ "miniprogramRoot": "miniprogram/",
    ....
  "setting": {
    "useCompilerPlugins": [
+      "sass"
     ],
+   "packNpmManually": true,
+   "packNpmRelationList": [
+      {
+        "miniprogramNpmDistDir": "./miniprogram",
+        "packageJsonPath": "./package.json"
+      }
+    ],
  "srcMiniprogramRoot": "miniprogram/"
   ......
}

03. 集成项目页面文件

思路分析:

打开 [慕尚花坊项目素材] 中的 [模板文件] 文件夹

复制该文件中全部的文件和文件夹,在项目的 miniprogram 目录下进行粘贴

代码分析:

  1. app.json 中配置了 pageswindowtabBar
  2. app.json 中对项目中会使用到的 Vant 组件进行了全部的注册
  3. app.scss 文件中导入了阿里巴巴使用图标库
  4. components 文件夹中定义了两个公共的组件
  5. pages 目录下存放了项目中所有页面的文件,后续我们会进行分包的处理

04. VsCode 开发小程序项目

知识点:

在进行项目开发的时候,部分同学可能不习惯微信开发者工具进行开发,而是习惯使用 VSCode 等编辑器

但是 VsCode 对小程序开发支持的不是非常友好,如果想通过 VSCode 开发小程序项目,需要安装以下插件:

  1. WXML - Language Service
  2. prettier
  3. 微信小程序开发工具
  4. 微信小程序助手-Y
  5. 小程序开发助手(可选)
  6. 其他......

💡 Tip:

使用 VsCode 开发小程序项目时,如果需要预览、调试小程序,依然需要借助微信开发者工具

安装插件:

配置详细插件:

  1. 在【项目的根目录】下创建 .vscode 文件夹,注意:文件夹名字前面带 . 点❗

  2. .vscode 文件夹下,创建 settings.json,用来对安装的插件属性进行设置,具体属性设置从下面复制即可

    • 注意:.vscode 文件夹下的 settings.json 文件只对当前一个项目生效
  3. 在【项目的根目录】下创建 .prettierrc 文件,进行 Prettier 代码规则的配置,规则从下面复制即可

  4. 为了让 Prettier 配置项在微信开发者工具生效,需要在微信开发者工具中也安装 Prettier 扩展插件。

➡️ .vscode/settings.json

json
{
  // 保存文件时是否自动格式化
  "editor.formatOnSave": true,

  // ---------------- 以下是 [ prettier ] 插件配置 ----------------

  // 指定 javascript、wxss、scss、less、json、jsonc 等类型文件使用 prettier 进行格式化
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

  "[wxss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

  "[less]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

  // Prettier 的一个配置项,用于指定哪些文件类型需要使用 Prettier 进行格式化
  "prettier.documentSelectors": ["**/*.wxml", "**/*.wxss", "**/*.wxs"],

  // ---------------- 以下是 [ WXML - Language Service ] 插件配置 ----------------

  // wxml 文件使用 prettier 进行格式化
  "[wxml]": {
    // "qiu8310.minapp-vscode" 是 WXML - Language Service 插件提供的配置项
    // 此插件主要是针对小程序的 wxml 模板语言,可以自动补全所有的组件、组件属性、组件属性值等等

    // 如果是 VsCode 需要开启这个配置
    "editor.defaultFormatter": "qiu8310.minapp-vscode"

    // 如果是微信小程序,需要开启这个配置,通过 esbenp.prettier-vscode 对代码进行格式化
    // "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

  // 创建组件时使用的 css 后缀
  "minapp-vscode.cssExtname": "scss", // 默认 wxss,支持 styl sass scss less css

  // 指定 WXML 格式化工具
  "minapp-vscode.wxmlFormatter": "prettier",
  // 配置 prettier 代码规范
  "minapp-vscode.prettier": {
    "useTabs": false,
    "tabWidth": 2,
    "printWidth": 80
  },

  // ---------------- 以下是 [ 微信小程序助手-Y ] 插件配置 ----------------

  // 新增、删除小程序页面时,是否自动同步 app.json pages 路径配置,默认为 false
  "wechat-miniapp.sync.delete": true,
  // 设置小程序页面 wxss 样式文件的扩展名
  "wechat-miniapp.ext.style": "scss",

  // ---------------- 其他配置项 ----------------

  // 配置语言的文件关联,运行 .json 文件时写注释
  // 但在 app.json 和 page.json 中无法使用
  "files.associations": {
    "*.json": "jsonc"
  }
}

➡️ .prettierrc

json
{
  "semi": false,
  "singleQuote": true,
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 180,
  "trailingComma": "none",
  "overrides": [
    {
      "files": "*.wxml",
      "options": { "parser": "html" }
    },
    {
      "files": "*.wxss",
      "options": { "parser": "css" }
    },
    {
      "files": "*.wxs",
      "options": { "parser": "babel" }
    }
  ]
}
配置项配置项含义
"semi": false不要有分号
"singleQuote": true使用单引号
"useTabs": false缩进不使用 tab,使用空格
"tabWidth": 2tab缩进为2个空格字符
"printWidth": 80一行的字符数,如果超过会进行换行,默认为80
"trailingComma": "none"尾随逗号问题,设置为none 不显示 逗号
"overrides": []overrides 解析器:默认情况下,Prettier 会根据文件文件拓展名推断要使用的解析器

📌:注意事项:

项目根目录 .vscode 文件夹中 settings.json 文件只对当前项目生效❗

如果想配置项生效,还需要注意:

在 VsCode 中只能打开当前一个小程序项目,不能同时打开多个小程序项目❗ 且项目目录请勿嵌套打开 ❗

四、通用模块封装

01. 为什么进行模块封装

在进行项目开发的时候,我们经常的会频繁的使用到一些 API,

例如:wx.showToast()wx.showModal()等消息提示 API ,这些 API 的使用方法如下:

js
wx.showToast({
  title: '消息提示框', // 提示的内容
  icon: 'success',   // 提示图标
  duration: 2000,	 // 提示的延迟时间
  mask: true		 // 是否显示透明蒙层,防止触摸穿透
})

wx.showModal({
  title: '提示', // 提示的标题
  content: '您确定执行该操作吗?', // 提示的内容
  confirmColor: '#f3514f', // 确定按钮的样式
  // 接口调用结束的回调函数(调用成功、失败都会执行)
  complete({ confirm, cancel }) {
    if (confirm) {
      console.log('用户点击了确定')
      return
    }
    if (cancel) {
      console.log('用户点击了取消')
    }
  }
})

如果每次使用的时候,都直接调用这些 API,会导致代码很冗余,为了减少了代码冗余,我们需要将这些 API 封装成公共方法,封装后的使用方式如下:

js
// wx.showToast() 封装后的调用方式
toast()
toast(title: '数据加载失败....', mask: true )

// wx.showModal() 封装后的调用方式
const res = await modal(title: '提示',content: '鉴权失败,请重新登录 ?')

// 用户点击了确定
if (res) { ... } else { ... }

02. 消息提示模块封装

基本使用:

wx.showToast() 消息提示框是在项目中频繁使用的一个小程序 API,常用来给用户进行消息提示反馈。使用方式如下:

js
wx.showToast({
  title: '消息提示框', // 提示的内容
  icon: 'success',   // 提示的图标,success(成功)、error(失败)、loading(加载)、none(不显示图标)
  duration: 2000,	 // 提示的延迟时间
  mask: true		 // 是否显示透明蒙层,防止触摸穿透
})

封装思路:

  1. 创建一个 toast 方法对 wx.showToast() 方法进行封装

  2. 在需要显示弹出框的时候调用 toast 方法,并传入相关的参数,有两种参数方式:

    • 不传递参数,使用默认参值
  • 传入部分参数,覆盖默认的参数

调用方式:

新封装的模块,我们希望有两种调用的方式:

  1. 模块化的方式对方法进行暴露,需要时导入使用

    js
    import { toast } from './extendApi'
    
    toast()
    toast(title: '数据加载失败....', mask: true)
  2. 将封装的模块挂载到 wx 全局对象身上,无需导入即可使用

    js
    wx.toast()
    wx.toast(title: '数据加载失败....', mask: true )

实现步骤:

  1. utils 目录下新建 extendApi.js 文件
  2. wx.showToast() 方法进行封装

落地代码:

➡️ utils/extendApi.js

js
// 在使用toast方法时可以传入参数也可以不传入参数,不传入参数时使用默认参数
const toast = (title = '数据加载中...', icon = 'none', duration = 2000, mask = true) => {
  wx.showToast({
    title: title,
    icon: icon,
    duration: duration,
    mask: mask
  })
}
// 将toast方法进行暴露
export { toast }
// 如果有很多js文件都需要调用toast方法,则每次都需要进行导入,过于麻烦
// 解决方法:将toast方法挂载到wx全局对象上,只需使用wx.toast()即可调用
wx.toast = toast

➡️ app.js

js
// import { toast } from './utils/extendApi'
// 导入 extendApi.js文件,先引入封装好的方法
import './utils/extendApi'
App({
  onShow() {
    // toast('项目启动成功', 'success')
    wx.toast('资源加载完毕', 'loading')
  }
})

03. 模态对话框封装

基本使用:

wx.showModal() 模态对话框也是在项目中频繁使用的一个小程序 API,通常用于向用户询问是否执行一些操作,例如:询问用户是否真的需要退出、是否确认删除等等

js
wx.showModal({
  title: '提示', // 提示的标题
  content: '您确定执行该操作吗?', // 提示的内容
  confirmColor: '#f3514f',
  // 接口调用结束的回调函数(调用成功、失败都会执行)
  complete({ confirm, cancel }) {
    confirm && console.log('点击了确定') // A&&B 如果表达式A为ture返回B
    cancel && console.log('点击了取消') 
  }
})

封装思路:

  1. wx.showModal() 方法进行封装, 封装后的新方法叫 modal
  2. 调用该方法时,根据需要传递参数覆盖默认参数
  3. 封装的 modal 方法的内部通过 Promise 返回用户执行的操作(确定和取消,都通过 resolve 返回)
  4. 在需要显示模态对话框的时候调用 modal 方法,并传入相关的参数,有二种调用方式:
    • 不传递参数,使用默认参数
    • 传递参数,覆盖默认的参数

调用方式:

新封装的本地存储模块,我们依然希望有两种调用的方式:

  1. 模块化的方式先暴露再导入使用
  2. 将封装的模块挂载到 wx 全局对象身上

实现步骤:

  1. extendApi.js 文件中新建 modal 方法,方法内部
  2. modal 方法,方法内部用来处理封装的逻辑

落地代码:

➡️ utils/extendApi.js

js
// 封装模态提示框
export const modal = (title = '提示', content = '您确定执行该操作吗?', showCancel = true, confirmColor = '#f3514f') => {
  // 通过Promise返回用户的操作,如果用户点击了确定返回true否则返回false
  return new Promise((resolve) => {
    // resolve:返回成功状态的Promise,可以通过await进行接收
    wx.showModal({
      title: title, // 提示的标题
      content: content, // 提示的内容
      confirmColor: confirmColor, // 确定按钮的颜色
      showCancel: showCancel, //是否展示取消按钮
      // 接口调用结束的回调函数(调用成功、失败都会执行)
      complete({ confirm, cancel }) {
        // 如果用户点击了确定,通过 resolve 抛出 true
        // 如果用户点击了取消,通过 resolve 抛出 false
        confirm && resolve(true)
        cancel && resolve(false)
      }
    })
  })
}
// 将封装好的方法挂载到wx全局对象上
wx.modal = modal

➡️ app.js

js
// import { toast, modal } from './utils/extendApi'
// 导入 extendApi.js文件,先引入封装好的方法
import './utils/extendApi'
App({
  async onShow() {
    // toast('项目启动成功', 'success')
    // wx.toast('资源加载完毕', 'loading')

    // const res = await modal()
    const res = await wx.modal('提示', '确定要删除该商品吗?')
    console.log(res) // true / false
  }
})

04. 封装本地存储 API

思路分析:

在小程序中,经常需要将一些数据存储到本地,方便多个页面的读取使用,例如:将用户的登录状态、用户的个人信息存储到本地。

小程序提供了同步、异步两类 API 来实现本地存储操作。例如: wx.setStorageSyncwx.setStorage 等方法

js
try {
  wx.setStorageSync(key, value) // 同步存储
} catch (err) { // try catch 对存储失败进行处理
  console.error(`存储指定 ${key} 数据发生错误:`, err)
}

wx.setStorage({ // 异步存储
  key: 'key',
  data: 'data',
  success (res) => {}, // 存储成功的回调
  fail (err) => {} // 存储失败的回调
})

如果直接使用这些 API,会比较麻烦,通常情况下,我们需要对本地存储的方法进行封装。

实现步骤:

  1. utils 目录下新建 storage.js 文件
  2. 在该文件中,封装对本地数据进行 存储、获取、删除、清除的方法(使用同步方法)

落地代码:

➡️ utils/storage.js

ts
// 封装本地存储的方法
// 用于存储数据 value可以为任意类型
export const setStroage = (key, value) => {
  try {
    wx.setStorageSync(key, value)
  } catch (error) {
    console.error('存储' + key + '异常')
  }
}
// 用于读取本地存储的数据
export const getStorage = (key) => {
  try {
    return wx.getStorageSync(key)
  } catch (error) {
    console.error('读取' + key + '异常')
  }
}
// 清除本地存储中指定的数据
export const removeStorage = (key) => {
  try {
    wx.removeStorageSync(key)
  } catch (error) {
    console.error('清除' + key + '异常')
  }
}
// 清除本地存储中的全部数据
export const clearStorage = () => {
  try {
    wx.clearStorageSync()
  } catch (error) {
    console.error('清空本地存储数据异常')
  }
}

➡️ app.js

js
import { clearStorage,removeStorage,getStorage,setStroage } from './utils/storage'
App({
    setStroage('token', '234k23n')
    setStroage('name', '王二狗')
    console.log(getStorage('name'))
	removeStorage('name')
	clearStorage()
})

05. 拓展:封装异步存储API

思路分析:

  1. 使用 Promise 封装异步存储 API

    js
    wx.setStorage({
      key: 'key',
      data: 'data',
      success(res) {}, // 存储成功的回调
      fail(err) {}, // 存储失败的回调
      complete(res) {} // 存储完成的回调
    })
  2. 给 toast 以及 modal 方法添加注释

使用方式:

js
// 异步将数据存储到本地
asyncSetStorage(key, data)
// 异步从本地读取指定 key 的数据
asyncGetStorage(key)
// 异步从本地移除指定 key 的数据
asyncRemoveStorage(key)
// 异步从本地移除、清空全部的数据
asyncClearStorage()

落地代码:

➡️ utils/storage.js

js
// 异步存储数据
export const asyncSetStorage = (key, data) => {
  // 返回一个Promise对象
  return new Promise((resolve) => {
    wx.setStorage({
      key,
      data,
      complete(res) {
        // 将存储成功/失败的信息返回 使用await进行接收
        resolve(res)
      }
    })
  })
}
// 异步读取数据
export const asyncGetStorage = (key) => {
  return new Promise((resolve) => {
    wx.getStorage({
      key,
      complete(res) {
        resolve(res)
      }
    })
  })
}
// 异步删除指定的数据
export const asyncRemoveStorage = (key) => {
  return new Promise((resolve) => {
    wx.removeStorage({
      key,
      complete(res) {
        resolve(res)
      }
    })
  })
}
// 异步清空本地存储的数据
export const asyncClearStorage = () => {
  return new Promise((resolve) => {
    wx.clearStorage({
      complete(res) {
        resolve(res)
      }
    })
  })
}

➡️ app.js

js
import { asyncClearStorage, asyncGetStorage, asyncRemoveStorage, asyncSetStorage } from './utils/storage'

App({
  async onShow() {
    const res1 = await asyncSetStorage('token', '234k23n')
    console.log(res1) // {errMsg: "setStorage:ok"}
    const res2 = await asyncSetStorage('name', '王二狗')
    console.log(res2)
    const res3 = await asyncGetStorage('name')
    console.log(res3) // {errMsg: "getStorage:ok", data: "王二狗"}
    const res4 = await asyncRemoveStorage('token')
    console.log(res4) // {errMsg: "removeStorage:ok"}
    asyncClearStorage()
  }
})

五、网络请求封装

01. 为什么要封装 wx.request

小程序大多数 API 都是异步 API,如 wx.request()wx.login() 等。这类 API 接口通常都接收一个 Object 对象类型的参数,参数中可以按需指定以下字段来接收接口调用结果:

参数名类型必填说明
successfunction调用成功的回调函数
failfunction调用失败的回调函数
completefunction调用结束的回调函数(调用成功、失败都会执行)
js
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 的 [使用方式] 来封装自己的网络请求模块,将wx.request封装成类似Axios网络请求

封装好的网络请求模块npm包地址 mina-request使用方法:

js
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
}

封装后网络请求模块包含以下功能

  1. 包含 request 实例方法发送请求
  2. 包含 getdeleteputpost 等实例方法可以快捷的发送网络请求
  3. 包含 请求拦截器、响应拦截器
  4. 包含 uploadFile 将本地资源上传到服务器 API
  5. 包含 all 并发请求方法
  6. 同时优化了并发请求时 loading 显示效果

02. 请求封装-request 方法

思路分析:

在封装网络请求模块的时候,采用 Class 类来进行封装,采用类的方式封装代码更具可复用性,也方便地添加新的方法和属性,提高代码的扩展性

我们先创建一个 class 类,同时定义 constructor 构造函数

js
// 创建 WxRequest 类
class WxRequest {
  constructor() {}
}

我们在 WxRequest 类内部封装一个 request 实例方法

request 实例方法中需要使用 Promise 封装 wx.request,也就是使用 Promise 处理 wx.request 的返回结果

request 实例方法接收一个 options 对象作为形参,options 参数和调用 wx.request 时传递的请求配置项一致

  • 接口调用成功时,通过 resolve 返回响应数据
  • 接口调用失败时,通过 reject 返回错误原因

➡️ /utils/request.js

js
// 采用类的方式进行封装会让代码更具有复用性
// 也方便添加新的属性和方法
class WxRequest {
  // 用于创建和初始化类的属性
  constructor() {}
  // 方法
  // request方法接收一个对象类型的参数,属性值和 wx.request 方法调用传递的参数保持一致
  request(options) {
    // 需要使用 Promise 封装 wx.request 处理异步请求
    return new Promise((resolve, reject) => {
      wx.request({
        ...options, // 将接收到的参数进行展开,作为请求方法的参数
        // 当接口调用成功时会触发success回调函数
        success: (res) => {
          resolve(res) // 返回请求成功的数据
        },
        // 当接口调用失败时会触发fail回调函数
        fail: (err) => {
          reject(err) // 返回错误信息
        }
      })
    })
  }
}

// 将实例暴露出去,方便在其他文件中使用
export default new WxRequest()

然后对 WxRequest 进行实例化,然后测试 request 实例方法是否封装成功!

注意:我们先将类 和 实例化的对象放到同一个文件中,这样方便进行调试,后面我们在拆分成两个文件

在其他模块中引入封装的文件后,我们期待通过 request() 方式发起请求,以 promise 的方式返回参数

➡️ /pages/test/test.js

js
// 导入封装好的请求实例
import instance from '../../utils/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)
      })
    // 第二种调用方式 await
    const result = await instance.request({
      url: 'https://gmall-prod.atguigu.cn/mall-api/index/findBanner',
      method: 'GET'
    })
    console.log(result)
  }
})

03. 请求封装-设置请求参数

思路分析:

在发起网络请求时,需要配置一些请求参数,

其中有一些参数我们可以设置为默认参数,例如:请求方法、超时时长 等等,因此我们在封装时我们要定义一些默认的参数。

js
// 默认参数对象
defaults = {
  baseURL: '', // 请求基准地址
  url: '', // 开发者服务器接口地址
  data: null, // 请求参数
  method: 'GET',// 默认请求方法
  // 请求头
  header: {
    'Content-type': 'application/json' // 设置数据的交互格式
  },
  timeout: 60000 // 小程序默认超时时间是 60000,一分钟
  // 其他参数...
}

但是不同的项目,请求参数的设置是不同的,我们还需要允许在进行实例化的时候,传入参数,对默认的参数进行修改。例如:

js
// 对 WxRequest 进行实例化
const instance = new WxRequest({
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api', // 请求基准地址
  timeout: 10000 // 微信小程序 timeout 默认值为 60000
})

在通过实例,调用 request 实例方法时也会传入相关的请求参数

js
const res = await instance.request({
  url: '/index/findBanner',
  method: 'GET'
})

从而得出结论:请求参数的设置有三种方式:

  1. 默认参数:在 WxRequest 类中添加 defaults 实例属性来设置默认值
  2. 实例化时参数:在对 WxRequest 类进行实例化时传入相关的参数,需要在 constructor 构造函数形参进行接收
  3. 调用实例方法时传入请求参数

默认参数和自定义参数的合并操作,通常会在constructor中进行。

因此我们就在 constructor 中将开发者传入的相关参数和defaults 默认值进行合并,需要传入的配置项覆盖默认配置项

➡️ utils/request.js

js
class WxRequest {
  // 定义实例属性用来设置默认请求参数
+  defaults = {
+    baseURL: '', // 请求基础路径
+    url: '', // 每个接口的请求路径会和基础路径进行拼接
+    data: null, // 请求参数
+    method: 'GET', // 默认请求方法
+    header: {
+      // 设置请求头
+      'Content-type': 'application/json' // 设置数据的交互格式
+    },
+    timeout: 60000 // 小程序默认的请求超时时间 1分钟
+  }:
  // 用于创建和初始化类的属性
+  constructor(params = {}) {
    // 将传递的参数覆盖默认参数
+    this.defaults = Object.assign({}, this.defaults, params)
+  }
  // 方法
  // request方法接收一个对象类型的参数,属性值和 wx.request 方法调用传递的参数保持一致
  request(options) {
    // 注意:需要先将基础路径和传入的url进行合并
+    options.url = this.defaults.baseURL + options.url
+    // 合并请求参数
+    options = { ...this.defaults, ...options }
    // 需要使用 Promise 封装 wx.request 处理异步请求
    return new Promise((resolve, reject) => {
      wx.request({
        ...options, // 将接收到的参数进行展开,作为请求方法的参数
        // 当接口调用成功时会触发success回调函数
        success: (res) => {
          resolve(res) // 返回请求成功的数据
        },
        // 当接口调用失败时会触发fail回调函数
        fail: (err) => {
          reject(err) // 返回错误信息
        }
      })
    })
  }
}
// -----------以下为测试代码-----------
// 将请求的实例暴露出去,方便在其他文件中使用
export default new WxRequest({
// 实例化请求对象时传入基础路径和超时时间
+  baseURL: 'https://gmall-prod.atguigu.cn/mall-api',
+  timeout: 15000
})

在调用 request 实例时也会传入相关的参数,是发起请求真正的参数,

我们需要将调用 reqeust 实例方法时传入的参数,继续覆盖合并以后的参数,请求才能够发送成功

注意:让使用传入的参数覆盖默认的参数,同时拼接完整的请求地址。

04. 请求封装-封装请求快捷方法

思路分析:

目前已经完成了 request() 请求方法的封装,同时处理了请求参数。

每次发送请求时都使用 request() 方法即可,但是项目中的请求方法有多种

需要封装 4 个快捷方法,分别是 getdeletepostput,他们的调用方式如下:

js
instance.get('请求地址', '请求参数', '请求配置')
instance.delete('请求地址', '请求参数', '请求配置')
instance.post('请求地址', '请求参数', '请求配置')
instance.put('请求地址', '请求参数', '请求配置')

这 4 个请求方法,都是通过实例化的方式进行调用,所以需要 Request 类中暴露出来 getdeletepostput 方法。每个方法接收三个参数,分别是:接口地址、请求参数以及其他参数。

这 4 个快捷方法,本质上其实还是调用 request 方法,我们只要在方法内部组织好参数,调用 request 发送请求即可

➡️ utils/request.js

js
class WxRequest {
  // 定义实例属性用来设置默认请求参数
  defaults = {
    baseURL: '', // 请求基础路径
    url: '', // 每个接口的请求路径会和基础路径进行拼接
    data: null, // 请求参数
    method: 'GET', // 默认请求方法
    header: {
      // 设置请求头
      'Content-type': 'application/json' // 设置数据的交互格式
    },
    timeout: 60000 // 小程序默认的请求超时时间 1分钟
  }
  // 用于创建和初始化类的属性
  constructor(params = {}) {
    // 将传递的参数覆盖默认参数
    this.defaults = Object.assign({}, this.defaults, params)
  }
  // 请求方法
  // request方法接收一个对象类型的参数,属性值和 wx.request 方法调用传递的参数保持一致
  request(options) {
    // 注意:需要先将基础路径和传入的url进行合并
    options.url = this.defaults.baseURL + options.url
    // 合并请求参数
    options = { ...this.defaults, ...options }
    // 需要使用 Promise 封装 wx.request 处理异步请求
    return new Promise((resolve, reject) => {
      wx.request({
        ...options, // 将接收到的参数进行展开,作为请求方法的参数
        // 当接口调用成功时会触发success回调函数
        success: (res) => {
          resolve(res) // 返回请求成功的数据
        },
        // 当接口调用失败时会触发fail回调函数
        fail: (err) => {
          reject(err) // 返回错误信息
        }
      })
    })
  }
  // 封装GET实例方法
  get(url, data = {}, config = {}) {
    // url:请求地址 data:请求数据 config:其他配置项
    // 本质还是调用了request方法,只是将请求方法固定为GET,将调用request方法的返回值通过return返回
    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))
  }
  // 封装DELETE实例方法
  delete(url, data = {}, config = {}) {
    return this.request(Object.assign({ url, data, method: 'DELETE' }, config))
  }
  // 封装PUT实例方法
  put(url, data = {}, config = {}) {
    return this.request(Object.assign({ url, data, method: 'PUT' }, config))
  }
}

// -----------以下为测试代码-----------
// 将请求的实例暴露出去,方便在其他文件中使用
export default new WxRequest({
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api',
  timeout: 15000
})

➡️ /pages/test/test.js

js
// 导入封装好的请求实例
import instance from '../../utils/request'
Page({
  async handler() {
    // 使用快捷方法发送请求
    let result1 = await instance.get('/goods/list/1/10?category1Id=1')
    console.log(result1)
  }
})

05. 请求封装-wx.request 注意事项

知识点:

在使用 wx.request 发送网络请求时,只要成功接收到服务器返回(请求成功发出),无论statusCode是多少,都会进入 success 回调,开发者需要根据业务逻辑对返回值进行判断。

什么时候会有 fail 回调函数 ?

一般只有网络出现异常、请求超时等(请求无法发出)时候,才会走 fail 回调

测试代码

js
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 添加拦截器功能,拦截器包括 请求拦截器响应拦截器

请求拦截器本质上是在请求之前调用的函数,用来对请求参数进行新增和修改(携带token)

响应拦截器本质上是在响应之后调用的函数,用来操作相应的数据(简化返回的数据,处理http网络错误 )

注意:不管成功响应还是失败响应,都会执行响应拦截器

拦截器的使用方式:

js
// 请求拦截器
instance.interceptors.request = (config) => {
  // 在发送请求之前做些什么
  return config  // 必须返回配置对象
}
// 响应拦截器
instance.interceptors.response = (response) => {
  // 对响应数据做点什么
  return response
}

通过使用方式,我们可以得出结论:

可以在 WxRequest 类内部定义 interceptors 实例属性,属性中需要包含 request 以及 response 方法

需要注意:在发送请求时,还需要区分是否通过实例调用了拦截器:

  1. 没有通过实例调用拦截器,需要定义默认拦截器,在默认拦截器中,需要将请求参数进行返回
  2. 通过实例调用拦截器,那么实例调用的拦截器会覆盖默认的拦截器方法,然后将新增或修改的请求参数进行返回

实现拦截器的思路:

  1. WxRequest 类内部定义 interceptors 实例属性,属性中需要包含 request 以及 response 方法
  2. 是否通过实例调用了拦截器(是否进行了二次封装)
    • 否:使用默认拦截器
    • 是:实例调用的拦截器覆盖默认拦截器
  3. 在发送请求之前,调用请求拦截器
  4. 在服务器响应以后,调用响应拦截器
    • 不管成功、失败响应,都需要调用响应拦截器

WxRequest 类内部定义 interceptors 实例属性,属性中需要包含 request 以及 response 方法。

➡️ utils/request.js

js
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.data // 简化返回数据
+ }

// 将 WxRequest 实例进行暴露出去,方便在其他文件中进行使用
export default instance

在发送请求之前,调用请求拦截器,在服务器响应以后,调用响应拦截器

不管成功、失败,都需要调用响应拦截器

js
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...
}

07. 请求封装-完善请求/响应拦截器

思路分析:

在响应拦截器,我们需要判断是请求成功,还是请求失败,然后进行不同的业务逻辑处理。

例如:请求成功以后将数据简化返回,网络出现异常则给用户进行网络异常提示

目前不管请求成功 (success),还是请求失败(fail),都会执行响应拦截器

那么怎么判断是请求成功,还是请求失败呢 ?

封装需求:

  1. 如果请求成功,将响应成功的数据传递给响应拦截器,同时在传递的数据中新增 isSuccess: true 字段,表示请求成功
  2. 如果请求失败,将响应失败的数据传递给响应拦截器,同时在传递的数据中新增 isSuccess: false 字段,表示请求失

在实例调用的响应拦截中,根据传递的数据进行以下的处理:

  • 如果isSuccess: true 表示服务器响应了结果,我们可以将服务器响应的数据简化以后进行返回
  • 如果isSuccess: false 表示是网络超时或其他网络问题,提示 网络异常,同时将返回即可

➡️ utils/request.js

js
class WxRequest {
  .....
   request(options) {
	.....
    // 使用 Promise 封装异步请求
    return new Promise((resolve, reject) => {
      // 使用 wx.request 发起请求
      wx.request({
        ...options,
        // 接口调用成功的回调函数
        success: (res) => {
          // 响应成功以后触发响应拦截器
+          // isSuccess属性用于判断响应是否成功
+          res = this.interceptors.response(Object.assign({}, res, { config: options, isSuccess: true }))
+          resolve(res) // 返回请求成功的数据
        },

        // 接口调用失败的回调函数
        fail: (err) => {
            // 将isSuccess设置为false
+           err = this.interceptors.response(Object.assign({}, err, { config: options, isSuccess: false }))
          reject(err) // 返回错误信息
        }
      })
    })
  }
    .....
}

// -----------------------------------------------------

// 对 WxRequest 进行实例化
const instance = new WxRequest({
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api'
})

// 设置请求拦截器
instance.setRequestInterceptor((config) => {
  console.log('执行请求拦截器')
  return config
})

// 设置响应拦截器
instance.interceptors.response = (response) => {
  // 从服务器返回的数据中获取isSuccess(服务器是否返回数据)和statusCode(状态码)
+  const { isSuccess, statusCode } = response
+  if (isSuccess) {
    // 服务器响应成功
    // 对响应状态码进行判断
+    switch (statusCode) {
+      case 200:
+        return response.data // 简化返回数据
+      case 208:
+        wx.toast('token失效', 'error')
+        break
+      case 404:
+        wx.toast('请求地址错误', 'error')
+        break
+      default:
+        wx.toas('网络异常', 'error')
+    }
+  } else {
+    // 服务器响应失败,网络异常
+    wx.toas('网络异常', 'error')
+    return response
+  }
}

// 将 WxRequest 的实例通过模块化的方式暴露出去
export default instance

08. 请求封装-使用请求/响应拦截器

思路分析:

使用请求拦截器:

在发送请求时,购物车列表、收货地址、更新头像等接口,都需要进行权限验证,因此我们需要在请求拦截器中判断本地是否存在访问令牌 token ,如果存在就需要在请求头中添加 token 字段。

使用响应拦截器:

在使用 wx.request 发送网络请求时。只要成功接收到服务器返回,无论statusCode是多少,都会进入 success 回调,因此开发者根据业务逻辑对返回值进行判断。

后端返回的业务状态码如下:

  1. 业务状态码(code)=== 200, 说明接口请求成功,服务器成功返回了数据
  2. 业务状态码(code)=== 208, 说明没有 token 或者 token 过期失效,需要登录或者重新登录
  3. 业务状态码(code)=== 其他,说明请求或者响应出现了异常

➡️ utils/request.js(对 wx.request 进行封装)

js
// 采用类的方式进行封装会让代码更具有复用性,也方便添加新的属性和方法
// 暴露封装好的wx.request类
export default 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(params = {}) {
    // 将传递的参数覆盖默认参数
    this.defaults = Object.assign({}, this.defaults, params)
  }
  // 请求方法
  // request方法接收一个对象类型的参数,属性值和 wx.request 方法调用传递的参数保持一致
  request(options) {
    // 注意:需要先将基础路径和传入的url进行合并
    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) => {
          // 不管是成功响应还是失败响应,都需要调用响应拦截器
          // 响应拦截器需要接收服务器响应的数据对数据进行处理,再进行返回
          // 在给响应拦截器传递参数时,需要将请求参数也一起传递,方便进行代码的调试和其他逻辑处理,需要进行参数合并
          // isSuccess属性用于判断响应是否成功
          res = this.interceptors.response(Object.assign({}, res, { config: options, isSuccess: true }))
          resolve(res) // 返回请求成功的数据
        },
        // 当接口调用失败时会触发fail回调函数
        fail: (err) => {
          err = this.interceptors.response(Object.assign({}, err, { config: options, isSuccess: false }))
          reject(err) // 返回错误信息
        }
      })
    })
  }
  // 封装GET实例方法
  get(url, data = {}, config = {}) {
    // url:请求地址 data:请求数据 config:其他配置项
    // 本质还是调用了request方法,只是将请求方法固定为GET,将调用request方法的返回值通过return返回
    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))
  }
  // 封装DELETE实例方法
  delete(url, data = {}, config = {}) {
    return this.request(Object.assign({ url, data, method: 'DELETE' }, config))
  }
  // 封装PUT实例方法
  put(url, data = {}, config = {}) {
    return this.request(Object.assign({ url, data, method: 'PUT' }, config))
  }
}

➡️ utils/http.js(对 wx.request 进行二次封装)

js
import WxRequest from './request'
import { getStorage, clearStorage } from './storage'
import { toast, modal } from './extendApi'
// 实例化请求对象
const instance = new WxRequest({
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api', // 请求的基础路径
  timeout: 15000 // 超时时间
})

// 自定义请求拦截器 会覆盖默认的请求拦截器
instance.interceptors.request = (config) => {
  // 在请求头中携带token
  config.header['token'] = getStorage('token')
  return config
}
// 自定义响应拦截器
instance.interceptors.response = async (response) => {
  // 从服务器返回的数据中获取isSuccess(服务器是否返回数据)和statusCode(状态码)
  const { isSuccess, data } = response
  if (isSuccess) {
    // 服务器响应成功
    // 对响应状态码进行判断 
    switch (data.code) {
      case 200: // 成功返回数据
        return data // 简化返回数据
      case 208: // token 失效 需要用户重新登录
        const isConfirm = await modal('提示', '身份验证过期,请重新登录', false) // 返回用户是否点击登录
        // 用户点击登录后清除本地存储的信息并跳转到登录页面
        if (isConfirm) {
          clearStorage()
          wx.navigateTo({
            url: '/pages/login/login'
          })
        }
        // 将Promise设置为失败状态并返回错误信息
        return Promise.reject(response)
      default:
        toast('小程序出现异常请重启小程序')
        // 将Promise设置为失败状态并返回错误信息
        return Promise.reject(response)
    }
  } else {
    // 服务器响应失败
    toast('网络异常', 'error')
    return response
  }
}
// 暴露二次封装的wx.request
export default instance

09. 请求封装-添加并发请求

思路分析:

前端并发请求是指在前端页面同时向后端发起多个请求的情况。当一个页面需要请求多个接口获取数据时,为了提高页面的加载速度和用户体验,可以同时发起多个请求,这些请求之间就是并发的关系。

我们通过两种方式演示发起多个请求:

  1. 使用 asyncawait 方式
  2. 使用 Promise.all() 方式

首先使用asyncawait 方式发送请求,使用 asyncawait 能够控制异步任务以同步的流程执行,代码如下

出现问题:

当第一个请求执行完以后,才能执行第二个请求,这样就会造成请求的阻塞,影响渲染的速度,如下图(请求是以同步的方式进行的)

解决方法:

这时候我们需要使用 Promise.all() 方式同时发起多个异步请求,并在所有请求完成后再进行数据处理和渲染。

使用Promise.all() 能够将多个请求同时发出,不会造成请求的阻塞,从而不会影响页面的渲染速度

在 WxRequest 实例中封装 all 方法,方法的内部,使用 Promise.all() 接收传递的多个异步请求,将处理的结果返回即可。

➡️ utils/request.js

js
class WxRequest {
  .....
+   // 封装处理并发请求的 all 方法
+   all(promise) { // 接收传递过来的请求数组
+     return Promise.all(promise)
+   }
....
}
.......

➡️ /pages/test/test.js

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)
  }
})

注意:

Promise.all:当全部的Promise状态为成功返回状态为成功,有一个状态为失败,返回状态为失败,可以使用 Promise.allSettled来代替

Promise.allSettled:返回结果永远为成功状态,成功的状态的值为每一个Promise对象状态和值

10. 请求封装-添加 loading

思路分析:

在封装时添加 loading 效果,从而提高用户使用体验

  1. 在请求发送之前,需要通过 wx.showLoading 展示 loading 效果

  2. 当服务器响应数据以后,需要调用 wx.hideLoading 隐藏 loading 效果

要不要加 loading 添加到 WxRequest 内部 ?

  1. 在类内部进行添加,方便多个项目直接使用类提供的 loading 效果,也方便统一优化 wx.showLoading 使用体验。

    但是不方便自己来进行 loading 个性化定制。

  2. 如果想自己来控制 loading 效果,带来更丰富的交互体验,就不需要将 loading 封装到类内部,但是需要开发者自己来优化 wx.showLoading 使用体验,每个页面都要写一份。

在项目中我们会选择第一种方式

不过也会通过属性控制是否展示 loading,从而方便类使用者自己控制 loading 显示

落地代码:

➡️ utils/request.js

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回调函数
        success: (res) => {
          // coding...
        },
        // 当接口调用失败时会触发fail回调函数(出现网络错误)
        fail: (err) => {
          // coding...
        },
          // 无论接口调用成功或失败都会执行
+         complete: () => {
+           // 接口调用完成后隐藏 loding
+           wx.hideLoading()
+         }
      })
    })
  }
  // coding...
}

Tips:

也可以在自己封装的请求拦截器中开启加载效果,在响应拦截器中关闭加载效果

11. 请求封装-完善 loading

思路分析:

目前在发送请求时,请求发送之前会展示 loading,响应以后会隐藏 loading

但是 loading 的展示和隐藏会存在以下问题:

  1. 每次请求都会执行 wx.showLoading(),但是页面中只会显示一个,后面的 loading会将前面的覆盖
  2. 同时发起多次请求,只要有一个请求成功响应就会调用 wx.hideLoading,导致其他请求还没完成,也不会 loading
  3. 请求过快 或 一个请求在另一个请求后立即触发,这时候会出现 loading 闪烁问题

我们通过 队列 的方式解决这三个问题:首先在类中新增一个实例属性 queue,初始值是一个空数组

  1. 发起请求之前,判断 queue 如果是空数组则显示 loading ,然后立即向queue新增请求标识
  2. complete 中每次请求成功结束,从 queue 中移除一个请求标识,queue 为空时隐藏 loading
  3. 为了解决网络请求过快产生loading 闪烁问题,可以使用定时器来做判断即可

原理:在发送并发请求时,每发送一次请求就向数组queue中添加一个标识,当请求完成时删除数组queue中的一个标识,当数组queue为空时关闭加载效果。从而实现当并发请求全部执行完毕后再关闭加载效果,当没有请求时设置定时器等待一定时间后再关闭加载效果

落地代码:

➡️ utils/request.js

js
class WxRequest {
    
  // coding...

+     // 初始化 queue 数组,用于存储请求队列
+     this.queue = []
  }

  // 创建 request 请求方法
  request(options) {
+     // 如果有新的请求,则清空上一次的定时器
+     this.timerId && clearTimeout(this.timerId)
     
    .....
    // 如果存在请求拦截器,我们则调用请求拦截器
    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,
       ......
        complete: () => {
+         // 从queen中移除一个标识
+         this.queen.pop()
+         // 当全部请求都返回 1s后隐藏loading
+          this.queen.length === 0 &&
+           setTimeout(() => {
+             wx.hideLoading()
+           }, 100)
+       }
      })
    })
  }
// coding...

export default instance

12. 请求封装-控制 loading 显示

思路分析:

在我们封装的网络请求文件中,通过 wx.showLoading 默认显示了 loading 效果

但是在实际开发中,有的接口可能不需要显示 loading 效果,或者开发者希望自己来控制 loading 的样式与交互,那么就需要关闭默认 loading 效果。

这时候我们就需要一个开关来控制 loading 显示。

  1. 类内部设置默认请求参数 isLoading 属性,默认值是 true,在类内部根据 isLoading 属性做判断即可
  2. 某个接口不需要显示 loading 效果,可以在发送请求的时候,可以新增请求配置 isLoading 设置为 false
  3. 整个项目都不需要显示loading 效果,可以在实例化的时候,传入 isLoading 配置为 false

实现步骤:

  1. 在 WxRequest 类的默认请求配置项中,设置 isLoading 默认值为 true,显示 loading

    js
    class WxRequest {
      // 初始化默认的请求属性
      defaults = {
        url: '', // 开发者服务器接口地址
        data: null, // 请求参数
        header: {}, // 设置请求的 header
        timeout: 60000, // 超时时间
        method: 'GET', // 请求方式
    +    isLoading: true // 是否显示 loading 提示框
      }
    
      // code...
    }
  2. 在进行实例化的时候,可以配置 isLoading 配置为 false,隐藏 loading

    js
    // 对 WxRequest 进行实例化
    const instance = new WxRequest({
      baseURL: 'https://gmall-prod.atguigu.cn/mall-api',
    +   isLoading: false // 隐藏 loading
    })
  3. 在发送网络请求时候,传入请求配置 isLoading 配置为 false,隐藏 loading

    js
    async func() {
    +  // 请求配置 isLoading 配置为 false,隐藏 loading
    +  await instance.get('/index/findCategory1', null, { isLoading: true })
    }
  4. wx-request 内部代码实现

    js
    // 创建 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,用来将本地资源上传到服务器。

例如:在获取到微信头像临时地址以后,将微信头像地址上传到公司服务器,获取持久地址。

js
wx.uploadFile({
  url: '', // 必填项,开发者服务器地址
  filePath: '', // 必填项,要上传文件资源的路径 (本地路径)
  name: '' // 必填项,文件对应的 key,开发者在服务端可以通过这个 key 获取文件的二进制内容(由后端决定)
})
// 示例
// 将头像路径上传到服务器,获取服务器返回的头像路径
    wx.uploadFile({
      filePath: avatarUrl, // 要上传的文件路径
      name: 'file', // 文件对应的key,服务器需要根据key获取文件二进制信息
      url: 'https://gmall-prod.atguigu.cn/mall-api/fileUpload', // 接口的服务器地址
      success: (res) => {
        // 服务器返回的为JSON格式的数据
        console.log(res.data)
        res.data = JSON.parse(res.data)
        // 获取服务器返回的URL地址
        console.log(res.data.data)
        // 将服务器返回额度URL地址进行存储
        this.setData({
          avatarUrl: res.data.data
        })
      }
    })

在了解了 API 以后,我们直接对 wx.uploadFile 进行封装即可。

首先在 WxRequest 类内部创建 upload 实例方法,实例方法接收四个属性:

js
/**
 文件上传接口封装
 url 文件上传地址,ilePath 要上传文件资源的路径,name 文件对应的 key,config 其他配置项
*/
upload(url, filePath, name, config = {}) {
  return this.request(
    Object.assign({ url, filePath, name, method: 'UPLOAD' }, config)
  )
}

这时候我们需要在 request 实例方法中,对 method 进行判断,如果是 UPLOAD,则调用 wx.uploadFile 上传API

➡️ utils/request.js

js
// request 实例方法接收一个对象类型的参数
// 属性值和 wx.request 方法调用时传递的参数保持一致
  request(options) {
    // 如果有新的请求,就清除上一次的定时器
    this.timeId && clearTimeout(this.timeId)
    // 注意:需要先将基础路径和传入的url进行合并
    options.url = this.defaults.baseURL + options.url
    // 合并请求参数
    options = { ...this.defaults, ...options }
    // 在请求发送之前,判断是否开启加载效果
+    if (options.isLoading && options.method != 'UPLOAD') {
+      // wx.upload自带加载效果
      // 判断 queue 队列是否为空,如果为空显示loading,如果不为空则不显示loading
      this.queue.length === 0 &&
        wx.showLoading({
          title: '加载中...'
        })
      // 然后向queue数组队列中添加请求标识
      this.queue.push('request')
    }
    // 在请求发送之前调用请求拦截器,对请求参数进行新增,修改
    options = this.interceptors.request(options)
    // 需要使用 Promise 封装 wx.request 处理异步请求
      return new Promise((resolve, reject) => {
+      if (options.method === 'UPLOAD') {
+        wx.uploadFile({
+          ...options,
+          success: (res) => {
+            // 1.对服务器返回的JSON字符串转成对象
+            res.data = JSON.parse(res.data)
+            // 2.合并参数传递给响应拦截器
+            const mergeRes = Object.assign({}, res, {
+              config: options,
+              isSuccess: true
+            })
+            // 3.将响应拦截器返回的数据返回
+            resolve(this.interceptors.response(mergeRes))
+          },
+          fail: (err) => {
+            // 1.合并参数传递给响应拦截器
+            const mergeRes = Object.assign({}, err, {
+              config: options,
+              isSuccess: true
+            })
+            // 2.将响应拦截器返回的数据返回
+            reject(this.interceptors.response(mergeRes))
+          }
+        })
    } else {
      wx.request({
        // coding...
      })
    }
  })
....
+  // upload实例方法,用来对wx.uploadFile进行封装
+  upload(url, filePath, name = 'file', config = {}) {
+    return this.request(
+      Object.assign({ url, filePath, name, method: 'UPLOAD' }, config)
+    )
+  }
}

test/test.js

js
Page({
  data: {
    avatarUrl: '../../assets/Jerry.png'
  },

// 获取微信头像
async chooseavatar(event) {
    // 获取临时头像路径
    const { avatarUrl } = event.detail
    // 调用封装好的uploadFile方法,将临时头像路径上传到服务器
    const result = await instance.upload('/fileUpload', avatarUrl, 'file')
    // 将服务器返回的持久头像路径进行存储
    this.setData({
      avatarUrl: result.data
    })
  },
}

完整封装代码:

request.js

js
// 采用类的方式进行封装会让代码更具有复用性,也方便添加新的属性和方法
// 暴露封装好的wx.request类
export default class WxRequest {
  // 定义实例属性用来设置默认请求参数
  defaults = {
    baseURL: '', // 请求基础路径
    url: '', // 每个接口的请求路径会和基础路径进行拼接
    data: null, // 请求参数
    method: 'GET', // 默认请求方法
    header: {
      // 设置请求头
      'Content-type': 'application/json' // 设置数据的交互格式
    },
    timeout: 60000, // 小程序默认的请求超时时间 1分钟
    isLoading: true // 控制是否使用默认的loading
  }
  // 定义拦截器对象(默认请求拦截器 + 默认响应拦截器)
  interceptors = {
    // 默认请求拦截器:在请求发送前对请求参数进行新增或修改
    request: (config) => config, // 注意:一定要返回请求参数对象,
    // 默认响应拦截器:对服务器返回的数据进行处理
    response: (response) => response
  }
  // 定义数组队列
  // 初始值需要是一个空数组,用存储请求队列,存储请求标识
  queue = []
  // 用于创建和初始化类的属性
  constructor(params = {}) {
    // 将传递的参数覆盖默认参数
    this.defaults = Object.assign({}, this.defaults, params)
  }
  // 请求方法
  // request方法接收一个对象类型的参数,属性值和 wx.request 方法调用传递的参数保持一致
  request(options) {
    // 如果有新的请求,就清除上一次的定时器
    this.timeId && clearTimeout(this.timeId)
    // 注意:需要先将基础路径和传入的url进行合并
    options.url = this.defaults.baseURL + options.url
    // 合并请求参数
    options = { ...this.defaults, ...options }
    // 在请求发送之前,开启加载效果
    if (options.isLoading && options.method != 'UPLOAD') {
      // wx.upload自带加载效果
      // 判断 queue 队列是否为空,如果为空显示loading,如果不为空则不显示loading
      this.queue.length === 0 &&
        wx.showLoading({
          title: '加载中...'
        })
      // 然后向queue数组队列中添加请求标识
      this.queue.push('request')
    }
    // 在请求发送之前调用请求拦截器,对请求参数进行新增,修改
    options = this.interceptors.request(options)
    // 需要使用 Promise 封装 wx.request 处理异步请求
    return new Promise((resolve, reject) => {
      if (options.method === 'UPLOAD') {
        wx.uploadFile({
          ...options,
          success: (res) => {
            // 1.对服务器返回的JSON字符串转成对象
            res.data = JSON.parse(res.data)
            // 2.合并参数传递给响应拦截器
            const mergeRes = Object.assign({}, res, {
              config: options,
              isSuccess: true
            })
            // 3.将响应拦截器返回的数据返回
            resolve(this.interceptors.response(mergeRes))
          },
          fail: (err) => {
            // 1.合并参数传递给响应拦截器
            const mergeRes = Object.assign({}, err, {
              config: options,
              isSuccess: true
            })
            // 2.将响应拦截器返回的数据返回
            reject(this.interceptors.response(mergeRes))
          }
        })
      } else {
        wx.request({
          ...options, // 将接收到的参数进行展开,作为请求方法的参数
          // 当接口调用成功时会触发success回调函数
          success: (res) => {
            // 不管是成功响应还是失败响应,都需要调用响应拦截器
            // 响应拦截器需要接收服务器响应的数据对数据进行处理,再进行返回
            // 在给响应拦截器传递参数时,需要将请求参数也一起传递,方便进行代码的调试和其他逻辑处理,需要进行参数合并
            // isSuccess属性用于判断响应是否成功
            // 调用响应拦截器
            res = this.interceptors.response(
              Object.assign({}, res, { config: options, isSuccess: true })
            )
            resolve(res) // 返回请求成功的数据
          },
          // 当接口调用失败时会触发fail回调函数(出现网络错误)
          fail: (err) => {
            // 调用响应拦截器
            err = this.interceptors.response(
              Object.assign({}, err, { config: options, isSuccess: false })
            )
            reject(err) // 返回错误信息
          },
          // 无论接口调用成功或失败都会执行
          complete: () => {
            if (options.isLoading) {
              // 每请求完成一次就删除一个标识
              this.queue.pop()
              this.queue.length === 0 && this.queue.push('request')
              // 等100ms若没有新的请求发起则清空quene关闭加载效果,此时若有新的请求发起queue不为空则不会出现新的加载效果
              this.timeId = setTimeout(() => {
                // 若在100ms内无新的请求进来则关闭加载效果
                this.queue.pop()
                // 判断queue队列是否为空,当queue为空时关闭加载效果
                this.queue.length === 0 && wx.hideLoading()
                clearTimeout(this.timeId)
              }, 100)
            }
          }
        })
      }
    })
  }
  // 封装GET实例方法
  get(url, data = {}, config = {}) {
    // url:请求地址 data:请求数据 config:其他配置项
    // 本质还是调用了request方法,只是将请求方法固定为GET,将调用request方法的返回值通过return返回
    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))
  }
  // 封装DELETE实例方法
  delete(url, data = {}, config = {}) {
    return this.request(Object.assign({ url, data, method: 'DELETE' }, config))
  }
  // 封装PUT实例方法
  put(url, data = {}, config = {}) {
    return this.request(Object.assign({ url, data, method: 'PUT' }, config))
  }
  // 封装ALL实例方法用于处理并发请求
  all(promise) {
    return Promise.all(promise)
  }
  // upload实例方法,用来对wx.uploadFile进行封装
  upload(url, filePath, name = 'file', config = {}) {
    return this.request(
      Object.assign({ url, filePath, name, method: 'UPLOAD' }, config)
    )
  }
}

实例化二次封装代码:

http.js

js
import WxRequest from './request'
import { getStorage, clearStorage } from './storage'
import { toast, modal } from './extendApi'
// 实例化请求对象
const instance = new WxRequest({
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api',
  timeout: 15000
  isLoading: false // 关闭默认的Loading效果,需要使用时在调用接口时传递{isLoading:true}
})
// 自定义请求拦截器 会覆盖默认的请求拦截器
instance.interceptors.request = (config) => {
  // // 开启加载效果
  // wx.showLoading({
  //   title: '加载中...'
  // })
  // 在请求头中携带token
  config.header['token'] = getStorage('token')
  return config
}
// 自定义响应拦截器
instance.interceptors.response = async (response) => {
  // // 关闭加载效果
  // wx.hideLoading()
  // 从服务器返回的数据中获取isSuccess(服务器是否返回数据)和statusCode(状态码)
  const { isSuccess, data } = response
  if (isSuccess) {
    // 服务器响应成功
    // 对响应状态码进行判断
    switch (data.code) {
      case 200: // 成功返回数据
        return data // 简化返回数据
      case 208: // token 失效 需要用户重新登录
        const isConfirm = await modal('提示', '身份验证过期,请重新登录', false) // 返回用户是否点击登录
        // 用户点击登录后清除本地存储的信息并跳转到登录页面
        if (isConfirm) {
          clearStorage()
          wx.navigateTo({
            url: '/pages/login/login'
          })
        }
        // 将Promise设置为失败状态并返回错误信息
        return Promise.reject(response)
      default:
        toast('小程序出现异常请重启小程序')
        // 将Promise设置为失败状态并返回错误信息
        return Promise.reject(response)
    }
  } else {
    // 服务器响应失败
    toast('网络异常', 'error')
    return response
  }
}
// 暴露二次封装的请求示例对象
export default instance

使用封装好的请求对象发送请求:

test.js

js
// 导入封装好的请求实例
import instance from '../../utils/http'
Page({
  data: {
    avatarUrl: '/assets/images/avatar.png'
  },
  async handler() {
    let result = await instance.get('/index/findBanner')
    console.log(result)
    // 关闭默认的加载样式
    let result1 = await instance.get('/index/findBanner', null, {
      isLoading: false
    })
    // 开启默认加载样式
    let result2 = await instance.get('/index/findBanner', null, {
      isLoading: true
    })
  },
  // 测试并发请求
  async allHandler() {
    // 采用封装好的请求请求方法同时发起多个请求
    const res = await instance.all([
      instance.get('/index/findBanner'),
      instance.get('/index/findCategory1'),
      instance.get('/index/findBanner'),
      instance.get('/index/findCategory1'),
      instance.get('/index/findBanner'),
      instance.get('/index/findCategory1'),
      instance.get('/index/findBanner')
    ])
    console.log(res)
  },
  // 上传头像的事件回调
  async chooseavatar(event) {
    // 获取临时头像路径
    const { avatarUrl } = event.detail
    // 调用封装好的uploadFile方法,将临时头像路径上传到服务器
    const result = await instance.upload('/fileUpload', avatarUrl, 'file')
    // 将服务器返回的持久头像路径进行存储
    this.setData({
      avatarUrl: result.data
    })
  }
})

14. 请求封装-使用 npm 包发送请求

思路分析:

封装的网络请求模块已经发布到了 npm ,可以先使用 npm 包实现功能。

shell
npm install mina-request

📌 构建 npm:

安装包后,需要在微信开发者工具中进行 npm 构建,点击 工具 ➡️ 构建 npm

其余步骤参考文档进行开发即可:

mina-request 地址 接口地址

对npm包提供的请求方法进行二次封装

utils/http.js

js
// 导入包提供的类
import WxRequest from 'mina-request'
import { getStorage, clearStorage } from './storage'
import { toast, modal } from './extendApi'
// 实例化请求对象
const instance = new WxRequest({
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api', // 基础路径
  timeout: 5000, // 超时时长
  isLoading: false // 关闭默认的加载效果 在发送请求时可以传入{isLoading: true}配置项开启默认加载效果
})
// 设置请求拦截器
instance.interceptors.request = (config) => {
  // 在请求头中添加token
  const token = getStorage('token')
  if (token) config.header['token'] = token
  return config
}
// 设置响应拦截器
instance.interceptors.response = async (response) => {
  // wx.request 会对服务器返回数据进行包装response.data才是真正返回的数据
  // 获取响应数据中的 isSuccess(请求是否发送成功)和 data(服务器真正返回的数据)
  const { isSuccess, data } = response
  if (isSuccess) {
    // 根据响应码判断请求返回的状态
    switch (data.code) {
      // code:200 请求成功
      case 200:
        return data // 请求成功返回简化后的数据
      // code:208 token失效
      case 208:
        // 提示用户重新登录
        const isConfirm = await modal('提示', '登录过期请重新登录', false)
        // 用户点击确定
        if (isConfirm) {
          // 清空本地存储
          clearStorage()
          // 跳转到登录页面
          wx.navigateTo({
            url: '/pages/login/login'
          })
          // 返回失败的Promise终止状态
          return Promise.reject(response)
        }
      default:
        toast('小程序出现异常,请联系客服或稍后重试')
        // 返回失败的Promise终止状态
        return Promise.reject(response)
    }
  } else {
    toast('网络出现异常', 'error')
    // 返回失败的Promise终止状态
    return Promise.reject(response)
  }
}
// 将请求示例进行暴露
export default instance

15. 环境变量-小程序设置环境变量

知识点:

在实际开发中,不同的开发环境,调用的接口地址是不一样的。

例如:开发环境需要调用开发版的接口地址,生产环境需要调用正式版的接口地址

这时候,我们就可以使用小程序提供了 wx.getAccountInfoSync() 接口,用来获取当前账号信息,在账号信息中包含着 小程序 当前环境版本。

环境版本合法值
开发版develop
体验版trial
正式版release

落地代码:

js
// 获取当前帐号信息
const accountInfo = wx.getAccountInfoSync()
// 获取小程序项目的 appId
console.log(accountInfo.miniProgram.appId) 
// 获取小程序 当前环境版本
console.log(accountInfo.miniProgram.envVersion) // develop

根据环境的不同,我们给 env 变量设置不同的请求基准路径 baseURL 然后将 env环境变量导出

utils/env.js

js
// 用于配置当前小程序项目的环境变量
// 获取当前小程序账号信息
const { miniProgram } = wx.getAccountInfoSync()
// 获取小程序版本
const { envVersion } = miniProgram
let env = {
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api'
}
switch (envVersion) {
  // 开发环境的baseURL
  case 'develop':
    env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
    break
  // 正式版本的baseURL
  case 'release':
    env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
    break
  // 体验版的baseURL
  case 'trial':
    env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
    break
  default:
    break

// 将环境变量进行暴露
export default env

utils/http.js

js
// 导入包提供的类
import WxRequest from 'mina-request'
import { getStorage, clearStorage } from './storage'
import { toast, modal } from './extendApi'
+ import env from './env'
// 实例化请求对象
const instance = new WxRequest({
+  baseURL: env.baseURL, // 基础路径
  timeout: 5000, // 超时时长
  isLoading: false // 关闭默认的加载效果 在发送请求时可以传入{isLoading: true}配置项开启默认加载效果
})
........

16. 接口调用方式说明

思路分析:

在开发中,我们会将所有的网络请求方法放置在 api 目录下统一管理,然后按照模块功能来划分成对应的文件,在文件中将接口封装成一个个方法单独导出,例如:

api/index.js

js
// 导入封装好的实例方法
import http from '../utils/http'

// 获取首页轮播图的接口方法
export const reqSwiperData = () => http.get('/index/findBanner')

这样做的有以下几点好处:

  1. 易于维护:一个文件就是一个模块,一个方法就是一个功能,清晰明了,查找方便
  2. 便于复用:哪里使用,哪里导入,可以在任何一个业务组件中导入需要的方法
  3. 团队合作:分工合作

调用接口方法

test.js

js
// 导入接口 API
import { reqSwiperData } from '../../api/index'
Page({
  // 获取首页数据
  async getHomeList() {
    // 获取轮播图数据
    const res = await reqSwiperData()
    console.log(res)
  }
})

六、项目首页

01. 获取首页数据

思路分析:

我们先来分析下在项目的首页我们需要完成的一些功能

  1. 轮播图区域
  2. 商品导航区域
  3. 活动宣传区域
  4. 猜你喜欢区域
  5. 人气推荐区域

在实现这些功能之前,我们需要先获取数据,在获取数据以后,然后进行页面的渲染,同时完成进行功能的开发。

因为需要同时获取 5 个接口的数据,所以我们使用并发请求来实现。这样能够提升页面的渲染速度。

实现步骤:

  1. 封装接口请求函数,可以一个个封装,也可以直接使用 all 方法进行封装
  2. 在页面 .js 文件中导入封装的接口 API 函数
  3. onLoad 钩子函数中调用方法,获取首页轮播图数据

落地代码:

➡️ api/index.js: 准备接口 API

js
// 导入封装好的实例方法
import http from '../utils/http'

// 通过并发请求一次获取首页轮播图、一级分类、活动宣传、猜你喜欢、人气推荐的全部数据
export const reqIndexData = () =>
  // 使用封装的all方法进行并发请求
  http.all(
    http.get('/index/findBanner'),
    http.get('/index/findCategory1'),
    http.get('/index/advertisement'),
    http.get('/index/findListGoods'),
    http.get('/index/findRecommendGoods')
  )

➡️ page/index/index.js

js
import { reqIndexData } from '../../api/index'
Page({
  data: {
    // 轮播图数据
    bannerList: [],
    // 商品一级分类数据
    categoryList: [],
    // 活动宣传区域数据
    activeList: [],
    // 人气推荐区域数据
    hotList: [],
    // 猜你喜欢区域的数据
    guessList: []
  },
  // 用于获取首页数据
  async getIndexData() {
    const result = await reqIndexData()
    // reqIndexData 内部使用的是Promise.all 返回一个数组,按照接口调用顺序返回
    // 存储接口返回的数据
    this.setData({
      bannerList: result[0].data,
      categoryList: result[1].data,
      activeList: result[2].data,
      guessList: result[3].data,
      hotList: result[4].data
    })
  },
  // 页面加载完毕时调用
  onLoad() {
    // 调用获取首页数据的方法
    this.getIndexData()
  }
})

02. 分析轮播图区域并渲染

分析轮播图结构:

轮播图区域采用组件化方式开发,我们在 index 目录下新建 banner 文件夹,里面存放轮播图组件。

index/index.json 文件中导入组件,然后将组件当成标签进行使用

json
{
  "usingComponents": {
    "banner": "./banner/banner"
  }
}
html
<!-- 轮播图区域 -->
<banner />
  1. swiperswiper-itemnavigatorimage 组件实现页面结构的搭建
  2. block 渲染数组,实现列表渲染
  3. 使用 flex 布局实现了页面样式的绘制

另外需要注意的是:轮播图面板指示点不支持自定义,所以只能页面结构的方式,实现轮播图的面板指示点功能

index/banner/banner.js

js
Component({
  // 接收父组件传递的数据
  properties: {
    // 轮播图数据
    bannerList: {
      type: Array,
      value: []
    }
  },
  data: {},
  methods: {}
})

➡️ page/index/index.js

html
<!-- 轮播图区域 -->
+ <banner bannerList="{{ bannerList }}" />  // 向子组件传递获取到的轮播图的数据

index/banner/banner.wxml

html
<!-- 轮播图 -->
<view class="swiper-box">
  <!-- swiper滑块视图容器,用来绘制轮播图 -->
  <swiper
    autoplay
    class="swiper"
    interval="2000"
    duration="1000"
    bindchange="getSwiperIndex"
    circular
  >
   <!-- 使用 block 标签实现通过数组进行列表渲染 -->
	<block wx:for="{{ bannerList }}" wx:key="index">
  	<!-- swiper-item 单个滑块视图容器 -->
 	 <swiper-item class="swiper-item">
  	  <!-- 通过 navigator 组件跳转的链接 -->
  	  <navigator
          class="navigator"
+    	  url="/pages/goods/detail/detail?goodsId={{item.id}}"
   	 >
+    	  <image class="img" src="" data-missing="{{ item.imageUrl }}"></image>
   	 </navigator>
 	 </swiper-item>
	</block>
  </swiper>

  <!-- 轮播图的面板指示点,因为面板指示点不支持自定义,所以我们只能通过自定义结构的方式 -->
  <view class="indicator">
    <!-- active 类名:当前被激活的面板指示点颜色 -->
    <!-- rectangle 类名:默认的面板指示点颜色 -->
    <text
      wx:for="{{bannerList.length}}"
      wx:key="id"
      class="{{ 'active rectangle' }}"
    ></text>
  </view>
</view>

03. 实现轮播图和指示点的联动

思路分析:

当轮播图进行切换时,面板指示点也要跟随着进行切换,如上图。

轮播图和指示点进行联动,当切换到第二张轮播图时,第二个面板指示点高亮,如果想实现这种一一对应的关系,需要借助索引

首先在 data 中初始化状态 activeIndex 默认为 0,代表第一个高亮,也是用来接收切换后的轮播图索引

然后使用 swiper 组件的 change 事件,监听轮播图是否发生改变,如果改变,则获取到轮播图的索引,赋值到data中,

通过 activeIndex 对小圆点进行动态的渲染

实现思路:

  1. data 中初始化状态 activeIndex 默认为 0
  2. swiper 绑定 bindchange 事件,监听轮播图是否切换,将切换后轮播图的索引赋值给activeIndex
  3. 利用 activeIndex 对小圆点进行动态的渲染(使用动态类名)

落地代码:

➡️ pages/index/banner/banner.js

js
import env from '../../../utils/env'

Component({
  // 接收父组件传递的数据
  properties: {
    // 轮播图数据
    bannerList: {
      type: Array,
      value: []
    }
  },
  data: {
+    // 被激活的轮播图索引,默认是 0
+    activeIndex: 0
  },
+  methods: {
+    // 获取被激活的轮播图索引
+    getSwiperIndex(event) {
+      this.setData({
+        activeIndex: event.detail.current
+      })
+    }
+  }
})

➡️ pages/index/banner/banner.wxml

html
<!-- 轮播图 -->
<view class="swiper-box">
  <!-- swiper 滑块视图容器,用来绘制轮播图 -->
  <swiper
    autoplay
    circular
    class="swiper"
    interval="2000"
    duration="1000"
+   bindchange="getSwiperIndex"
  >
    <!-- 通过 block 标签对 轮播图数据 进行渲染 -->
    <block wx:for="{{ bannerList }}" wx:key="index">
      <!-- coding... -->
    </block>
  </swiper>

  <!-- 轮播图的面板指示点,因为面板指示点不支持自定义,所以我们只能通过自定义结构的方式 -->
  <view class="indicator">
    <!-- active 类名:当前被激活的面板指示点颜色 -->
    <!-- rectangle 类名:默认的面板指示点颜色 -->
    <!-- 为自定义指示器动态绑定类名 -->
    <text
      wx:for="{{bannerList.length}}"
      wx:key="id"
+     class="{{  index === activeIndex ? 'active rectangle' : 'rectangle' }}"
    ></text>
  </view>
</view>

04. 分析商品导航区域并渲染

商品导航结构分析

商品导航区域采用组件化方式开发,我们在 index 目录下新建 entrance 文件夹,里面存放导航分类组件。

  1. 采用 viewnavigatorimagetext 组件实现了进行页面结构的搭建
  2. 使用 flex 布局实现了页面样式的绘制

➡️ pages/index/entrance/entrance.html

html
<view class="nav-list">
  <!-- 一级分类导航容器 -->
  <view
+     wx:for="{{ cateList }}"
    wx:key="index"
    class="nav-item {{ index >= 5 ? 'small' : '' }}"
  >
    <!-- 导航链接 -->
    <navigator
      class="navigator-nav"
+       url="/pages/goods/list/list?category1Id={{item.id}}"
    >
+       <image class="nav-img" src="" data-missing="{{ item.imageUrl }}" />
+       <text class="nav-text">{{ item.name }}</text>
    </navigator>
  </view>
</view>

index/index.json 文件中导入组件,然后将组件当成标签进行使用

json
{
  "usingComponents": {
    // ...
    "entrance": "./entrance/entrance"
  },
  "navigationBarTitleText": "慕尚花坊"
}

➡️ page/index/index.wxml

html
<!-- 导航分类 -->
<entrance cateList="{{ categoryList }}" />

➡️ pages/index/entrance/entrance.js

js
Component({
  // 组件的属性列表
  properties: {
    cateList: {
      type: Array,
      value: []
    }
  },
 
  // coding...
}

➡️ pages/index/index.html

html
<!-- 广告区域 -->
<view class="adver">
  <view class="adver-left">
+    <navigator url="/pages/goods/list/list?category2Id={{ activeList[0].category2Id }}">
+      <image src="" data-missing="{{ activeList[0].imageUrl }}" mode="widthFix" />
+    </navigator>
  </view>

  <view class="adver-right">
    <view>
+       <navigator url="/pages/goods/list/list?category2Id={{ activeList[1].category2Id }}">
+         <image src="" data-missing="{{ activeList[1].imageUrl }}" mode="widthFix" />
+       </navigator>
    </view>
    <view>
+       <navigator url="/pages/goods/list/list?category2Id={{ activeList[2].category2Id }}">
+         <image src="" data-missing="{{ activeList[2].imageUrl }}" mode="widthFix" />
+       </navigator>
    </view>
  </view>
</view>

05. 分析猜你喜欢+人气推荐并渲染

猜你喜欢和人气推荐区域结构分析

猜你喜欢和人气推荐的布局是一致的,所以将两个模块放到一起实现

商品列表在项目中属于常见的结构,商品列表区域依然采用组件化方式开发,商品列表组件由两个组件进行构成:

  1. goods-list 商品列表组件,包含者商品卡片组件,用来展示商品列表
  2. goods-card 商品卡片组件,承载某个商品的结构,用来展示单个商品

goods-list 商品列表组件包含三个结构:标题(允许用户自定义)、商品列表(商品卡片)、查看更多

只不过在渲染数据的时候需要将数据传递给 goods-list 商品列表组件, goods-list组件内部传递给 goods-card 商品卡片组件,goods-card 组件内部渲染数据。

➡️ page/index/index.wxml:将数据传递给 goods-list 组件

html
<!-- 商品列表 -->
<goods-list title="猜你喜欢" list="{{ guessList }}"></goods-list>
<goods-list title="人气推荐" list="{{ hotList }}"></goods-list>

➡️ components/goods-list/goods-list.js:接收首页传递的 list 数据

js
Component({
  // 组件的属性列表
  properties: {
    // 列表标题
    title: {
      type: String,
      value: '',
    },
    // 传递的列表数据
    list: {
      type: Array,
      value: []
    }
  }
}

➡️ components/goods-list/goods-list.wxml:遍历 goods-item 组件,并将数据传递给 goods-item

html
<!-- 商品列表组件 -->
+ <view class="goods_container" wx:if="{{ list.length }}">
  
  <!-- 标题 -->
+   <view class="goods_title">{{title}}</view>
  
  <!-- 列表区域 -->
  <view class="goods_card_list">
+     <goods-card wx:for="{{ list }}" wx:key="id" goodItem="{{ item }}"></goods-card>
  </view>
  
  <!-- 查看更多 -->
  <!-- coding -->
</view>

➡️ components/goods-list/goods-item.js:将数据传递给 goods-item 组件

js
Component({
  // 组件的属性列表
  properties: {
    // 每一项商品的数据
    goodItem: {
      type: Object,
      value: {}
    }
  }
}

➡️ components/goods-list/goods-item.wxml:将数据传递给 goods-item 组件

html
<!-- 列表分类卡片 -->
<view class="goods_cart_container">
  <navigator class="navigator_nav" url="/pages/goods/detail/detail?goodsId={{goodItem.id}}">

    <!-- 商品图片 -->
    <image class="good_img" src="" data-missing="{{ goodItem.imageUrl }}" mode="widthFix" />

    <!-- 商品详细信息 -->
    <view class="goods_item_info">
      <!-- 商品名称 -->
      <text class="goods_item_info_name">{{ goodItem.name }}</text>
      <!-- 商品描述 -->
      <text class="goods_item_info_promo">{{ goodItem.floralLanguage }}</text>
      <!-- 商品价格 -->
      <view class="goods_item_info_bottom">
        <view class="goods_item_info_price">
          <text class="text">¥</text>{{ goodItem.price }}
        </view>
        <view class="goods_item_info_origin_price">
          <text class="text">¥</text> {{goodItem.marketPrice}}
        </view>
        <!-- 加入购物车图片 -->
        <view class="goods_item_info_btn">
          <image class="goods_image" src="/static/images/buybtn.png" mode="" />
        </view>
      </view>
    </view>
  </navigator>
</view>

06. 首页骨架屏组件

思路分析:

骨架屏是页面的一个空白版本,开发者会使用 CSS 绘制一些灰色的区块,将页面内容大致勾勒出轮廓。

通常会在页面完全渲染之前,将骨架屏代码进行展示,待数据加载完成后,再替换成真实的内容。

骨架屏的设计旨在优化用户体验。

在进行项目开发时,我们需要手工维护骨架屏的代码,当业务变更时,同样需要对骨架屏代码进行调整。

为了方便开发者进行骨架屏的绘制,开发者工具提供了自动生成骨架屏代码的能力。

使用步骤:

  1. 使用微信开发者工具为当前正在预览的页面生成骨架屏代码,工具入口位于模拟器面板右下角三点处

  2. 点击生成骨架屏,将有弹窗提示是否允许插入骨架屏代码。

    确定后将在当前页面同级目录下生成 page.skeleton.wxmlpage.skeleton.wxss 两个文件,分别为骨架屏代码的模板和样式。

  3. 将生成的骨架屏文件存放在当前页面的 skeleton文件夹下

  4. 在页面 index/index.wxml 中引入并使用骨架屏通过 loading属性控制骨架屏的显示与隐藏

    html
    <!-- 引入骨架屏 -->
    <import src="" data-missing="index.skeleton.wxml" />
    <!-- 使用骨架屏 -->
    <template is="skeleton" wx:if="{{loading}}" /> 
    <view wx:else class="index-container">
    	......
    </view>
  5. 在页面样式文件 index/index.scss 中引入骨架屏的样式

    css
    // 引入骨架屏的样式文件
    @import './skeleton/index.skeleton.scss';
  6. data 中声明 loading 的状态为 true 当数据请求完以后将 loading 的状态为 false

    js
    import { reqIndexData } from '../../api/index'
    Page({
      data: {
        // 轮播图数据
        bannerList: [],
        // 商品一级分类数据
        categoryList: [],
        // 活动宣传区域数据
        activeList: [],
        // 人气推荐区域数据
        hotList: [],
        // 猜你喜欢区域的数据
        guessList: [],
    +    // 控制骨架屏的显示与隐藏
    +    loading: true
      },
      // 用于获取首页数据
      async getIndexData() {
        const result = await reqIndexData()
        // reqIndexData 内部使用的是Promise.all 返回一个数组,按照接口调用顺序返回
        // 存储接口返回的数据
        this.setData({
          bannerList: result[0].data,
          categoryList: result[1].data,
          activeList: result[2].data,
          guessList: result[3].data,
          hotList: result[4].data,
    +      // 关闭骨架屏
    +      loading: false
        })
      },
      // 页面加载完毕时调用
      onLoad() {
        // 调用获取首页数据的方法
        this.getIndexData()
      }
    })
  7. 查看页面效果

注意:

​ 需要将骨架屏中的行内样式提取为 class 样式

七、商品分类

商品分类是一个单独的 tabBar 页面,当点击分类 tabBar 的时候就能够进入商品分类页面。

在商品分类页面我们主要实现三个功能:

  1. 一级分类的渲染
  2. 一级分类的切换
  3. 二级分类的渲染

01. 获取商品分类数据

思路分析:

需要调用接口获取分类的数据接口地址

在熟悉了接口文档以后,根据接口文档封装接口 API 函数,然后在页面调用 API 函数获取分类的数据,在获取到数据以后,使用后端返回的数据对一级、二级分类进行渲染。

实现步骤:

  1. 在项目根目录下 api 目录下新建 category.js 文件,用来管理分类页面接口请求
  2. 在该文件中导入封装的网络请求模块,根据接口文档,创建获取分类数据的 API 函数 reqCategoryData
  3. /pages/category/category.js 中导入封装好的获取分类数据的 API 函数
  4. 页面数据需要在页面加载的时候进行调用,因此需要在 onLoad 钩子函数中调用 reqCategoryData 方法
  5. 在获取到数据以后,使用后端返回的数据对页面进行渲染

落地代码:

➡️ api/category.js

js
// 导入封装的网络请求模块实例
import http from '../utils/http'

// 获取商品分类的接口方法
export const reqCategoryData = () => http.get('/index/findCategoryTree')

➡️ page/category/category.js

js
import { reqCategoryData } from '../../api/category'
Page({
  data: {
    // 商品分类列表数据
    categoryList: []
  },
  // 获取商品分类的数据
  async getCategory() {
    const result = await reqCategoryData()
    this.setData({
      categoryList: result.data
    })
  },
  // 页面加载完毕
  onLoad() {
    this.getCategory()
  }
})

02. 渲染一级分类并实现切换功能

思路分析:

当进入分类页面的时候,第一个一级分类默认是高亮选中的状态

当点击任意的一级分类以后,对应的一级分类需要高亮选中 (active 类名),其余的一级分类取消高亮选中

实现思路:

我们可以先初始化数据 activeIndex, 代表被激活的那一项的索引,默认值为 0

给一级分类绑定点击事件,当点击某个一级分类时,将对应分类的索引赋值给 activeIndex

这时候在模板中,可以拿遍历后一级分类的对应的索引 indexactiveIndex 来进行对比

如果值一致,说明是当前分类被激活,添加 active 类名即可

实现步骤:

  1. data 中初始化变量 activeIndex,初始值为 0
  2. 给一级分类绑定点击事件 updateActive ,同时自定义一个自定义属性 data-index 值为索引 index
  3. 在事件处理程序updateActive中,将 index 赋值给 activeIndex
  4. 在模板中进行 indexactiveIndex 的对比

落地代码:

➡️ page/category/category.html

html
<!-- 左侧的滚动视图区域 -->
<scroll-view class="left-view" scroll-y>
  <view
+    wx:for="{{ categoryList }}"
+    wx:key="id"
+    bindtap="updateActive"
+    data-index="{{ index }}"
+    class="left-view-item {{ activeIndex === index ? 'active' : '' }}"
   >
    {{ item.name }}
  </view>
</scroll-view>

➡️ page/category/category.js

js
Page({
  data: {
    categoryList: [], // 分类数据列表
+     activeIndex: 0, // 点击高亮导航id
  },
+   // 导航分类点击事件
+   updateActive(e) {
+     this.setData({
+       activeIndex: e.currentTarget.dataset.index
+     })
+   }
  // coding...
}

03. 获取 & 渲染二级分类数据

思路分析:

在一级分类下存在 children 字段, children 字段中的数据则是一级分类对应的二级分类的数据,当我们访问一级分类时,只需要将一级分类对应的二级分类拿出来进行渲染即可。

利用 activeIndex 获取对应二级分类数据即可,当点击 鲜花玫瑰 时,该一级分类索引为 1,对应着索引为 1 的数据,即可取出对应 children 中的数据

实现步骤:

  1. 在模板中利用 activeIndex 获取到对应二级分类的数据
  2. 对页面进行渲染即可

落地代码:

html
<!-- 右侧的滚动视图区域 -->
<scroll-view class="right-view" scroll-y enable-flex="true">
  <view class="right-view-item" wx:for="{{ category[activeIndex].children }}" wx:key="id">
    <navigator class="navigator" url="/pages/goods/list/list?category2Id={{item.id}}">
      <image class="" src="" data-missing="{{ item.imageUrl }}"></image>
      <text class='goods_item_name'>{{ item.name }}</text>
    </navigator>
  </view>
</scroll-view>

八、用户管理

01. 用户登录-什么是 token

Token 是服务器生成的一串字符串,用作客户端发起请求的一个身份令牌。当第一次登录成功后,服务器生成一个 Token 便将此 Token 返回给客户端,客户端在接收到 Token 以后,会使用某种方式将 Token 保存到本地。以后客户端发起请求,只需要在请求头上带上这个 Token ,服务器通过验证 Token 来确认用户的身份,而无需再次带上用户名和密码。

**Token的具体流程 **

  1. 客户端向服务器发起登录请求,服务端验证用户名与密码
  2. 验证成功后,服务端会签发一个 Token,并将 Token 发送到客户端
  3. 客户端收到 token 以后,将其存储起来,比如放在 localStoragesessionStorage
  4. 客户端每次向服务器请求资源的时候需要带着服务端签发的 Token,服务端收到请求,然后去验证客户端请求里面带着的 Token ,如果验证成功,就向客户端返回请求的数据

02. 用户登录-小程序登录流程介绍

业务介绍:

传统的登录功能,需要用户先注册,注册完成以后,使用注册的账号、密码进行登录。

小程序的登录操作则比较简单,小程序可以通过微信提供的登录能力,便捷地获取微信提供的用户身份标识进行登录。

免去了注册和输入账号密码的步骤,从而提高了用户体验。

小程序登录图示:

登录流程说明:

  1. 用户访问小程序,点击 [登录] ,调用 wx.login() 方法获取 临时登录凭证code

    临时登录凭证 code,就像是一个会过期的临时身份证一样,有效时间仅为 5分钟

  2. 使用 wx.request() 方法将 临时登录凭证code 传递给开发者服务器,方便后续可以换取微信用户身份 id

  3. 开发者的后台接收 临时登录凭证code,同时在微信公众后台拿到 AppIdAppSecret ,向微信服务器发送请求, 请求参数合法的话,微信服务器会给开发者后台返回 openid(微信用户的唯一标识) 以及 session_key(会话密钥) 等

    openid 是微信用户的唯一标识,也就是微信用户身份 id,可以用这个 id 来区分不同的微信用户

    session_key 则是微信服务器给开发者服务器颁发的身份凭证,

    开发者可以用session_key请求微信服务器其他接口来获取一些其他信息

  4. 开发者后台在接收到微信服务器返回的数据以后,会执行一些业务逻辑的处理,例如:将用户标识和其他信息进行加密处理,生成自定义登录态,这个登录态可以理解为就是 Token ,然后让 Tokenopenidsession_key 进行关联

  5. 开发者后台处理好逻辑后,会将 自定义登录态 Token 返回给微信小程序客户端,客户端收到 token 以后,将其存储起来,比如放在 localStorage 中。

  6. 客户端每次向开发者后台发送请求的时候,需要携带自定义登录态 Token ,开发者后台收到请求后,对 Token 进行验证识别用户身份,同时拿自定义登录态 Token 查询 openidsession_key,从而获取用户请求的数据,进行返回。

03. 用户登录-实现小程序登录功能

思路分析:

当用户没有登录的时候,需要点击个人中心的头像,跳转到登录页面进行登录。在登录成功以后,需要再次返回到个人中心页面

在登录页面我们使用了 Vant 提供的两个组件来进行页面结构的绘制

  1. empty 组件:空状态时的占位提示
  2. button 组件:按钮组件

给登录按钮绑定点击事件,在事件处理程序中,调用 wx.login 获取 临时登录凭证code

然后调用后端接口,将 临时登录凭证code 传递给后端

根据接口文档封装接口 API 函数,当点击授权登录按钮的时候调用 API 函数,在获取到 token 以后,将 token 存储到本地,然后跳转到登录之前的页面。

实现步骤:

  1. /api/user.js 文件中根据接口文档,创建登录的 API 函数 login
  2. 给登录按钮绑定点击事件,对应 login 回调函数
  3. login 回调函数中调用 wx.login() 方法,获取临时登录凭证code
  4. /pages/login/login.js 中导入封装好的 API 函数,传入 临时登录凭证code 然后调用
  5. 在登录成功以后将 token 存储到本地

落地代码:

➡️ /api/user.js

js
import http from '../utils/http'
// 上传用户code获取token的接口方法
export const reqLogin = (code) => http.get(`/weixin/wxLogin/${code}`)

➡️ /pages/login/login.js

js
import { toast } from '../../utils/extendApi'
import { userStore } from '../../stores/userStore'
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
// 需要使用ComponentWithStore构建页面
ComponentWithStore({
  // 获取user仓库中的数据和方法
  storeBindings: {
    store: userStore,
    actions: ['getToken', 'getUserInfo'] // 获取仓库中方法
  },
  methods: {
    // 用于授权登录
    login() {
      // 1.使用wx.login来获取用户的临时登录凭证 code
      wx.login({
        success: async (res) => {
          if (res.code) {
            // 2.调用仓库中的方法获取token并存储
            await this.getToken(res.code)
            // 3.等待token获取成功后通知仓库获取用户信息并存储
            await this.getUserInfo()
            // 4.获取用户信息完毕后返回上一级页面
            wx.navigateBack()
          } else {
            toast('授权失败,请重试')
          }
        }
      })
    }
  }
})

04. 用户登录-token 存储到 Store

思路分析:

我们已经将token存储到了本地,但是将 Token 直接存储到本地不方便对数据进行操作,要先从本地存储取出,然后在使用,关键的一点,存储到本地的数据不是响应式的,当本地存储里面的内容发生改变,页面不会发生改变。这时候我们就需要将 token 也存储到 Store

Mobx允许开发人员在应用程序中统一管理所有组件之间的公共数据。通过使用 Mobx,开发人员可以轻松地将 token 存储到全局状态中,并实现在整个应用程序中的共享。并且,存储到Mobx中的数据是响应式的,数据发生了变化,使用的地方也会发生变化

然后使用 Component 构造页面,并导入ComponentWithStore 方法,并配置 storeBindings 方法让页面和 Store 对象关联

实现步骤:

  1. 安装Mobx两个包,在安装好包以后,对包进行构建,点击 构建 npm
  2. 在项目的根目录下创建 store 文件夹,然后在该文件夹下新建 userstore.js
  3. 导入核心的observable action 方法,创建Store,同时声明数据和方法
  4. 在登录页面,导入ComponentWithStore 方法,并配置 storeBindings 方法让页面和 Store 对象关联

落地代码:

安装依赖,安装完成后构建 npm

shell
npm i mobx-miniprogram mobx-miniprogram-bindings

➡️ /store/userStore.js

js
// observable用于创建仓库,actions函数用于定义方法
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'
import { reqLogin } from '../api/user'
import { setStorage } from '../utils/storage'
export const userStore = observable({
  // 定义响应式数据
  // 在仓库中存储token
  token: getStorage('token') || '',
  // 定义方法
  // 获取token并存储
  getToken: action(async function (code) {
    // 将code传递给开发者服务器获取token
    const result = await reqLogin(code)
    // 本地存储token
    setStorage('token', result.data.token)
    // 将token存储到用户仓库中
    this.token = result.data.token
  })
})

➡️ /pages/login/login.js

js
import { toast } from '../../utils/extendApi'
import { userStore } from '../../stores/userStore'
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
// 需要使用ComponentWithStore构建页面
ComponentWithStore({
  // 获取user仓库中的数据和方法
  storeBindings: {
    store: userStore,
    fields: ['token'], // 获取仓库中的属性
    actions: ['getToken'] // 获取仓库中方法
  },
  methods: {
    // 用于授权登录
    login() {
      // 1.使用wx.login来获取用户的临时登录凭证 code
      wx.login({
        success: (res) => {
          if (res.code) {
            // 2.调用仓库中的方法获取token并存储
            this.getToken(res.code)
          } else {
            toast('授权失败,请重试')
          }
        }
      })
    }
  }
})

05. 用户信息-用户信息存储到 Store

思路分析:

我们需要调用接口获取用户的信息,在获取到数据以后,我们需要存储用户信息数据到本地,

用户信息可能会在多个地方使用到,为了方便对用户信息的获取和使用,我们依然将用户信息存储到store

我们首先在 store/userStore.js 中新增userInfo可观测字段,同时创建获取用户信息的action方法

获取用户信息的接口需要使用 token,所以我们需要在登录成功以后,调用获取用户信息的接口

登录成功以后,调用action方法,将用户信息存储到 Store和本地中

实现步骤:

  1. store/userstore.js 中新增userInfo字段,同时创建修改的action方法

  2. login.js 中使用映射 userInfo 数据和 setUserInfo 方法

  3. /api/user.js 文件中根据接口文档,创建获取用户信息的 API 函数 reqUserInfo

  4. /pages/login/login.js 中导入封装好的获取商品列表的 API 函数

  5. 创建 getUserInfo 方法,在 getUserInfo 方法中调用接口 API 函数 reqUserInfo

  6. 在登录成功以后,调用getUserInfo 方法获取用户,然后将用户信息存到本地以及 Store

落地代码:

➡️ /api/user.js

js
import http from '../utils/http'
// 上传用户code获取token的接口方法
export const reqLogin = (code) => http.get(`/weixin/wxLogin/${code}`)
+ // 获取用户信息的接口方法
+ export const reqUserInfo = () => http.get('/weixin/getuserInfo')

➡️ /store/userstore.js

js
// observable用于创建仓库,actions函数用于定义方法
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'
import { reqLogin, reqUserInfo } from '../api/user'
import { setStorage } from '../utils/storage'
export const userStore = observable({
  // 定义响应式数据
  // 存储token
  token: getStorage('token') || '',
+  // 存储用户信息
+  userInfo: getStorage('userInfo') || {},
  // 定义方法
  // 获取token并存储
  getToken: action(async function (code) {
    // 将code传递给开发者服务器获取token
    const result = await reqLogin(code)
    // 本地存储token
    setStorage('token', result.data.token)
    // 将token存储到用户仓库中
    this.token = result.data.token
  }),
+  // 获取用户信息并存储
+  getUserInfo: action(async function () {
+    const result = await reqUserInfo()
+    // 将用户信息存储到本地
+    setStorage('userInfo', result.data)
+    // 将用户信息存储到仓库中
+    this.userInfo = result.data
+  })
})

➡️/pages/login/login.js

js
import { toast } from '../../utils/extendApi'
import { userStore } from '../../stores/userStore'
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
// 需要使用ComponentWithStore构建页面
ComponentWithStore({
  // 获取user仓库中的数据和方法
  storeBindings: {
    store: userStore,
    fields: ['token', 'userInfo'], // 获取仓库中的属性
    actions: ['getToken', 'getUserInfo'] // 获取仓库中方法
  },
  methods: {
    // 用于授权登录
    login() {
      // 1.使用wx.login来获取用户的临时登录凭证 code
      wx.login({
        success: async (res) => {
          if (res.code) {
            // 2.调用仓库中的方法获取token并存储
            await this.getToken(res.code)
+            // 3.等待token获取成功后通知仓库获取用户信息并存储
+            await this.getUserInfo()
+            // 4.获取用户信息完毕后返回上一级页面
+            wx.navigateBack()
          } else {
            toast('授权失败,请重试')
          }
        }
      })
    }
  }
})

06. 用户信息-使用数据渲染用户信息

思路分析:

在获取到数据以后,我们已经将用户信息数据存储到本地和Store

我们需要从 Store 中取出用户信息数据,并渲染到页面上

个人中心页面展示用于展示个人信息

如果用户没有登录的时候,展示没有登录的头像、提示用户登录的文案信息,不展示设置按钮

如果用户已经登录,展示用户的头像和昵称,并且展示设置按钮,方便用户对收货地址、头像、昵称进行更改

实现步骤:

  1. 在个人中心页面导入ComponentWithStore 方法构建页面
  2. 配置 storeBindings 让组件和 Store 建立关联
  3. 渲染页面

落地代码:

➡️/pages/my/my.js

js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '../../stores/userStore'
ComponentWithStore({
  // 获取用户仓库中存储的用户信息
  storeBindings: {
    store: userStore,
    fields: ['userInfo', 'token']
  },
  // 页面的初始数据
  data: {
    // 初始化第二个面板数据
    initpanel: [
      {
        url: '/pages/order/list/list',
        title: '商品订单',
        iconfont: 'icon-dingdan'
      },
      ......
  },
  methods: {
    // 跳转到登录页面
    toLoginPage() {
      wx.navigateTo({
        url: '/pages/login/login'
      })
    }
  }
})

➡️/pages/my/my.wxml

html
<!--pages/info/info.wxml-->
<view class="container">
  <!-- 顶部展示图 -->
  <view class="top-show">
    <image
      mode="widthFix"
      class="top-show-img"
      src="/assets/images/banner.jpg"
    ></image>
  </view>

  <view class="bottom-show">
    <!-- 未登录面板 -->
    <view
+      <!-- 根据仓库中是否有token来判断用户是否登录 -->
+      wx:if="{{!token}}"
      class="user-container section"
      bindtap="toLoginPage"
    >
      <view class="avatar-container">
        <image src="/assets/images/avatar.png"></image>
        <view class="no-login">
          <text class="ellipsis">未登录</text>
          <text>点击授权登录</text>
        </view>
      </view>
    </view>

    <!-- 登录以后的面板 -->
+    <view wx:else class="user-container section">
      <view class="avatar-container">
+        <image src="" data-missing="{{userInfo.headimgurl}}"></image>
        <view class="no-login">
+          <text class="ellipsis">{{userInfo.nickname}}</text>
        </view>
      </view>
      <view class="setting">
        <navigator url="/pages/settings/settings"> 设置 </navigator>
      </view>
    </view>

    <!-- 订单面板 -->
    <view class="order section">
      <view class="order-title-wrap">
        <text class="title">我的订单</text>
        <text class="more">查看更多></text>
      </view>
      <view class="order-content-wrap">
        <view class="order-content-item" wx:for="{{ initpanel }}">
+          <!-- 未登录时点击我的订单跳转到登录页面 -->
+          <navigator url="{{ token?item.url:'/pages/login/login' }}">
            <view class="iconfont {{ item.iconfont }}"></view>
            <text>{{ item.title }}</text>
          </navigator>
        </view>
      </view>
    </view>

    <!-- 关于售前售后服务面板 -->
    <view class="after-scale section">
      <view class="order-title-wrap">
        <text class="title">关于售前售后服务</text>
      </view>
      <view class="after-scale-item">
        <view class="iconfont icon-kefufenxiermaikefu"></view>
        <text>可与小程序客服实时聊天或电话咨询</text>
      </view>
      <view class="after-scale-item">
        <view class="iconfont icon-shijian"></view>
        <text>小程序客服工作时间为: 8:30 ~ 20:30</text>
      </view>
      <view class="after-scale-item">
        <view class="iconfont icon-dizhiguanli"></view>
        <text>鲜花制作完毕情况下暂不支持退款</text>
      </view>
      <view class="after-scale-item">
        <view class="iconfont icon-zhangben"></view>
        <text>鲜花可以提前7-15天预订重大节假日不支持定时配送</text>
      </view>
    </view>

    <!-- 底部面板 -->
    <view class="info-footer"> 尚硅谷技术支持 </view>
  </view>
</view>

07. 分包处理-配置分包以及预下载

思路分析:

随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。

因此我们需要将 更新个人资料收货地址 功能配置成一个分包,

当用户在访问设置页面时,还预先加载 更新个人资料收货地址 所在的分包

在分包后,通过查看代码依赖查看是否分包完成

📌 注意事项

  1. 在配置好商品详情和商品列表的分包以后,需要更改页面中的跳转路径 (路径前加分包路径)!
  2. 可以利用项目全局搜索的功能,进行批量更改

实现步骤:

  1. 在项目的根目录新建目录 modules/settingModule/pages 将设置分包下的页面移动到该目录
  2. app.json 新增 subpackages 进行分包配置,新增 preloadRule 进行分包预下载配置
  3. subpackages 设置分包的 根目录 root 、别名 name 、页面路径 pages
  4. preloadRule 设置预下载。

落地代码

➡️ app.json

json
// 配置分包  
"subPackages": [
    {
      "root": "modules/settingModule", // 分包根目录
      "name": "settingModule",  // 分包名
      "pages": [ // 该分包包含的页面
        "pages/address/add/index",
        "pages/address/list/index",
        "pages/profile/profile"
      ]
    }
  ],
// 配置分包预下载
  "preloadRule": {
    "pages/settings/settings": { // 在设置页面预下载 settingModule 分包
      "network": "all",
      "packages": ["settingModule"]
    }
  }

08. 更新用户信息-渲染用户信息

思路分析:

点击个人中心的设置,然后点击修改个人资料,就可以对用户的头像和昵称进行修改

在这个页面中,我们需要先渲染信息用户,用户信息目前是存储到 Store 中的,因此我们需要先从 Store 中取出用户信息的数据,进行渲染的渲染。

让页面和 Store 数据建立关联,可以使用 mobx-miniprogram-bindings 提供的 BehaviorWithStore 方法

实现步骤:

  1. 新建 behavior.js 文件,从 mobx-miniprogram-bindings 库中导入 BehaviorWithStore 方法
  2. BehaviorWithStore 方法中配置 storeBindings 配置项从 Store 中映射数据和方法
  3. Page 方法中导入创建的 behavior ,然后配置 behavior 属性,并使用导入的 behavior

落地代码:

js
// behavior.js
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '../../../../stores/userStore'
export default BehaviorWithStore({
  storeBindings: {
    store: userStore,
    fields: ['userInfo']
  }
})

➡️ modules/settingModule/pages/profile/profile.js

js
+ import { userBehavior } from './behavior'
Page({
+  behaviors: [userBehavior],
  // 页面的初始数据
  data: {
    isShowPopup: false // 控制更新用户昵称的弹框显示与否
  },
  // 其他代码略...
})

➡️ modules/settingModules/pages/profile/profile.wxml

html
<view class="container">
  <view class="setting-list avatar-container">
    <text>头像</text>
    <view class="avatar">
      <button hover-class="none">
+        <image src="" data-missing="{{ userInfo.headimgurl }}" mode="" />
      </button>
    </view>
  </view>
  <view class="setting-list nickname">
    <text>昵称</text>
+    <text>{{ userInfo.nickname }}</text>
  </view>
  <!-- coding... -->
</view>

09. 更新用户信息-获取头像临时路径

思路分析:

当用户点击头像时,可以对头像进行更新操作,我们使用通过微信提供的头像昵称填写能力快速完善

如果需要使用小程序提供的头像填写能力,需要两步:

  1. button 组件 open-type 的值设置为 chooseAvatar
  2. 当用户选择需要使用的头像之后,可以通过 bindchooseavatar 事件回调获取到头像信息的临时路径

实现步骤:

  1. button 按钮绑定 open-type 属性,值为 chooseAvatar
  2. 用户点击了头像后,在 bindchooseavatar 事件回调获取到头像信息的临时路径

落地代码:

➡️ modules/settingModules/pages/profile/profile.wxml

html
<view class="avatar">
  <button
    class="avatar-btn"
    hover-class="none"
+     open-type="chooseAvatar"
+     bindchooseavatar="chooseAvatar"
  >
    <image src="" data-missing="avatar.png' }}" />
  </button>
</view>

➡️ modules/settingModules/pages/profile/profile.js

js
// pages/profile/profile.js
import { userBehavior } from './behavior'
Page({
  // 注册 behavior
  behaviors: [userBehavior],
  // 页面的初始数据
  data: {
    isShowPopup: false // 控制更新用户昵称的弹框显示与否
  },
+   // 更新用户头像
+   chooseAvatar(event) {
+     // 获取头像的临时路径
+     // 临时路径具有失效时间,需要将临时路径上传到公司的服务器,获取永久的路径
+     // 在获取永久路径以后,需要使用永久路径更新 headimgurl
+     // 用户点击 保存按钮,才算真正的更新了头像和昵称
+     const { avatarUrl } = event.detail
+     this.setData({
+       'userInfo.headimgurl': avatarUrl
+     })
+   },
  // 略....
})

10. 更新用户信息-头像上传到服务器

思路分析:

通过 bindchooseavatar 事件回调获取到头像信息的临时路径。

当临时文件超过一定的容量的时候,小程序就会将临时文件清理掉,也就是说临时文件可能会随时失效,为了解决这个问题,我们需要将获取到头像信息的临时路径上传到自己的服务器。如果需要将本地资源上传到服务器,需要使用到小程序提供的 API 方法: wx.uploadFile ,语法如下:

js
wx.uploadFile({
  url: '开发者服务器地址',
  filePath: '要上传文件资源的路径 (本地路径)',
  name: '文件对应的 key',
  header: 'HTTP 请求 Header',
  // 接口调用成功的回调函数
  success: (res) => {},
  // 接口调用失败的回调函数
  fail: (err) => {}
})

实现步骤:

  1. 在获取到用户的临时头像路径以后,调用 wx.uploadFile() 方法,同时设置好基本的参数,
  2. 在上传成功后,获取到服务器返回的永久地址
  3. 将地址赋值给 data 中的数据

落地代码:

➡️ api/user.js

js
+ // 上传用户头像到服务器的接口方法
+ export const reqUploadFile = (filePath, name) =>
  http.upload('/fileUpload', filePath, name)

➡️ modules/settingModules/pages/profile/profile.js

js
Page({
  // 使用userBehavior
  behaviors: [userBehavior],
  // 页面的初始数据
  data: {
    isShowPopup: false // 控制更新用户昵称的弹框显示与否
  },
  // 获取微信头像的事件回调
  async chooseAvatar(event) {
    // 1.获取头像的临时路径(会失效)
    const { avatarUrl } = event.detail
    // 2.将临时路径上传到服务器获取永久路径
    const result = await reqUploadFile(avatarUrl, 'file')
    if (result.code === 200) {
      // 更新当前组件中头像路径
      this.setData({
        'userInfo.headimgurl': result.data
      })
    }
})

11. 更新用户信息-完成头像更新

思路分析:

当用户点击保存时,就需要实现头像的更新功能,既然需要同步到服务器,依然需要调用接口

根据接口文档封装接口 API 函数,点击保存的时候调用接口函数,然后将最新的用户信息同步到服务器。

在同步到服务器以后,我们需要将用户信息存储到本地同时同步到 Store

实现步骤:

  1. /api/user.js 文件中根据接口文档,创建获取用户信息的 API 函数 reqUpdateUserInfo
  2. 给修改个人资料的保存按钮绑定点击事件,触发 updateUserInfo 回调函数
  3. 在回调函数中调用接口 API 函数 reqUpdateUserInfo ,同时传入用户的信息
  4. 更新用户信息以后,将用户信息存储到本地同时同步到 Store

落地代码:

➡️/api/user.js

js
// 更新用户信息的接口方法
export const updateUserInfo = (userInfo) =>
  http.post('/weixin/updateUser', userInfo)

➡️ modules/settingModule/pages/profile/profile.js

js
// pages/profile/profile.js
import { reqUpdateUserInfo, reqUserInfo } from '../../../../api/user'
Page({
  // coding...
  // 点击保存按钮的事件回调
  async save() {
    // 1.调用接口将用户头像和昵称上传到服务器中
    const result = await updateUserInfo(this.data.userInfo)
    if (result.code == 200) {
      // 2.通知仓库重新获取用户信息并存储
      await this.getUserInfo()
      // 3.跳转到我的页面
      await wx.switchTab({
        url: '/pages/my/my'
      })
      // 4.提示用户信息存储成功
      toast('信息更新成功')
    } else {
      toast('信息更新失败')
    }
  },
  // coding...
}

12. 更新用户信息-更新用户昵称

思路分析:

我们需要实现将用户昵称更改的功能

更新用户昵称的接口和更新用户头像的接口是同一个,因此不需要再次封装,直接复用即可

当点击更新用户昵称时,弹出弹框,当用户将在输入框光标聚焦到输入框时,可以通过两种方式更新用户昵称操作

  1. 使用微信昵称
  2. 用户输入最新的昵称

当用户提交表单时,我们将最新的昵称,同步到 userInfonickname字段中

当用户点击了确定以后,我们将新的的用户信息赋值给 data 中的 userInfo 字段

当用户点击保存时,更新用户信息

实现步骤:

  1. form 表单绑定 bindsubmit 事件,用来获取输入框最新的值
  2. input 组件绑定 type 属性,属性值为 nickname,获取微信昵称
  3. input 组件绑定 bindinput 事件,获取用户输入最新的昵称
  4. formType 设置为 submit 当用户点击确定后,触发 form 表单的 bindsubmit 事件
  5. form 表单的 bindsubmit 事件中进行赋值
  6. form 表单的取消按钮绑定事件,取消弹框

落地代码:

➡️ modules/settingModule/pages/profile/profile.wxml

html
 <van-dialog
    custom-style="position: relative"
    use-slot
    title="修改昵称"
    show="{{ isShowPopup }}"
    showConfirmButton="{{ false }}"
    showCancelButton="{{ false }}"
    transition="fade"
  >
    <!-- 1.需要使用form组件包裹input输入框以及按钮组件 -->
    <!-- 6.为form表单绑定submit事件在事件回调中获取用户输入的昵称 -->
+    <form bindsubmit="getNickName">
      <!-- 2.为input输入框添加 type='nickname'属性 -->
      <!-- 3.为input组件添加name属性form表单的submit事件会收集带有name属性的表单的值 -->
      <input
        class="input-name"
+       type="nickname"
+       name="nickname"
        value="{{ userInfo.nickname }}"
      />
      <view class="dialog-content">
        <!-- 4.form-type="reset"重置表单 -->
+       <button class="cancel" bindtap="cancelForm" form-type="reset">
          取消
        </button>
        <!-- 5.form-type="submit"按钮变为提交按钮,触发form组件的提交事件 -->
+       <button class="confirm" form-type="submit" type="primary">确定</button>
      </view>
    </form>
  </van-dialog>

➡️ modules/settingModule/pages/profile/profile.js

js
  // 获取用户昵称,并更新
  getNickName(event) {
    this.setData({
      'userInfo.nickname': event.detail.value.nickname, // 更新用户昵称
      isShowPopup: false // 关闭弹窗
    })
  },
  // 显示修改昵称弹框
  onUpdateNickName() {
    this.setData({
      isShowPopup: true,
      'userInfo.nickname': this.data.userInfo.nickname // 解决表单被重置数据问题
    })
  },

注意:

​ 使用 model:value 简易双向绑定数据,不能收集对象类型的数据