小程序慕尚花坊项目1
一、项目介绍
01. 项目概述
[慕尚花坊] 是一款 同城鲜花订购 的小程序,专业提供各地鲜花速递、鲜花预定、网上订花、包月鲜花等服务。最快3小时送花上门,保证花材新鲜和鲜花质量,可先送花后付款,专业花艺师傅精美包扎,品质保证,至诚服务。
02. 项目演示
[慕尚花坊] 项目涵盖电商项目常见功能模块,包含:
- 项目首页
- 商品分类
- 商品列表
- 商品详情
- 用户管理
- 收货地址
- 购物车
- 结算支付
- 订单管理
- 等……
03. 项目技术栈
[慕尚花坊] 项目使用原生小程序进行搭建开发,项目涵盖小程序开发所有常用的知识点
小程序内置组件:采用小程序内置组件 结合
Vant组件库实现页面结构的搭建项目中使用了 css 拓展语言 Scss 绘制页面的结构
小程序内置
API:交互、支付、文件上传、地图定位、网络请求、预览图片、本地存储等小程序分包加载:降低小程序的启动时间、包的体积,提升用户体验度
小程序组件开发:将页面内的功能模块抽象成自定义组件,实现代码的复用
网络请求封装:
wx.request方法封装、快捷方式封装、响应拦截器、请求拦截器骨架屏组件:利用开发者工具提供了自动生成骨架屏代码的能力,提高了整体使用体验和用户满意度。UI组件库:使用Vant组件库实现小程序 结构的绘制LBS:使用腾讯地图服务进行LBS逆地址解析,实现选择收货地址功能miniprogram-licia:使用 licia 进行函数的防抖节流async-validator:使用 async-validator 实现表单验证miniprogram-computed: 使用 miniprogram-computed 进行计算属性功能mobx-miniprogram:使用mobx-miniprogram进行项目状态的管理
04. 接口文档
二、申请开发权限
在开始开发一个小程序项目之前,需要先申请开发权限。
需要将自己的微信号发送给对应小程序账号的管理员,在小程序微信公众后台添加我们自己为开发者。
📌:注意事项:
需要将自己的 微信号 发送给对应小程序账号的管理员
手机号不是微信号 (除非将手机号设置为了微信号)
如何查看自己的微信号:

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

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

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


三、项目初始化
01. 创建项目与项目初始化
创建项目
在微信开发者工具的开始界面左侧检查项目类型,需要为 [小程序]
然后在右侧点击 [+] 开始新建项目
最后在弹出的界面中输入项目相关的信息,点击确定即可
📌 注意:
在新建项目的时候,[填写的 AppId 需要是自己的 AppId]


项目初始化
- 重置
app.js中的代码 - 删除
app.json中pages下的"pages/logs/logs"路径,同时删除pages/logs文件夹 - 删除
app.json中pages下的"rendererOptions"以及"componentFramework"字段(不使用最新的渲染引擎) - 重置
app.wxss中的代码 - 删除
components中的自定义组件 - 重置
pages/index文件夹下的index.js、index.wxss、index.html以及index.json文件 - 更新
utils下util.js的文件名为formatTime.js
02. 自定义构建 npm + 集成Sass
随着项目的功能越来越多、项目越来越复杂,文件目录也变的很繁琐,为了方便进行项目的开发,开发人员通常会对目录结构进行调整优化,在慕尚花坊项目中,我们就需要将小程序源码放到 miniprogram 目录下。
自定义构建
首先在项目根目录创建
miniprogram文件夹,将项目的核心文件移动到该目录下在
project.config.json配置miniprogramRoot选项,指定小程序源码的目录然后配置
project.config.json的setting.packNpmManually为true,开启自定义 node_modules 和 miniprogram_npm 位置的构建 npm 方式- 最后配置 project.config.json 的
setting.packNpmRelationList项,指定packageJsonPath和miniprogramNpmDistDir的位置 - packageJsonPath 表示 node_modules 源对应的 package.json
- miniprogramNpmDistDir 表示 node_modules 的构建结果目标位置
- 最后配置 project.config.json 的
安装
vant,然后进行npm 构建,测试是否能够正常vant构建成功npm i @vant/weapp
📌 注意:
配置后如果没有生效,需要 [ 重启微信开发者工具 ] ❗ ❗
集成 Sass
在 project.config.json 文件中,修改 setting 下的 useCompilerPlugins 字段为 ["sass"],即可开启工具内置的 sass 编译插件。
{
+ "miniprogramRoot": "miniprogram/",
....
"setting": {
"useCompilerPlugins": [
+ "sass"
],
+ "packNpmManually": true,
+ "packNpmRelationList": [
+ {
+ "miniprogramNpmDistDir": "./miniprogram",
+ "packageJsonPath": "./package.json"
+ }
+ ],
"srcMiniprogramRoot": "miniprogram/"
......
}03. 集成项目页面文件
思路分析:
打开 [慕尚花坊项目素材] 中的 [模板文件] 文件夹
复制该文件中全部的文件和文件夹,在项目的 miniprogram 目录下进行粘贴
代码分析:
app.json中配置了pages、window、tabBarapp.json中对项目中会使用到的Vant组件进行了全部的注册app.scss文件中导入了阿里巴巴使用图标库components文件夹中定义了两个公共的组件pages目录下存放了项目中所有页面的文件,后续我们会进行分包的处理
04. VsCode 开发小程序项目
知识点:
在进行项目开发的时候,部分同学可能不习惯微信开发者工具进行开发,而是习惯使用 VSCode 等编辑器
但是 VsCode 对小程序开发支持的不是非常友好,如果想通过 VSCode 开发小程序项目,需要安装以下插件:
WXML - Language Serviceprettier- 微信小程序开发工具
- 微信小程序助手-Y
- 小程序开发助手(可选)
- 其他......
💡 Tip:
使用 VsCode 开发小程序项目时,如果需要预览、调试小程序,依然需要借助微信开发者工具
安装插件:





配置详细插件:
在【项目的根目录】下创建
.vscode文件夹,注意:文件夹名字前面带.点❗在
.vscode文件夹下,创建settings.json,用来对安装的插件属性进行设置,具体属性设置从下面复制即可- 注意:
.vscode文件夹下的settings.json文件只对当前一个项目生效
- 注意:
在【项目的根目录】下创建
.prettierrc文件,进行Prettier代码规则的配置,规则从下面复制即可为了让
Prettier配置项在微信开发者工具生效,需要在微信开发者工具中也安装Prettier扩展插件。
➡️ .vscode/settings.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
{
"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": 2 | tab缩进为2个空格字符 |
| "printWidth": 80 | 一行的字符数,如果超过会进行换行,默认为80 |
| "trailingComma": "none" | 尾随逗号问题,设置为none 不显示 逗号 |
| "overrides": [] | overrides 解析器:默认情况下,Prettier 会根据文件文件拓展名推断要使用的解析器 |
📌:注意事项:
项目根目录
.vscode文件夹中settings.json文件只对当前项目生效❗如果想配置项生效,还需要注意:
在 VsCode 中只能打开当前一个小程序项目,不能同时打开多个小程序项目❗ 且项目目录请勿嵌套打开 ❗
四、通用模块封装
01. 为什么进行模块封装
在进行项目开发的时候,我们经常的会频繁的使用到一些 API,
例如:wx.showToast() 、wx.showModal()等消息提示 API ,这些 API 的使用方法如下:
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 封装成公共方法,封装后的使用方式如下:
// wx.showToast() 封装后的调用方式
toast()
toast(title: '数据加载失败....', mask: true )
// wx.showModal() 封装后的调用方式
const res = await modal(title: '提示',content: '鉴权失败,请重新登录 ?')
// 用户点击了确定
if (res) { ... } else { ... }02. 消息提示模块封装
基本使用:
wx.showToast() 消息提示框是在项目中频繁使用的一个小程序 API,常用来给用户进行消息提示反馈。使用方式如下:
wx.showToast({
title: '消息提示框', // 提示的内容
icon: 'success', // 提示的图标,success(成功)、error(失败)、loading(加载)、none(不显示图标)
duration: 2000, // 提示的延迟时间
mask: true // 是否显示透明蒙层,防止触摸穿透
})封装思路:
创建一个
toast方法对wx.showToast()方法进行封装在需要显示弹出框的时候调用
toast方法,并传入相关的参数,有两种参数方式:- 不传递参数,使用默认参值
- 传入部分参数,覆盖默认的参数
调用方式:
新封装的模块,我们希望有两种调用的方式:
模块化的方式对方法进行暴露,需要时导入使用
jsimport { toast } from './extendApi' toast() toast(title: '数据加载失败....', mask: true)将封装的模块挂载到
wx全局对象身上,无需导入即可使用jswx.toast() wx.toast(title: '数据加载失败....', mask: true )
实现步骤:
- 在
utils目录下新建extendApi.js文件 - 对
wx.showToast()方法进行封装
落地代码:
➡️ utils/extendApi.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
// import { toast } from './utils/extendApi'
// 导入 extendApi.js文件,先引入封装好的方法
import './utils/extendApi'
App({
onShow() {
// toast('项目启动成功', 'success')
wx.toast('资源加载完毕', 'loading')
}
})03. 模态对话框封装
基本使用:
wx.showModal() 模态对话框也是在项目中频繁使用的一个小程序 API,通常用于向用户询问是否执行一些操作,例如:询问用户是否真的需要退出、是否确认删除等等
wx.showModal({
title: '提示', // 提示的标题
content: '您确定执行该操作吗?', // 提示的内容
confirmColor: '#f3514f',
// 接口调用结束的回调函数(调用成功、失败都会执行)
complete({ confirm, cancel }) {
confirm && console.log('点击了确定') // A&&B 如果表达式A为ture返回B
cancel && console.log('点击了取消')
}
})封装思路:
- 对
wx.showModal()方法进行封装, 封装后的新方法叫modal - 调用该方法时,根据需要传递参数覆盖默认参数
- 封装的
modal方法的内部通过Promise返回用户执行的操作(确定和取消,都通过resolve返回) - 在需要显示模态对话框的时候调用
modal方法,并传入相关的参数,有二种调用方式:- 不传递参数,使用默认参数
- 传递参数,覆盖默认的参数
调用方式:
新封装的本地存储模块,我们依然希望有两种调用的方式:
- 模块化的方式先暴露再导入使用
- 将封装的模块挂载到
wx全局对象身上
实现步骤:
- 在
extendApi.js文件中新建modal方法,方法内部 modal方法,方法内部用来处理封装的逻辑
落地代码:
➡️ utils/extendApi.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
// 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.setStorageSync、wx.setStorage 等方法
try {
wx.setStorageSync(key, value) // 同步存储
} catch (err) { // try catch 对存储失败进行处理
console.error(`存储指定 ${key} 数据发生错误:`, err)
}
wx.setStorage({ // 异步存储
key: 'key',
data: 'data',
success (res) => {}, // 存储成功的回调
fail (err) => {} // 存储失败的回调
})如果直接使用这些 API,会比较麻烦,通常情况下,我们需要对本地存储的方法进行封装。
实现步骤:
- 在
utils目录下新建storage.js文件 - 在该文件中,封装对本地数据进行 存储、获取、删除、清除的方法(使用同步方法)
落地代码:
➡️ utils/storage.js
// 封装本地存储的方法
// 用于存储数据 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
import { clearStorage,removeStorage,getStorage,setStroage } from './utils/storage'
App({
setStroage('token', '234k23n')
setStroage('name', '王二狗')
console.log(getStorage('name'))
removeStorage('name')
clearStorage()
})05. 拓展:封装异步存储API
思路分析:
使用
Promise封装异步存储 APIjswx.setStorage({ key: 'key', data: 'data', success(res) {}, // 存储成功的回调 fail(err) {}, // 存储失败的回调 complete(res) {} // 存储完成的回调 })给 toast 以及 modal 方法添加注释
使用方式:
// 异步将数据存储到本地
asyncSetStorage(key, data)
// 异步从本地读取指定 key 的数据
asyncGetStorage(key)
// 异步从本地移除指定 key 的数据
asyncRemoveStorage(key)
// 异步从本地移除、清空全部的数据
asyncClearStorage()落地代码:
➡️ utils/storage.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
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 对象类型的参数,参数中可以按需指定以下字段来接收接口调用结果:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| success | function | 否 | 调用成功的回调函数 |
| fail | function | 否 | 调用失败的回调函数 |
| complete | function | 否 | 调用结束的回调函数(调用成功、失败都会执行) |
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使用方法:
import WxRequest from 'mina-request'
// 自定义配置新建一个实例
const instance = new WxRequest({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
})
// 通过 instance.request(config) 方式发起网络请求
instance.requst({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})
// 通过 instance.get 方式发起网络请求
instance.get(url, data, config)
// 通过 instance.delete 方式发起网络请求
instance.delete(url, data, config)
// 通过 instance.post 方式发起网络请求
instance.post(url, data, config)
// 通过 instance.put 方式发起网络请求
instance.put(url, data, config)
// ----------------------------------------------
// 添加请求拦截器
instance.interceptors.request = (config) => {
// 在发送请求之前做些什么
return config
}
// 添加响应拦截器
instance.interceptors.response = (response) => {
// response.isSuccess = true,代码执行了 wx.request 的 success 回调函数
// response.isSuccess = false,代码执行了 wx.request 的 fail 回调函数
// response.statusCode // http 响应状态码
// response.config // 网络请求请求参数
// response.data 服务器响应的真正数据
// 对响应数据做点什么
return response
}封装后网络请求模块包含以下功能
- 包含 request 实例方法发送请求
- 包含
get、delete、put、post等实例方法可以快捷的发送网络请求 - 包含 请求拦截器、响应拦截器
- 包含
uploadFile将本地资源上传到服务器 API - 包含
all并发请求方法 - 同时优化了并发请求时 loading 显示效果
02. 请求封装-request 方法
思路分析:
在封装网络请求模块的时候,采用 Class 类来进行封装,采用类的方式封装代码更具可复用性,也方便地添加新的方法和属性,提高代码的扩展性
我们先创建一个 class 类,同时定义 constructor 构造函数
// 创建 WxRequest 类
class WxRequest {
constructor() {}
}我们在 WxRequest 类内部封装一个 request 实例方法
request 实例方法中需要使用 Promise 封装 wx.request,也就是使用 Promise 处理 wx.request 的返回结果
request 实例方法接收一个 options 对象作为形参,options 参数和调用 wx.request 时传递的请求配置项一致
- 接口调用成功时,通过
resolve返回响应数据 - 接口调用失败时,通过
reject返回错误原因
➡️ /utils/request.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
// 导入封装好的请求实例
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. 请求封装-设置请求参数
思路分析:
在发起网络请求时,需要配置一些请求参数,
其中有一些参数我们可以设置为默认参数,例如:请求方法、超时时长 等等,因此我们在封装时我们要定义一些默认的参数。
// 默认参数对象
defaults = {
baseURL: '', // 请求基准地址
url: '', // 开发者服务器接口地址
data: null, // 请求参数
method: 'GET',// 默认请求方法
// 请求头
header: {
'Content-type': 'application/json' // 设置数据的交互格式
},
timeout: 60000 // 小程序默认超时时间是 60000,一分钟
// 其他参数...
}但是不同的项目,请求参数的设置是不同的,我们还需要允许在进行实例化的时候,传入参数,对默认的参数进行修改。例如:
// 对 WxRequest 进行实例化
const instance = new WxRequest({
baseURL: 'https://gmall-prod.atguigu.cn/mall-api', // 请求基准地址
timeout: 10000 // 微信小程序 timeout 默认值为 60000
})在通过实例,调用 request 实例方法时也会传入相关的请求参数
const res = await instance.request({
url: '/index/findBanner',
method: 'GET'
})从而得出结论:请求参数的设置有三种方式:
- 默认参数:在
WxRequest类中添加defaults实例属性来设置默认值 - 实例化时参数:在对
WxRequest类进行实例化时传入相关的参数,需要在constructor构造函数形参进行接收 - 调用实例方法时传入请求参数
默认参数和自定义参数的合并操作,通常会在constructor中进行。
因此我们就在 constructor 中将开发者传入的相关参数和defaults 默认值进行合并,需要传入的配置项覆盖默认配置项
➡️ utils/request.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 个快捷方法,分别是 get、delete、post、put,他们的调用方式如下:
instance.get('请求地址', '请求参数', '请求配置')
instance.delete('请求地址', '请求参数', '请求配置')
instance.post('请求地址', '请求参数', '请求配置')
instance.put('请求地址', '请求参数', '请求配置')这 4 个请求方法,都是通过实例化的方式进行调用,所以需要 Request 类中暴露出来 get、delete、post、put 方法。每个方法接收三个参数,分别是:接口地址、请求参数以及其他参数。
这 4 个快捷方法,本质上其实还是调用 request 方法,我们只要在方法内部组织好参数,调用 request 发送请求即可
➡️ utils/request.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
// 导入封装好的请求实例
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 回调

测试代码
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网络错误 )
注意:不管成功响应还是失败响应,都会执行响应拦截器
拦截器的使用方式:
// 请求拦截器
instance.interceptors.request = (config) => {
// 在发送请求之前做些什么
return config // 必须返回配置对象
}
// 响应拦截器
instance.interceptors.response = (response) => {
// 对响应数据做点什么
return response
}通过使用方式,我们可以得出结论:
可以在 WxRequest 类内部定义 interceptors 实例属性,属性中需要包含 request 以及 response 方法
需要注意:在发送请求时,还需要区分是否通过实例调用了拦截器:
- 没有通过实例调用拦截器,需要定义默认拦截器,在默认拦截器中,需要将请求参数进行返回
- 通过实例调用拦截器,那么实例调用的拦截器会覆盖默认的拦截器方法,然后将新增或修改的请求参数进行返回
实现拦截器的思路:
- 在
WxRequest类内部定义interceptors实例属性,属性中需要包含request以及response方法 - 是否通过实例调用了拦截器(是否进行了二次封装)
- 否:使用默认拦截器
- 是:实例调用的拦截器覆盖默认拦截器
- 在发送请求之前,调用请求拦截器
- 在服务器响应以后,调用响应拦截器
- 不管成功、失败响应,都需要调用响应拦截器
在 WxRequest 类内部定义 interceptors 实例属性,属性中需要包含 request 以及 response 方法。
➡️ utils/request.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在发送请求之前,调用请求拦截器,在服务器响应以后,调用响应拦截器
不管成功、失败,都需要调用响应拦截器
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),都会执行响应拦截器
那么怎么判断是请求成功,还是请求失败呢 ?
封装需求:
- 如果请求成功,将响应成功的数据传递给响应拦截器,同时在传递的数据中新增
isSuccess: true字段,表示请求成功 - 如果请求失败,将响应失败的数据传递给响应拦截器,同时在传递的数据中新增
isSuccess: false字段,表示请求失
在实例调用的响应拦截中,根据传递的数据进行以下的处理:
- 如果
isSuccess: true表示服务器响应了结果,我们可以将服务器响应的数据简化以后进行返回 - 如果
isSuccess: false表示是网络超时或其他网络问题,提示网络异常,同时将返回即可
➡️ utils/request.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 instance08. 请求封装-使用请求/响应拦截器
思路分析:
使用请求拦截器:
在发送请求时,购物车列表、收货地址、更新头像等接口,都需要进行权限验证,因此我们需要在请求拦截器中判断本地是否存在访问令牌 token ,如果存在就需要在请求头中添加 token 字段。
使用响应拦截器:
在使用 wx.request 发送网络请求时。只要成功接收到服务器返回,无论statusCode是多少,都会进入 success 回调,因此开发者根据业务逻辑对返回值进行判断。
后端返回的业务状态码如下:
- 业务状态码(code)=== 200, 说明接口请求成功,服务器成功返回了数据
- 业务状态码(code)=== 208, 说明没有 token 或者 token 过期失效,需要登录或者重新登录
- 业务状态码(code)=== 其他,说明请求或者响应出现了异常
➡️ utils/request.js(对 wx.request 进行封装)
// 采用类的方式进行封装会让代码更具有复用性,也方便添加新的属性和方法
// 暴露封装好的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 进行二次封装)
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 instance09. 请求封装-添加并发请求
思路分析:
前端并发请求是指在前端页面同时向后端发起多个请求的情况。当一个页面需要请求多个接口获取数据时,为了提高页面的加载速度和用户体验,可以同时发起多个请求,这些请求之间就是并发的关系。
我们通过两种方式演示发起多个请求:
- 使用
async和await方式 - 使用
Promise.all()方式
首先使用async 和 await 方式发送请求,使用 async 和 await 能够控制异步任务以同步的流程执行,代码如下
出现问题:
当第一个请求执行完以后,才能执行第二个请求,这样就会造成请求的阻塞,影响渲染的速度,如下图(请求是以同步的方式进行的)

解决方法:
这时候我们需要使用 Promise.all() 方式同时发起多个异步请求,并在所有请求完成后再进行数据处理和渲染。
使用
Promise.all()能够将多个请求同时发出,不会造成请求的阻塞,从而不会影响页面的渲染速度

在 WxRequest 实例中封装 all 方法,方法的内部,使用 Promise.all() 接收传递的多个异步请求,将处理的结果返回即可。
➡️ utils/request.js
class WxRequest {
.....
+ // 封装处理并发请求的 all 方法
+ all(promise) { // 接收传递过来的请求数组
+ return Promise.all(promise)
+ }
....
}
.......
➡️ /pages/test/test.js
import instance from '../../utils/http'
Page({
async getData() {
// 使用 Promise.all 同时处理多个异步请求
const [res1, res2] = await instance.all([
instance.get('/mall-api/index/findBanner'),
instance.get('/mall-api/index/findCategory1')
])
console.log(res1)
console.log(res2)
}
})注意:
Promise.all:当全部的Promise状态为成功返回状态为成功,有一个状态为失败,返回状态为失败,可以使用Promise.allSettled来代替
Promise.allSettled:返回结果永远为成功状态,成功的状态的值为每一个Promise对象状态和值
10. 请求封装-添加 loading
思路分析:
在封装时添加 loading 效果,从而提高用户使用体验
在请求发送之前,需要通过
wx.showLoading展示loading效果当服务器响应数据以后,需要调用
wx.hideLoading隐藏loading效果
要不要加 loading 添加到 WxRequest 内部 ?
在类内部进行添加,方便多个项目直接使用类提供的 loading 效果,也方便统一优化 wx.showLoading 使用体验。
但是不方便自己来进行 loading 个性化定制。
如果想自己来控制 loading 效果,带来更丰富的交互体验,就不需要将 loading 封装到类内部,但是需要开发者自己来优化 wx.showLoading 使用体验,每个页面都要写一份。
在项目中我们会选择第一种方式
不过也会通过属性控制是否展示 loading,从而方便类使用者自己控制 loading 显示
落地代码:
➡️ utils/request.js
class WxRequest {
// coding...
constructor(options = {}) {
// coding...
}
// 创建 request 请求方法
request(options) {
// 拼接完整的请求地址
options.url = this.defaults.baseURL + options.url
// 合并请求参数
options = { ...this.defaults, ...options }
+ // 发送请求之前添加 loding
+ wx.showLoading()
// 如果存在请求拦截器,我们则调用请求拦截器
if (this.interceptors.request) {
// 请求之前,触发请求拦截器
options = this.interceptors.request(options)
}
// 方法返回一个 Promise 对象
return new Promise((resolve, reject) => {
wx.request({
...options,
// 当接口调用成功时会触发success回调函数
success: (res) => {
// coding...
},
// 当接口调用失败时会触发fail回调函数(出现网络错误)
fail: (err) => {
// coding...
},
// 无论接口调用成功或失败都会执行
+ complete: () => {
+ // 接口调用完成后隐藏 loding
+ wx.hideLoading()
+ }
})
})
}
// coding...
}Tips:
也可以在自己封装的请求拦截器中开启加载效果,在响应拦截器中关闭加载效果
11. 请求封装-完善 loading
思路分析:
目前在发送请求时,请求发送之前会展示 loading,响应以后会隐藏 loading。
但是 loading 的展示和隐藏会存在以下问题:
- 每次请求都会执行
wx.showLoading(),但是页面中只会显示一个,后面的loading会将前面的覆盖 - 同时发起多次请求,只要有一个请求成功响应就会调用
wx.hideLoading,导致其他请求还没完成,也不会loading - 请求过快 或 一个请求在另一个请求后立即触发,这时候会出现
loading闪烁问题
我们通过 队列 的方式解决这三个问题:首先在类中新增一个实例属性 queue,初始值是一个空数组
- 发起请求之前,判断
queue如果是空数组则显示loading,然后立即向queue新增请求标识 - 在
complete中每次请求成功结束,从queue中移除一个请求标识,queue为空时隐藏loading - 为了解决网络请求过快产生
loading闪烁问题,可以使用定时器来做判断即可
原理:在发送并发请求时,每发送一次请求就向数组queue中添加一个标识,当请求完成时删除数组queue中的一个标识,当数组queue为空时关闭加载效果。从而实现当并发请求全部执行完毕后再关闭加载效果,当没有请求时设置定时器等待一定时间后再关闭加载效果
落地代码:
➡️ utils/request.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 instance12. 请求封装-控制 loading 显示
思路分析:
在我们封装的网络请求文件中,通过 wx.showLoading 默认显示了 loading 效果
但是在实际开发中,有的接口可能不需要显示 loading 效果,或者开发者希望自己来控制 loading 的样式与交互,那么就需要关闭默认 loading 效果。
这时候我们就需要一个开关来控制 loading 显示。
- 类内部设置默认请求参数
isLoading属性,默认值是true,在类内部根据isLoading属性做判断即可 - 某个接口不需要显示
loading效果,可以在发送请求的时候,可以新增请求配置isLoading设置为false - 整个项目都不需要显示
loading效果,可以在实例化的时候,传入isLoading配置为false
实现步骤:
在 WxRequest 类的默认请求配置项中,设置 isLoading 默认值为 true,显示 loading
jsclass WxRequest { // 初始化默认的请求属性 defaults = { url: '', // 开发者服务器接口地址 data: null, // 请求参数 header: {}, // 设置请求的 header timeout: 60000, // 超时时间 method: 'GET', // 请求方式 + isLoading: true // 是否显示 loading 提示框 } // code... }在进行实例化的时候,可以配置 isLoading 配置为 false,隐藏 loading
js// 对 WxRequest 进行实例化 const instance = new WxRequest({ baseURL: 'https://gmall-prod.atguigu.cn/mall-api', + isLoading: false // 隐藏 loading })在发送网络请求时候,传入请求配置 isLoading 配置为 false,隐藏 loading
jsasync func() { + // 请求配置 isLoading 配置为 false,隐藏 loading + await instance.get('/index/findCategory1', null, { isLoading: true }) }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,用来将本地资源上传到服务器。
例如:在获取到微信头像临时地址以后,将微信头像地址上传到公司服务器,获取持久地址。
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 实例方法,实例方法接收四个属性:
/**
文件上传接口封装
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
// 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
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
// 采用类的方式进行封装会让代码更具有复用性,也方便添加新的属性和方法
// 暴露封装好的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
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
// 导入封装好的请求实例
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 包实现功能。
npm install mina-request📌 构建 npm:
安装包后,需要在微信开发者工具中进行 npm 构建,点击
工具➡️构建 npm
其余步骤参考文档进行开发即可:
对npm包提供的请求方法进行二次封装
utils/http.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 instance15. 环境变量-小程序设置环境变量
知识点:
在实际开发中,不同的开发环境,调用的接口地址是不一样的。
例如:开发环境需要调用开发版的接口地址,生产环境需要调用正式版的接口地址
这时候,我们就可以使用小程序提供了 wx.getAccountInfoSync() 接口,用来获取当前账号信息,在账号信息中包含着 小程序 当前环境版本。
| 环境版本 | 合法值 |
|---|---|
| 开发版 | develop |
| 体验版 | trial |
| 正式版 | release |
落地代码:
// 获取当前帐号信息
const accountInfo = wx.getAccountInfoSync()
// 获取小程序项目的 appId
console.log(accountInfo.miniProgram.appId)
// 获取小程序 当前环境版本
console.log(accountInfo.miniProgram.envVersion) // develop根据环境的不同,我们给 env 变量设置不同的请求基准路径 baseURL 然后将 env环境变量导出
utils/env.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 envutils/http.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
// 导入封装好的实例方法
import http from '../utils/http'
// 获取首页轮播图的接口方法
export const reqSwiperData = () => http.get('/index/findBanner')这样做的有以下几点好处:
- 易于维护:一个文件就是一个模块,一个方法就是一个功能,清晰明了,查找方便
- 便于复用:哪里使用,哪里导入,可以在任何一个业务组件中导入需要的方法
- 团队合作:分工合作
调用接口方法
test.js
// 导入接口 API
import { reqSwiperData } from '../../api/index'
Page({
// 获取首页数据
async getHomeList() {
// 获取轮播图数据
const res = await reqSwiperData()
console.log(res)
}
})六、项目首页

01. 获取首页数据
思路分析:
我们先来分析下在项目的首页我们需要完成的一些功能
- 轮播图区域
- 商品导航区域
- 活动宣传区域
- 猜你喜欢区域
- 人气推荐区域
在实现这些功能之前,我们需要先获取数据,在获取数据以后,然后进行页面的渲染,同时完成进行功能的开发。
因为需要同时获取 5 个接口的数据,所以我们使用并发请求来实现。这样能够提升页面的渲染速度。
实现步骤:
- 封装接口请求函数,可以一个个封装,也可以直接使用
all方法进行封装 - 在页面 .js 文件中导入封装的接口
API函数 - 在
onLoad钩子函数中调用方法,获取首页轮播图数据
落地代码:
➡️ api/index.js: 准备接口 API
// 导入封装好的实例方法
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:
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 文件中导入组件,然后将组件当成标签进行使用
{
"usingComponents": {
"banner": "./banner/banner"
}
}<!-- 轮播图区域 -->
<banner />swiper、swiper-item、navigator、image组件实现页面结构的搭建block渲染数组,实现列表渲染- 使用
flex布局实现了页面样式的绘制
另外需要注意的是:轮播图面板指示点不支持自定义,所以只能页面结构的方式,实现轮播图的面板指示点功能
index/banner/banner.js
Component({
// 接收父组件传递的数据
properties: {
// 轮播图数据
bannerList: {
type: Array,
value: []
}
},
data: {},
methods: {}
})➡️ page/index/index.js
<!-- 轮播图区域 -->
+ <banner bannerList="{{ bannerList }}" /> // 向子组件传递获取到的轮播图的数据index/banner/banner.wxml
<!-- 轮播图 -->
<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 对小圆点进行动态的渲染
实现思路:
- 在
data中初始化状态activeIndex默认为 0 - 给
swiper绑定bindchange事件,监听轮播图是否切换,将切换后轮播图的索引赋值给activeIndex - 利用
activeIndex对小圆点进行动态的渲染(使用动态类名)
落地代码:
➡️ pages/index/banner/banner.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
<!-- 轮播图 -->
<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 文件夹,里面存放导航分类组件。
- 采用
view、navigator、image、text组件实现了进行页面结构的搭建 - 使用
flex布局实现了页面样式的绘制
➡️ pages/index/entrance/entrance.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 文件中导入组件,然后将组件当成标签进行使用
{
"usingComponents": {
// ...
"entrance": "./entrance/entrance"
},
"navigationBarTitleText": "慕尚花坊"
}➡️ page/index/index.wxml
<!-- 导航分类 -->
<entrance cateList="{{ categoryList }}" />➡️ pages/index/entrance/entrance.js
Component({
// 组件的属性列表
properties: {
cateList: {
type: Array,
value: []
}
},
// coding...
}➡️ pages/index/index.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. 分析猜你喜欢+人气推荐并渲染
猜你喜欢和人气推荐区域结构分析:
猜你喜欢和人气推荐的布局是一致的,所以将两个模块放到一起实现

商品列表在项目中属于常见的结构,商品列表区域依然采用组件化方式开发,商品列表组件由两个组件进行构成:
goods-list商品列表组件,包含者商品卡片组件,用来展示商品列表goods-card商品卡片组件,承载某个商品的结构,用来展示单个商品
goods-list 商品列表组件包含三个结构:标题(允许用户自定义)、商品列表(商品卡片)、查看更多
只不过在渲染数据的时候需要将数据传递给 goods-list 商品列表组件, goods-list组件内部传递给 goods-card 商品卡片组件,goods-card 组件内部渲染数据。
➡️ page/index/index.wxml:将数据传递给 goods-list 组件
<!-- 商品列表 -->
<goods-list title="猜你喜欢" list="{{ guessList }}"></goods-list>
<goods-list title="人气推荐" list="{{ hotList }}"></goods-list>➡️ components/goods-list/goods-list.js:接收首页传递的 list 数据
Component({
// 组件的属性列表
properties: {
// 列表标题
title: {
type: String,
value: '',
},
// 传递的列表数据
list: {
type: Array,
value: []
}
}
}➡️ components/goods-list/goods-list.wxml:遍历 goods-item 组件,并将数据传递给 goods-item
<!-- 商品列表组件 -->
+ <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 组件
Component({
// 组件的属性列表
properties: {
// 每一项商品的数据
goodItem: {
type: Object,
value: {}
}
}
}➡️ components/goods-list/goods-item.wxml:将数据传递给 goods-item 组件
<!-- 列表分类卡片 -->
<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 绘制一些灰色的区块,将页面内容大致勾勒出轮廓。
通常会在页面完全渲染之前,将骨架屏代码进行展示,待数据加载完成后,再替换成真实的内容。
骨架屏的设计旨在优化用户体验。
在进行项目开发时,我们需要手工维护骨架屏的代码,当业务变更时,同样需要对骨架屏代码进行调整。
为了方便开发者进行骨架屏的绘制,开发者工具提供了自动生成骨架屏代码的能力。
使用步骤:
使用微信开发者工具为当前正在预览的页面生成骨架屏代码,工具入口位于模拟器面板右下角三点处

点击生成骨架屏,将有弹窗提示是否允许插入骨架屏代码。
确定后将在当前页面同级目录下生成
page.skeleton.wxml和page.skeleton.wxss两个文件,分别为骨架屏代码的模板和样式。
将生成的骨架屏文件存放在当前页面的
skeleton文件夹下在页面
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>在页面样式文件
index/index.scss中引入骨架屏的样式css// 引入骨架屏的样式文件 @import './skeleton/index.skeleton.scss';在
data中声明loading的状态为true当数据请求完以后将loading的状态为falsejsimport { 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() } })查看页面效果

注意:
需要将骨架屏中的行内样式提取为 class 样式
七、商品分类
商品分类是一个单独的 tabBar 页面,当点击分类 tabBar 的时候就能够进入商品分类页面。
在商品分类页面我们主要实现三个功能:
- 一级分类的渲染
- 一级分类的切换
- 二级分类的渲染

01. 获取商品分类数据
思路分析:
需要调用接口获取分类的数据接口地址
在熟悉了接口文档以后,根据接口文档封装接口 API 函数,然后在页面调用 API 函数获取分类的数据,在获取到数据以后,使用后端返回的数据对一级、二级分类进行渲染。
实现步骤:
- 在项目根目录下
api目录下新建category.js文件,用来管理分类页面接口请求 - 在该文件中导入封装的网络请求模块,根据接口文档,创建获取分类数据的
API函数reqCategoryData - 在
/pages/category/category.js中导入封装好的获取分类数据的API函数 - 页面数据需要在页面加载的时候进行调用,因此需要在
onLoad钩子函数中调用reqCategoryData方法 - 在获取到数据以后,使用后端返回的数据对页面进行渲染
落地代码:
➡️ api/category.js
// 导入封装的网络请求模块实例
import http from '../utils/http'
// 获取商品分类的接口方法
export const reqCategoryData = () => http.get('/index/findCategoryTree')➡️ page/category/category.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
这时候在模板中,可以拿遍历后一级分类的对应的索引 index 和 activeIndex 来进行对比
如果值一致,说明是当前分类被激活,添加 active 类名即可
实现步骤:
- 在
data中初始化变量activeIndex,初始值为 0 - 给一级分类绑定点击事件
updateActive,同时自定义一个自定义属性data-index值为索引index - 在事件处理程序
updateActive中,将index赋值给activeIndex - 在模板中进行
index和activeIndex的对比
落地代码:
➡️ page/category/category.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
Page({
data: {
categoryList: [], // 分类数据列表
+ activeIndex: 0, // 点击高亮导航id
},
+ // 导航分类点击事件
+ updateActive(e) {
+ this.setData({
+ activeIndex: e.currentTarget.dataset.index
+ })
+ }
// coding...
}03. 获取 & 渲染二级分类数据
思路分析:
在一级分类下存在 children 字段, children 字段中的数据则是一级分类对应的二级分类的数据,当我们访问一级分类时,只需要将一级分类对应的二级分类拿出来进行渲染即可。

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

实现步骤:
- 在模板中利用
activeIndex获取到对应二级分类的数据 - 对页面进行渲染即可
落地代码:
<!-- 右侧的滚动视图区域 -->
<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的具体流程 **
- 客户端向服务器发起登录请求,服务端验证用户名与密码
- 验证成功后,服务端会签发一个
Token,并将Token发送到客户端 - 客户端收到
token以后,将其存储起来,比如放在localStorage、sessionStorage中 - 客户端每次向服务器请求资源的时候需要带着服务端签发的
Token,服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据

02. 用户登录-小程序登录流程介绍
业务介绍:
传统的登录功能,需要用户先注册,注册完成以后,使用注册的账号、密码进行登录。
小程序的登录操作则比较简单,小程序可以通过微信提供的登录能力,便捷地获取微信提供的用户身份标识进行登录。
免去了注册和输入账号密码的步骤,从而提高了用户体验。
小程序登录图示:

登录流程说明:
用户访问小程序,点击 [登录] ,调用
wx.login()方法获取 临时登录凭证code临时登录凭证 code,就像是一个会过期的临时身份证一样,有效时间仅为 5分钟
使用
wx.request()方法将 临时登录凭证code 传递给开发者服务器,方便后续可以换取微信用户身份 id开发者的后台接收 临时登录凭证code,同时在微信公众后台拿到
AppId和AppSecret,向微信服务器发送请求, 请求参数合法的话,微信服务器会给开发者后台返回 openid(微信用户的唯一标识) 以及 session_key(会话密钥) 等openid 是微信用户的唯一标识,也就是微信用户身份 id,可以用这个 id 来区分不同的微信用户
session_key 则是微信服务器给开发者服务器颁发的身份凭证,
开发者可以用session_key请求微信服务器其他接口来获取一些其他信息
开发者后台在接收到微信服务器返回的数据以后,会执行一些业务逻辑的处理,例如:将用户标识和其他信息进行加密处理,生成自定义登录态,这个登录态可以理解为就是
Token,然后让Token与openid和session_key进行关联开发者后台处理好逻辑后,会将 自定义登录态
Token返回给微信小程序客户端,客户端收到token以后,将其存储起来,比如放在localStorage中。客户端每次向开发者后台发送请求的时候,需要携带自定义登录态
Token,开发者后台收到请求后,对Token进行验证识别用户身份,同时拿自定义登录态Token查询openid和session_key,从而获取用户请求的数据,进行返回。
03. 用户登录-实现小程序登录功能
思路分析:
当用户没有登录的时候,需要点击个人中心的头像,跳转到登录页面进行登录。在登录成功以后,需要再次返回到个人中心页面
在登录页面我们使用了 Vant 提供的两个组件来进行页面结构的绘制
给登录按钮绑定点击事件,在事件处理程序中,调用 wx.login 获取 临时登录凭证code ,
然后调用后端接口,将 临时登录凭证code 传递给后端
根据接口文档封装接口 API 函数,当点击授权登录按钮的时候调用 API 函数,在获取到 token 以后,将 token 存储到本地,然后跳转到登录之前的页面。
实现步骤:
- 在
/api/user.js文件中根据接口文档,创建登录的API函数login - 给登录按钮绑定点击事件,对应
login回调函数 - 在
login回调函数中调用wx.login()方法,获取临时登录凭证code - 在
/pages/login/login.js中导入封装好的API函数,传入 临时登录凭证code 然后调用 - 在登录成功以后将
token存储到本地
落地代码:
➡️ /api/user.js
import http from '../utils/http'
// 上传用户code获取token的接口方法
export const reqLogin = (code) => http.get(`/weixin/wxLogin/${code}`)➡️ /pages/login/login.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 对象关联
实现步骤:
- 安装
Mobx两个包,在安装好包以后,对包进行构建,点击构建 npm - 在项目的根目录下创建
store文件夹,然后在该文件夹下新建userstore.js - 导入核心的
observable、action方法,创建Store,同时声明数据和方法 - 在登录页面,导入
ComponentWithStore方法,并配置storeBindings方法让页面和Store对象关联
落地代码:
安装依赖,安装完成后构建 npm
npm i mobx-miniprogram mobx-miniprogram-bindings➡️ /store/userStore.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
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和本地中
实现步骤:
在
store/userstore.js中新增userInfo字段,同时创建修改的action方法在
login.js中使用映射userInfo数据和setUserInfo方法在
/api/user.js文件中根据接口文档,创建获取用户信息的API函数reqUserInfo在
/pages/login/login.js中导入封装好的获取商品列表的API函数创建
getUserInfo方法,在getUserInfo方法中调用接口API函数reqUserInfo在登录成功以后,调用
getUserInfo方法获取用户,然后将用户信息存到本地以及Store
落地代码:
➡️ /api/user.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
// 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
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 中取出用户信息数据,并渲染到页面上
个人中心页面展示用于展示个人信息
如果用户没有登录的时候,展示没有登录的头像、提示用户登录的文案信息,不展示设置按钮
如果用户已经登录,展示用户的头像和昵称,并且展示设置按钮,方便用户对收货地址、头像、昵称进行更改
实现步骤:
- 在个人中心页面导入
ComponentWithStore方法构建页面 - 配置
storeBindings让组件和Store建立关联 - 渲染页面
落地代码:
➡️/pages/my/my.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
<!--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. 分包处理-配置分包以及预下载
思路分析:
随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。
因此我们需要将 更新个人资料 和 收货地址 功能配置成一个分包,
当用户在访问设置页面时,还预先加载 更新个人资料 和 收货地址 所在的分包
在分包后,通过查看代码依赖查看是否分包完成

📌 注意事项:
- 在配置好商品详情和商品列表的分包以后,需要更改页面中的跳转路径 (路径前加分包路径)!
- 可以利用项目全局搜索的功能,进行批量更改
实现步骤:
- 在项目的根目录新建目录
modules/settingModule/pages将设置分包下的页面移动到该目录 - 在
app.json新增subpackages进行分包配置,新增preloadRule进行分包预下载配置 - 在
subpackages设置分包的 根目录root、别名name、页面路径pages - 在
preloadRule设置预下载。
落地代码:
➡️ app.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 方法
实现步骤:
- 新建
behavior.js文件,从mobx-miniprogram-bindings库中导入BehaviorWithStore方法 - 在
BehaviorWithStore方法中配置storeBindings配置项从Store中映射数据和方法 - 在
Page方法中导入创建的behavior,然后配置behavior属性,并使用导入的behavior
落地代码:
// 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
+ import { userBehavior } from './behavior'
Page({
+ behaviors: [userBehavior],
// 页面的初始数据
data: {
isShowPopup: false // 控制更新用户昵称的弹框显示与否
},
// 其他代码略...
})➡️ modules/settingModules/pages/profile/profile.wxml
<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. 更新用户信息-获取头像临时路径
思路分析:
当用户点击头像时,可以对头像进行更新操作,我们使用通过微信提供的头像昵称填写能力快速完善

如果需要使用小程序提供的头像填写能力,需要两步:
- 将 button 组件
open-type的值设置为chooseAvatar - 当用户选择需要使用的头像之后,可以通过
bindchooseavatar事件回调获取到头像信息的临时路径
实现步骤:
- 给
button按钮绑定open-type属性,值为chooseAvatar - 用户点击了头像后,在
bindchooseavatar事件回调获取到头像信息的临时路径
落地代码:
➡️ modules/settingModules/pages/profile/profile.wxml
<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
// 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 ,语法如下:
wx.uploadFile({
url: '开发者服务器地址',
filePath: '要上传文件资源的路径 (本地路径)',
name: '文件对应的 key',
header: 'HTTP 请求 Header',
// 接口调用成功的回调函数
success: (res) => {},
// 接口调用失败的回调函数
fail: (err) => {}
})实现步骤:
- 在获取到用户的临时头像路径以后,调用
wx.uploadFile()方法,同时设置好基本的参数, - 在上传成功后,获取到服务器返回的永久地址
- 将地址赋值给
data中的数据
落地代码:
➡️ api/user.js
+ // 上传用户头像到服务器的接口方法
+ export const reqUploadFile = (filePath, name) =>
http.upload('/fileUpload', filePath, name)➡️ modules/settingModules/pages/profile/profile.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
实现步骤:
- 在
/api/user.js文件中根据接口文档,创建获取用户信息的API函数reqUpdateUserInfo - 给修改个人资料的保存按钮绑定点击事件,触发
updateUserInfo回调函数 - 在回调函数中调用接口
API函数reqUpdateUserInfo,同时传入用户的信息 - 更新用户信息以后,将用户信息存储到本地同时同步到
Store
落地代码:
➡️/api/user.js
// 更新用户信息的接口方法
export const updateUserInfo = (userInfo) =>
http.post('/weixin/updateUser', userInfo)➡️ modules/settingModule/pages/profile/profile.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. 更新用户信息-更新用户昵称
思路分析:
我们需要实现将用户昵称更改的功能

更新用户昵称的接口和更新用户头像的接口是同一个,因此不需要再次封装,直接复用即可
当点击更新用户昵称时,弹出弹框,当用户将在输入框光标聚焦到输入框时,可以通过两种方式更新用户昵称操作
- 使用微信昵称
- 用户输入最新的昵称
当用户提交表单时,我们将最新的昵称,同步到 userInfo 的 nickname字段中
当用户点击了确定以后,我们将新的的用户信息赋值给 data 中的 userInfo 字段
当用户点击保存时,更新用户信息
实现步骤:
- 给
form表单绑定bindsubmit事件,用来获取输入框最新的值 - 给
input组件绑定type属性,属性值为nickname,获取微信昵称 - 给
input组件绑定bindinput事件,获取用户输入最新的昵称 - 将
formType设置为submit当用户点击确定后,触发form表单的bindsubmit事件 - 在
form表单的bindsubmit事件中进行赋值 - 给
form表单的取消按钮绑定事件,取消弹框
落地代码:
➡️ modules/settingModule/pages/profile/profile.wxml
<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
// 获取用户昵称,并更新
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简易双向绑定数据,不能收集对象类型的数据