1.mobx-miniprogram
1.1 mobx-miniprogram 介绍
mobx-miniprogram是针对微信小程序开发的一个简单、高效、轻量级状态管理库,它基于Mobx状态管理框架实现。
-
作用与优势:
- 全局共享状态:使用mobx-miniprogram,开发者可以很方便地在小程序中全局共享状态。
- 自动更新视图组件:当状态发生变化时,所有关联组件都会自动更新相对应的数据,无需手动操作。
- 提升开发效率:通过自动更新视图组件,可以显著提升小程序的开发效率。
-
核心概念:
- observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。
- action:用于定义改变状态的方法,确保状态改变的可追踪性和可预测性。
-
使用方式:
- 安装:需要安装两个包,分别是mobx-miniprogram和mobx-miniprogram-bindings。其中,mobx-miniprogram用于创建Store对象,存储应用的数据;而mobx-miniprogram-bindings则用于将状态和组件、页面进行绑定关联。
- 创建Store对象:在小程序根目录下新建store.js文件,并使用observable和action等核心概念定义Store对象。
- 在页面和组件中使用:可以通过behavior绑定或手工绑定的方式,将页面、自定义组件和store进行绑定,从而在页面和组件中操作数据。
1.2 创建 Store 对象
创建 Store 对象是使用 mobx-miniprogram 进行状态管理的第一步。Store 对象用于存储应用的数据,并通过 observable 属性使其响应式,以及通过 action 方法来修改这些数据。以下是创建 Store 对象的基本步骤:
1. 引入 mobx-miniprogram
首先,确保已经安装了 mobx-miniprogram
包。如果尚未安装,可以通过 npm 进行安装:
npm install mobx-miniprogram
2. 定义 Store
在小程序项目中创建一个新文件,例如 store.js
,并在该文件中定义 Store
对象:
import { observable, action, computed } from 'mobx-miniprogram';
// 创建 Store 对象
const store = observable({
// 定义响应式状态
count: 0,
// 定义修改状态的 action 方法
increment: action(function () {
this.count++;
}),
decrement: action(function () {
this.count--;
}),
// 定义计算属性
get doubleCount() {
return this.count * 2;
}
});
// 导出 Store 对象
export default store;
3. 使用 Store
创建完 Store
对象后,可以在小程序的页面或组件中使用它。为了实现这一点,需要使用 mobx-miniprogram-bindings
包来绑定 Store
和页面或组件
4. 绑定 Store 到页面或组件
使用 mobx-miniprogram-bindings
中的 createStoreBindings
函数将 Store
中的数据和行为绑定到页面或组件的 data
和方法中:
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import store from './store.js'; // 引入 Store 对象
Page({
data: {
// 使用 createStoreBindings 来绑定 Store 中的数据
count: createStoreBindings(store, ['count']).get()
},
onLoad() {
// 绑定 Store
this.storeBindings = createStoreBindings(this, {
store,
fields: ['count'],
actions: ['increment', 'decrement']
});
},
onUnload() {
// 页面卸载时解绑
if (this.storeBindings) {
this.storeBindings.destroyStoreBindings();
}
},
increment() {
this.storeBindings.actions.increment();
},
decrement() {
this.storeBindings.actions.decrement();
}
});
在这个例子中,increment
和 decrement
方法被绑定到页面上,允许通过调用这些方法来修改 Store
中的状态。同时,count
状态被绑定到页面的 data
中,使得它在状态变化时能够自动更新。
1.3 在组件中使用数据
如果需要 `Page` 或者`Component`中对共享的数据进行读取、更新操作,需要使用 `mobx-miniprogram-bindings``mobx-miniprogram-bindings` 的作用就是将 `Store` 和 页面或组件进行绑定关联
如果需要在组件中使用状态,需要 `mobx-miniprogram-bindings` 库中导入 `ComponentWithStore` 方法在使用时:**<font color="red">需要将 `Component` 方法替换成 `ComponentWithStore` 方法 </font>**,原本组件配置项也需要写到该方法中
在替换以后,就会新增一个 `storeBindings` 配置项,配置项常用的属性有以下三个:
1. `store`: 指定要绑定的 `Store` 对象
2. `fields`: 指定需要绑定的 `data` 字段
3. `actions`: 指定需要映射的 `actions` 方法
// components/custom01/custom01.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
ComponentWithStore({
data: {
someData: '...'
},
storeBindings: {
store: numStore,
fields: ['numA', 'numB', 'sum'],
actions: ['update']
}
})
1.4 在页面中使用数据-方式1
Component 方法用于创建自定义组件。
小程序的页面也可以视为自定义组件,因此页面也可以使用 Component 方法进行构建,从而实现复杂的页面逻辑开发。
如果我们使用了 Component 方法来构建页面,那么页面中如果想使用 `Store` 中的数据,使用方式和组件的使用方式是一样的
1. 从 `mobx-miniprogram-bindings` 库中导入 `ComponentWithStore` 方法
2. 将 `Component` 方法替换成 `ComponentWithStore` 方法
3. 然后配置 `storeBindings` 从 `Store` 中映射数据和方法即可
// index/index.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
ComponentWithStore({
data: {
someData: '...'
},
storeBindings: {
store: numStore,
fields: ['numA', 'numB', 'sum'],
actions: ['update']
}
})
1.5 在页面中使用数据-方式2
使用 `mobx-miniprogram-bindings` 提供的 `BehaviorWithStore` 方法来和 `Store` 建立关联。
小程序的 behavior 方法是一种代码复用的方式,可以将一些通用的逻辑和方法提取出来,然后在多个组件中复用,从而减少代码冗余,提高代码的可维护性。**<font color="red">在页面中也可以使用 `behaviors` 配置项</font>**
使用方式如下:
1. 新建 `behavior` 文件,从 `mobx-miniprogram-bindings` 库中导入 `BehaviorWithStore` 方法
2. 在 `BehaviorWithStore` 方法中配置 `storeBindings` 配置项从 `Store` 中映射数据和方法
3. 在 `Page` 方法中导入创建的 `behavior` ,然后配置 `behavior` 属性,并使用导入的 `behavior`
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
export const indexBehavior = BehaviorWithStore({
storeBindings: {
store: numStore,
fields: ['numA', 'numB', 'sum'],
actions: ['update'],
}
})
// index.js
import { indexBehavior } from './behavior'
Page({
behaviors: [indexBehavior]
// 其他配置项
})
1.6 fields、actions 对象写法
在mobx-miniprogram-bindings中,fields和actions是用于定义你需要从MobX store中绑定到页面或组件的字段(observable)和动作(actions)的对象。
fields 对象
fields对象应该是一个对象字面量,其属性名对应你想要绑定的observable字段名。这些字段名将作为数据源的键(key)被映射到页面的data对象中。
actions 对象
actions对象也应该是一个对象字面量,其属性名对应你想要绑定的action名。这些动作名将被映射为页面方法,以便你可以在页面中直接调用它们。
1.7 绑定多个 store 以及命名空间
在实际开发中,一个页面或者组件可能会绑定多个 `Store` ,这时候我们可以将 `storeBindings` 改造成数组。数组每一项就是一个个要绑定的 `Store`。
如果多个 `Store` 中存在相同的数据,显示会出现异常。还可以通过 `namespace` 属性给当前 `Store` 开启命名空间,在开启命名空间以后,访问数据的时候,需要加上 `namespace ` 的名字才可以。
// behavior.js
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
export const indexBehavior = BehaviorWithStore({
storeBindings: [
{
namespace: 'numStore',
store: numStore,
fields: ['numA', 'numB', 'sum'],
actions: ['update'],
}
]
})
// index/index.wxml
<view>{{ numStore.numA }} + {{ numStore.numB }} = {{numStore.sum}}</view>
2. miniprogram-computed
2.1 计算属性 `computed`
在 mobx-miniprogram
中,可以使用计算属性(computed
)来基于其他状态(observable)定义衍生数据。这些计算属性会根据其所依赖的状态的变化而自动重新计算,从而确保数据始终是最新的。
然而,与 MobX 的传统用法不同,mobx-miniprogram
是为微信小程序设计的,它并没有直接提供 computed
装饰器或函数。但是,可以通过定义 getter 方法来模拟计算属性的行为。
以下是如何在 mobx-miniprogram
的 Store 中定义和使用计算属性的示例:
- 定义 Store:
在 store.js
文件中,使用 observable
来定义状态,并使用 getter 方法来定义计算属性。
import { observable, action } from 'mobx-miniprogram';
const store = observable({
// 响应式数据
numA: 1,
numB: 2,
// 计算属性(通过 getter 方法定义)
get sum() {
return this.numA + this.numB;
},
// 状态修改方法
addNumA: action('addNumA', (num) => {
this.numA += num;
}),
// ... 其他属性和方法
});
export default store;
在这个例子中,sum
是一个计算属性,它基于 numA
和 numB
的值进行计算。
2.2在组件中使用计算属性:
在组件中,可以像访问普通数据属性一样访问计算属性。由于已经在 Store 中定义了 getter 方法,所以当 numA
或 numB
发生变化时,sum
的值也会自动更新。
import { observer, inject } from 'mobx-miniprogram-bindings';
const myBehavior = inject('store')(observer);
Component({
behaviors: [myBehavior],
methods: {
onTapButton() {
console.log(this.data.sum); // 输出 numA 和 numB 的和
this.addNumA(1); // 调用 action 更新 numA
console.log(this.data.sum); // 输出更新后的 numA 和 numB 的和
},
},
});
在上面的例子中,onTapButton
方法首先输出了 sum
的当前值,然后调用了 addNumA
方法来增加 numA
的值。由于 sum
是一个计算属性,它会自动重新计算并反映 numA
的新值。因此,第二次调用 console.log(this.data.sum);
会输出更新后的和。
2.3监听器 `watch`
在使用时:**<font color="red">需要将 `Component` 方法替换成 `ComponentWithComputed` 方法 </font>**,原本组件配置项也需要写到该方法中在替换以后,就可以新增 `computed` 以及 `watch` 配置项。
// 引入 miniprogram-computed
import { ComponentWithComputed } from 'miniprogram-computed'
ComponentWithComputed({
data: {
a: 1,
b: 1
},
computed: {
total(data) {
// 注意:
// computed 函数中不能访问 this ,只有 data 对象可供访问
// 这个函数的返回值会被设置到 this.data.sum 字段中
return data.a + data.b
}
}
watch: {
// 同时对 a 和 b 进行监听
'a, b': function (a, b) {
this.setData({
total: a + b
})
}
},
methods: {
updateData() {
this.setData({
a: this.data.a + 1,
b: this.data.b + 1
})
}
}
})
3.用户管理
3.1 用户登录-什么是 token
- 定义与功能:
- Token是在微信小程序中进行API接口调用的一种凭据。
- 它类似于身份认证,允许开发者在获取到Token后,使用该Token进行小程序的接口调用,以实现各种功能。
- 获取方式:
- Token的获取通常通过与微信服务器进行交互完成。具体来说,微信小程序登录采用了微信开放平台的登录接口,通过该接口可以实现用户的快速登录,并获取到用户的Token。
- 在微信小程序中,用户的登录信息会被加密存储在微信的服务器中,而Token则用于后续的接口调用验证。
3.2 用户登录-小程序登录流程介绍
业务介绍: 传统的登录功能,需要用户先注册,注册完成以后,使用注册的账号、密码进行登录。小程序的登录操作则比较简单,小程序可以通过微信提供的登录能力,便捷地获取微信提供的用户身份标识进行登录。免去了注册和输入账号密码的步骤,从而提高了用户体验。 登录流程说明: 1. 用户访问小程序,点击 [登录] ,调用 `wx.login()` 方法获取 **临时登录凭证code** 临时登录凭证 code,就像是一个会过期的临时身份证一样,有效时间仅为 5分钟
2. 使用 `wx.request()` 方法将 **临时登录凭证code** 传递给开发者服务器,方便后续可以换取微信用户身份 id
3. 开发者的后台接收 **临时登录凭证code**,同时在微信公众后台拿到 `AppId` 和 `AppSecret` ,向微信服务器发送请求, 请求参数合法的话,微信服务器会给开发者后台返回 openid(微信用户的唯一标识) 以及 session_key(会话密钥) 等。
openid 是微信用户的唯一标识,也就是微信用户身份 id,可以用这个 id 来区分不同的微信用户
session_key 则是微信服务器给开发者服务器颁发的身份凭证,开发者可以用session_key请求微信服务器其他接口来获取一些其他信息
4. 开发者后台在接收到微信服务器返回的数据以后,会执行一些业务逻辑的处理,例如:将用户标识和其他信息进行加密处理,生成自定义登录态,这个登录态可以理解为就是 `Token` ,然后让 `Token` 与 `openid` 和 `session_key` 进行关联
5. 开发者后台处理好逻辑后,会将 自定义登录态 `Token` 返回给微信小程序客户端,客户端收到 `token` 以后,将其存储起来,比如放在 `localStorage` 中。
6. 客户端每次向开发者后台发送请求的时候,需要携带自定义登录态 `Token` ,开发者后台收到请求后,对 `Token` 进行验证识别用户身份,同时拿自定义登录态 `Token` 查询 `openid` 和 `session_key`,从而获取用户请求的数据,进行返回。
3.3 用户登录-实现小程序登录功
实现步骤:
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`
import http from '../utils/http'
/**
* @description 授权登录
* @param {*} code 临时登录凭证code
* @returns Promise
*/
export const reqLogin = (code) => {
return http.get(`/mall-api/weixin/wxLogin/${code}`)
}
/pages/login/login.js
import { reqLogin } from '../../api/user'
import { toast } from '../../utils/extendApi'
Page({
// 点击登录
login() {
// 调用 wx.login 获取用户信息
wx.login({
success: async ({ code }) => {
if (code) {
// 调用接口 API,传入 code 进行登录
const res = await reqLogin(code)
// 登录成功以后将 token 存储到本地
wx.setStorageSync('token', res.data.token)
// 返回之前的页面
wx.navigateBack()
} else {
// 登录失败后给用户进行提示
toast({ title: '授权失败,请稍后再试~~~' })
}
}
})
}
})
3.4用户登录-token 存储到 Store
思路分析: `Mobx`允许开发人员在应用程序中统一管理所有组件之间的公共数据。通过使用 `Mobx`,开发人员可以轻松地将 token 存储到全局状态中,并实现在整个应用程序中的共享。并且,存储到`Mobx`中的数据是响应式的,数据发生了变化,使用的地方也会发生变化 首先我们先安装`Mobx`,然后进行实例化,在实例化的时候,创建共享的数据 `Token`,以及对 `Token` 修改的方法 然后使用 `Component` 构造页面,并导入`ComponentWithStore` 方法,并配置 `storeBindings` 方法让页面和 `Store` 对象关联
实现步骤: 1. 安装`Mobx`两个包,在安装好包以后,对包进行构建,点击 `构建 npm` 2. 在项目的根目录下创建 `store` 文件夹,然后在该文件夹下新建 `userstore.js` 3. 导入核心的`observable ` 、`action` 方法,创建`Store`,同时声明数据和方法 4. 在登录页面,导入`ComponentWithStore` 方法,并配置 `storeBindings` 方法让页面和 `Store` 对象关联
落地代码: 安装依赖,安装完成后构建 npm
npm i mobx-miniprogram mobx-miniprogram-bindings
/store/index.js`
```js
// 导入 observable 函数用于创建可观察对象
// 导入 action 修改 store 中的可观察状态
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'
// 创建 store 对象,存储应用的状态
export const userStore = observable({
// 创建可观察状态 token
token: getStorage('token') || '',
// 对 token 进行修改
setToken: action(function (token) {
this.token = token
})
})
import { reqLogin } from '../../api/user'
import { userStore } from '../../api/userstore'
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
ComponentWithStore({
storeBindings: {
store: userStore,
fields: ['token'],
actions: ['setToken']
}
methods: {
// 授权登录
login() {
// 使用 wx.login 获取用户的临时登录凭证 code
wx.login({
success: async ({ code }) => {
if (code) {
// 在获取到临时登录凭证 code 以后,需要传递给开发者服务器
const { data } = await reqLogin(code)
// 登录成功以后,需要将服务器响应的自定义登录态存储到本地
setStorage('token', data.token)
// 将数据存储到 store 对象中
this.setToken(data.token)
} else {
toast({ title: '授权失败,请重新授权' })
}
}
})
}
}
})
3.5用户信息-用户信息存储到 Store
思路分析: 需要调用接口获取用户的信息,在获取到数据以后,需要存储用户信息数据到本地, 用户信息可能会在多个地方使用到,为了方便对用户信息的获取和使用,依然将用户信息存储到`store` 首先在 `store/index.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`
/**
* @description 获取用户信息
* @returns Promise
*/
export const reqUserInfo = () => {
return http.get(`/mall-api/weixin/getuserInfo`)
}
/store/userstore.js`
// 导入 observable 函数用于创建可观察对象
// 导入 action 修改 store 中的可观察状态
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'
// 创建 store 对象,存储应用的状态
export const userStore = observable({
// 创建可观察状态 token
// token,登录令牌
token: getStorage('token') || '',
// 用户信息
userInfo: wx.getStorageSync('userInfo') || {},
// 对 token 进行修改
setToken: action(function (token) {
this.token = token
}),
// 设置用户信息
setUserInfo: action(function (userInfo) {
this.userInfo = userInfo
})
})
// pages/login/login.js
// 导入封装通用模块方法
import { toast } from '../../utils/extendApi'
// 导入本地存储 api
import { setStorage } from '../../utils/storage'
// 导入接口 API 函数
+ import { reqLogin, reqUserInfo } from '../../api/user'
// 导入 ComponentWithStore 方法
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
// 导入 store 对象
import { userStore } from '../../stores/userstore'
// 使用 ComponentWithStore 方法替换 Component 方法构造页面
ComponentWithStore({
// 让页面和 Store 对象建立关联
storeBindings: {
store: userStore,
fields: ['token', 'userInfo'],
actions: ['setToken', 'setUserInfo']
},
methods: {
// 授权登录
login() {
// 使用 wx.login 获取用户的临时登录凭证 code
wx.login({
success: async ({ code }) => {
if (code) {
// 在获取到临时登录凭证 code 以后,需要传递给开发者服务器
const { data } = await reqLogin(code)
// 登录成功以后,需要将服务器响应的自定义登录态存储到本地
setStorage('token', data.token)
// 将自定义登录态 token 存储到 Store 对象
this.setToken(data.token)
// 获取用户信息
this.getUserInfo()
} else {
toast({ title: '授权失败,请重新授权' })
}
}
})
},
// 获取用户信息
async getUserInfo() {
const { data } = await reqUserInfo()
// 将用户信息存储到本地
setStorage('userInfo', data)
// 将用户信息存储到 Store
this.setUserInfo(data)
}
}
})
3.6用户信息-使用数据渲染用户信息
思路分析: 在获取到数据以后,已经将用户信息数据存储到本地和`Store`,从 `Store` 中取出用户信息数据,并渲染到页面上 个人中心页面展示用于展示个人信息 如果用户没有登录的时候,展示没有登录的头像、提示用户登录的文案信息,不展示设置按钮 如果用户已经登录,展示用户的头像和昵称,并且展示设置按钮,方便用户对收货地址、头像、昵称进行更改 实现步骤: 1. 在个人中心页面导入`ComponentWithStore` 方法构建页面 2. 配置 `storeBindings` 让组件和 `Store` 建立关联 3. 渲染页面
落地代码:
/pages/info/info.js`
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
ComponentWithStore({
storeBindings: {
store: userStore,
fields: ['token', 'userInfo']
}
})
3.7分包处理-配置分包以及预下载
思路分析: 随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。 因此需要将 `更新个人资料` 和 `收货地址` 功能配置成一个分包, 当用户在访问设置页面时,还预先加载 `更新个人资料` 和 `收货地址` 所在的分包
实现步骤: 1. 在 `app.json` 新增 `subpackages` 进行分包配置,新增 `preloadRule` 进行分包预下载配置 2. 在 `subpackages` 设置分包的 根目录 `root` 、别名 `name` 、页面路径 `pages` 3. 在 `preloadRule` 设置预下载。
落地代码:
"subpackages": [
{
"root": "modules/settingModule",
"name": "settingModule",
"pages": [
"pages/address/add/index",
"pages/address/list/index",
"pages/profile/profile"
]
}
],
"preloadRule": {
"pages/settings/settings": {
"network": "all",
"packages": ["settingModule"]
}
}
4. 收货地址
4.1 定义新增参数以及封装接口 API
思路分析: 点击新建地址按钮,需要跳转到新增地址页面 因为新增和编辑收货地址页面是同一个页面,我们需要在这个页面处理新增和编辑功能,为了做区分处理。 我们在后续做进行编辑的时候传递 `id` 属性,值为 收货地址的 `id` 值。
实现步骤: 1. 在新增收货地址页面 `data` 中声明所需要的字段 2. 定义收货地址所需要的全部接口 `API` 函数
落地代码:
modules/settingModule/pages/address/add/index`
Page{{
// 页面的初始数据
data: {
name: '', // 收货人
phone: '', // 手机号
provinceName: '', // 省
provinceCode: '', // 省 编码
cityName: '', // 市
cityCode: '', // 市 编码
districtName: '', // 区
districtCode: '', // 区 编码
address: '', // 详细地址
fullAddress: '', // 完整地址 (省 + 市 + 区 + 详细地址)
isDefault: 0 // 设置默认地址,是否默认地址 → 0:否 1:是
}
}}
/api/address`
import http from '../utils/http'
/**
* @description 实现新增收货地址
* @param {*} data
* @returns Promise
*/
export const reqAddAddress = (data) => {
return http.post('/userAddress/save', data)
}
/**
* @description 获取收货地址列表
* @returns Promise
*/
export const reqAddressList = () => {
return http.get('/userAddress/findUserAddress')
}
/**
* @description 获取收货地址详情
* @param {*} id 收货地址id
* @returns Promise
*/
export const reqAddressInfo = (id) => {
return http.get(`/userAddress/${id}`)
}
/**
* @description 编辑收货地址
* @param {*} data
* @returns Promise
*/
export const reqUpdateAddress = (data) => {
return http.post('/userAddress/update', data)
}
/**
* @description 删除收货地址
* @param {*} id 收货地址 id
* @returns Promise
*/
export const reqDelAddress = (id) => {
return instance.get(`/userAddress/delete/${id}`)
}
4.2 收集省市区数据
思路分析: 省市区的结构使用了小程序本身自带的`picker` 件,并将组件的 `mode` 属性设置为了 `region`,从而变成省市区选择器 如果想获取省市区的数据,需要给 `picker` 选择组件添加`change` 事件来监听属性值的改变,获取选中的省市区
<!-- 省市县 -->
<view class="item">
<text class="label">省/市/县 (区)</text>
<!-- mode:给组件添加 mode 属性设置为了 region,从而变成省市区选择器 -->
<!-- value:要求是一个数组,表示选中的省市区,默认选中每一列的第一个值 -->
<!-- bindchange:来监听属性值的改变,也就是获取选中的省市区 -->
<picker
mode="region"
value="{{ [provinceName, cityName, districtName] }}"
bindchange="onAddressChange"
>
<view wx:if="{{ provinceName }}" class="region">
{{ provinceName + ' ' + cityName + ' ' + districtName }}
</view>
<view wx:else class="placeholder">请填写收货人所在城市</view>
</picker>
<view class="location" bindtap="onLocation">
<van-icon name="location-o" color="#777" />
<text>定位</text>
</view>
</view>
4.3 收集新增地址其他请求参数
思路分析: 使用简易双向数据 `model:value` 绑定来收集新增地址表单数据。 在将数据收集以后,需要组织两个数据: 1. 是否是默认地址,0 不设置为默认地址,1 设置为默认地址 2. 拼接完整的收货地址
实现步骤: 1. 使用简易双向数据绑定来收集新增地址表单数据。 3. 给按钮绑定点击事件,在事件处理函数中收集并整理数据
落地代码:
Page({
saveAddrssForm(event) {
// 解构出省市区以及 是否是默认地址
const { provinceName, cityName, districtName, address, isDefault } = this.data
// 拼接完整的地址
const fullAddress = provinceName + cityName + districtName + address
// 合并接口请求参数
const params = {
...this.data,
fullAddress,
isDefault: isDefault ? 1 : 0
}
console.log(params)
}
})
4.4 地理定位功能介绍
地理定位介绍: 小程序地理定位是指通过小程序开发平台提供的 `API`,来获取用户的地理位置信息。用户在使用小程序时,可以授权小程序获取自己的地理位置信息 1. `wx.getLocation()` :获取当前的地理位置 2. `wx.chooseLocation()`:打开地图选择位置
申请开通: 暂时只对部分类目的小程序开放,需要先通过类目审核,然后在小程序管理后台,「开发」-「开发管理」-「接口设置」中自助开通该接口权限。
使用方法:
1. 在 app.json 中配置 `requiredPrivateInfos` 进行声明启用
2. 在调用 `wx.getLocation()` 时需要在 app.json 配置 `permission `字段,同时使用 `scope.userLocation` 声明**收集用户选择的位置信息**的目的,`wx.chooseLocation()` 接口不需要配置该字段,可以直接进行调用
3. 在配置好以后,调用 `wx.getLocation()` 和 `wx.chooseLocation()` 接口
在 `app.json` 中进行配置
{
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
],
"permission": {
"scope.userLocation": {
"desc": "获取用户位置信息用于填写收货地址"
}
}
}
4.5 拒绝授权后的解决方案
async onLocation() {
const { authSetting } = await wx.getSetting()
console.log(authSetting)
const isAuth = !!authSetting['scope.userLocation']
if (!isAuth) {
const modalRes = await wx.modal({
title: '授权提示',
content: '需要需要您的地理位置信息,请确认授权'
})
// 如果用户点击了取消,说明用户拒绝了授权,给用户提示
if (!modalRes) return wx.toast({ title: '您拒绝了授权' })
// 如果用户点击了确定,调用 wx.openSetting 打开微信客户端小程序授权页面
// 并返回授权以后的结果
const { authSetting } = await wx.openSetting()
// 如果用户没有更新授权信息,提示没有更新授权
if (!authSetting['scope.userLocation'])
return wx.toast({ title: '授权失败!' })
try {
const locationRes = await wx.getLocation()
// 打印地理位置信息
console.log(locationRes)
} catch (err) {
console.log(err)
}
} else {
try {
const locationRes = await wx.getLocation()
console.log(locationRes)
} catch (error) {
wx.toast({ title: '您拒绝授权获取地址位置' })
}
}
}
4.6 开通腾讯位置服务
1. 申请密钥:[密钥申请](https://lbs.qq.com/dev/console/application/mine),微信扫码进行登录,选择绑定已有账号、或者注册新账号 (需要绑定手机、验证邮箱) 2. 控制台 → 应用管理→[我的应用](https://lbs.qq.com/dev/console/key/manage) → 创建应用 → 添加 key → 创建完成 <img src="http://8.131.91.46:6677/mina/floor/创建应用.jpg" style="zoom:60%;" /> <img src="http://8.131.91.46:6677/mina/floor/添加 key.jpg" style="zoom:76%; border: 1px solid #ccc" /> 3. 下载微信小程序 [JavaScriptSDK v1.2](https://mapapi.qq.com/web/miniprogram/JSSDK/qqmap-wx-jssdk1.2.zip),下载将 `.js` 文件放到小程序的 `libs` 目录下 4. 进行安全域名设置,或者点击微信开发者工具中的暂时不校验域名
4.7 LBS 逆地址解析
使用步骤: 1. 在项目中引入 SDK 核心类 2. 在 `onLoad` 中实例化 API 核心类,同时配置创建的 key 3. 使用实例方法 `reverseGeocoder` 方法进行逆地址解析,将提供的坐标转换为详细的地址位置信息
落地代码: 1. 引入 SDK 核心类 2. 实例化 API 核心类 3. 使用 `reverseGeocoder` 方法进行逆地址解析,将提供的坐标转换为所在位置的文字描述的转换
4.8 async-validator 基本使用
知识点: `async-validator`是一个基于 `JavaScript` 的表单验证库,支持异步验证规则和自定义验证规则 主流的 `UI` 组件库 `Ant-design` 和 `Element`中的表单验证都是基于 `async-validator` 使用 `async-validator` 可以方便地构建表单验证逻辑,使得错误提示信息更加友好和灵活。 使用步骤: 1. 安装并在项目中导入 `async-validator` 2. 创建验证规则 3. 创建表单验证实例,将验证规则传递给构造函数,产生实例 4. 调用实例方法 `validate` 对数据进行验证 - 第一个参数:需要验证的数据 - 第二个参数:回调函数,回调函数有两个参数 errors, fields - errors:如果验证成功,返回 null,验证错误,返回数组 - fields:需要验证的字段,属性值错误数组
5.商品管理
5.1 配置商品管理分包
思路分析: 随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。 因此我们需要将 `商品列表` 和 `商品详情` 功能配置成一个分包, 当用户在访问设置页面时,还预先加载 `商品列表` 和 `商品详情` 所在的分包
落地代码:
{
"subPackages": [
{
"root": "modules/settingModule",
"name": "settingModule",
"pages": [
"pages/address/add/index",
"pages/address/list/index",
"pages/profile/profile"
]
},
{
"root": "modules/goodModule",
"name": "goodModule",
"pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
}
],
"preloadRule": {
"pages/settings/settings": {
"network": "all",
"packages": ["settingModule"]
},
"pages/category/category": {
"network": "all",
"packages": ["goodModule"]
}
}
}
5.2 封装商品模块接口 API
import http from '../utils/http'
/**
* @description 获取商品列表
* @return Promise
*/
export const reqGoodsList = ({ limit, page, ...reset }) => {
return http.get(`/mall-api/goods/list/${page}/${limit}`, reset)
}
/**
* @description 获取商品详情
* @param {*} goodsId 商品Id
* @returns Promise
*/
export const reqGoodsInfo = (goodsId) => {
return http.get(`/mall-api/goods/${goodsId}`)
}
5.3 商品列表-准备列表请求参数
实现步骤: 1. 在商品列表的 `data` 字段中,根据接口文档,定义商品列表接口需要使用的字段 2. 在商品列表的 `onLoad` 钩子函数中接收请求的参数,并将请求参数进行合并
落地代码:
/modules/goodsModule/pages/list/list.js`
Page({
// 页面的初始数据
data: {
goodsList: [], // 商品列表数据
isFinish: false, // 判断数据是否加载完毕
// 接口请求参数
requestData: {
page: 1, // 页码
limit: 10, // 每页请求多少条数据
category1Id: '', // 一级分类 id
category2Id: '' // 二级分类 id
}
},
// 生命周期函数--监听页面加载
onLoad(options) {
// 接收传递的参数
Object.assign(this.data.requestData, options)
}
})
5.4 商品列表-获取商品列表数据并渲染
思路分析: 在准备商品列表的请求参数以后, 在页面调用 `API` 函数获取商品列表的数据,在获取到数据以后,使用后端返回的数据对页面进行渲染。 实现步骤: 1. 在 `/pages/goods/list/list.js` 中导入封装好的获取商品列表的 `API` 函数 2. 页面数据在页面加载的时候进行调用,在 `onLoad` 钩子函数中调用 `reqGoodsList` 方法 3. 在获取到数据以后,使用后端返回的数据对页面进行渲染
/modules/goodsModules/pages/list/list.js`
import { reqGoodsList } from '../../../../../api/good'
Page({
// 页面的初始数据
data: {
goodsList: [], // 商品列表数据
total: 0, // 数据总条数
isFinish: false, // 判断数据是否加载完毕
// 接口请求参数
requestData: {
page: 1, // 页码
limit: 10, // 每页请求多少条数据
category1Id: '', // 一级分类 id
category2Id: '' // 二级分类 id
}
},
// 获取商品列表的数据
async getGoodsList() {
// 调用 API 获取数据
const { data } = await reqGoodsList(this.data.requestData)
// 将返回的数据赋值给 data 中的变量
this.setData({
goodsList: data.records,
total: data.total
})
},
// 生命周期函数--监听页面加载
onLoad(options) {
// 接收传递的参数
Object.assign(this.data.requestData, options)
// 获取商品列表的数据
this.getGoodsList()
}
})
5.5 商品列表-实现上拉加载更多功能
思路分析: 当用户从下向上滑动屏幕时,需要加载更多的商品数据。 首先需要在 `.js` 文件中声明 `onReachBottom` 方法监听用户是否进行了上拉 当用户上拉时,需要对 `page` 页码进行加 1,代表要请求下一页的数据 当参数发生改变后,需要重新发送请求,拿最新的 `page` 向服务器发送请求获取数据。 在下一页的商品数据返回以后,将最新的数据和之前的数据进行合并 实现步骤: 1. `list.js` 文件中声明 `onReachBottom` 事件处理函数,监听用户的上拉行为 2. 在 `onReachBottom` 函数中加 `page` 进行加 1 的操作,同时发送请求获取下一页数据 3. 在 `getGoodsList` 函数中,实现参数的合并
/modules/goodsModule/pages/list/list.js`
import { reqGoodsList } from '../../../api/goods'
Page({
// coding...
// 获取商品列表的数据
async getGoodsList() {
// 调用 API 获取数据
const { data } = await reqGoodsList(this.data.params)
// 将返回的数据赋值给 data 中的变量
this.setData({
goodsList: [...this.data.goodsList, ...data.records],
total: data.total
})
},
// coding...
// 监听页面的上拉操作
onReachBottom() {
let { page } = this.data.requestData
// 页码 + 1
this.setData({
requestData: { ...this.data.requestData, page: page + 1 }
})
// 重新发送请求
this.getGoodsList()
}
})
5.6 商品列表-判断数据是否加载完毕
实现步骤: 1. 在数据返回以后,将数据中的 `total` 赋值给 `data` 中的变量 `total` 2. 在 `onReachBottom` 中进行 `total` 和 `goodsList` 进行对比 3. 模板中使用 `total` 和 `goodsList` 进行对比
import { reqGoodsList } from '../../../api/goods'
Page({
// coding...
// 监听页面的上拉操作
onReachBottom() {
// 从 data 中解构数据
const { total, goodsList, requestData } = this.data
let { page } = requestData
// 判断数据是否加载完毕
if (total === goodsList.length) {
// 如果相等,数据数据加载完毕
// 如果数据加载完毕,需要给用户提示,同时不继续加载下一个数据
this.setData({
isFinish: true
})
return
}
// 页码 + 1
this.setData({
requestData: { ...this.data.requestData, page: (page += 1) }
})
// 重新发送请求
this.getGoodsList()
}
})
5.7 商品列表-节流阀进行列表节流
在用户网速很慢的情况下,如果用户在距离底部来回的进行多次滑动,可能会发送一些无意义的请求、造成请求浪费的情况,因此需要给上拉加载添加节流功能。 我们使用节流阀来给商品列表添加节流功能。 在 `data` 中定义节流阀状态 `isLoading`,默认值是 `false`。 在请求发送之前,将 `isLoading` 设置为 `true`,表示请求正在发送。 在请求结束以后,将 `isLoading` 设置为 `false`,表示请求已经完成。 在 `onReachBottom` 事件监听函数中,对 `isLoading` 进行判断,如果数据正在请求中,不请求下一页的数据
/modules/goodsModule/pages/list/list.js`
import { reqGoodsList } from '../../../../../api/good'
Page({
// 页面的初始数据
data: {
goodsList: [], // 商品列表数据
isFinish: false, // 判断数据是否加载完毕
isLoading: false, // 判断数据是否记载完毕
total: 0, // 列表总数据量
// 接口请求参数
requestData: {
page: 1, // 页码
limit: 10, // 每页请求多少条数据
category1Id: '', // 一级分类 id
category2Id: '' // 二级分类 id
}
},
// 获取商品列表的数据
async getGoodsList() {
// 数据真正请求中
this.data.isLoading = true
// 调用 API 获取数据
const { data } = await reqGoodsList(this.data.requestData)
// 数据加载完毕
this.data.isLoading = false
// 将返回的数据赋值给 data 中的变量
this.setData({
goodsList: [...this.data.goodsList, ...data.records],
total: data.total
})
},
// 监听页面的上拉操作
onReachBottom() {
// 从 data 中解构数据
const { total, goodsList, requestData, isLoading } = this.data
let { page } = requestData
// 判断是否加载完毕,如果 isLoading 等于 true
// 说明数据还没有加载完毕,不加载下一页数据
if (isLoading) return
// 判断数据是否加载完毕
// coding...
}
})
5.8商品列表-实现下拉刷新功能
下拉刷新是小程序中常见的一种刷新方式,当用户下拉页面时,页面会自动刷新,以便用户获取最新的内容。 小程序中实现上拉加载更多的方式: 1. 在`页面.json` 中开启允许下拉,同时可以配置 窗口、loading 样式等 2. 在`页面.js` 中定义 `onPullDownRefresh` 事件监听用户下拉刷新
/modules/goodsModule/pages/list/list.json`
{
"usingComponents": {
"goods-card": "/components/goods-card/goods-card"
},
"navigationBarTitleText": "商品列表",
"enablePullDownRefresh": true,
"backgroundColor": "#f7f4f8",
"backgroundTextStyle": "dark"
}
/modules/goodsModule/pages/list/list.js`
// 监听页面的下拉刷新
onPullDownRefresh() {
// 将数据进行重置
this.setData({
goodsList: [],
total: 0,
isFinish: false,
requestData: { ...this.data.requestData, page: 1 }
})
// 重新获取列表数据
this.getGoodsList()
}
6. 购物车
6.1 购物车-封装购物车接口 API
import http from '../utils/http'
/**
* @description 获取购物车列表数据
* @returns Promise
*/
export const reqCartList = () => {
return http.get('/mall-api/cart/getCartList')
}
/**
* @description 加入购物车
* @param {*} data
* @returns Promise
*/
export const reqAddCart = (data) => {
return http.get(`/cart/addToCart/${data.goodsId}/${data.count}`, data)
}
/**
* @description 更新商品的选中状态
* @param {*} goodsId 商品 id
* @param {*} isChecked 商品的选中状态
* @returns Promise
*/
export const reqUpdateChecked = (goodsId, isChecked) => {
return http.get(`/cart/checkCart/${goodsId}/${isChecked}`)
}
/**
* @description 全选和全不选
* @param {*} isChecked 商品的选中状态
* @returns Promise
*/
export const reqCheckAllCart = (isChecked) => {
return http.get(`/cart/checkAllCart/${isChecked}`)
}
/**
* @description 删除购物车商品
* @param {*} goodsId 商品 id
* @returns Promise
*/
export const reqDelCart = (goodsId) => {
return http.get(`/cart/delete/${goodsId}`)
}
6.2 加入购物车-模板分析和渲染
业务介绍: 点击加入购物车和立即购买的时候,展示购物弹框,在弹框中需要用户选择购买数量和祝福语 点击加入购物车和立即购买,触发的是同一个弹框。 因此点击弹框中的确定按钮时,我们需要区分当前是加入购物车操作还是立即购买操作。 这时候定义一个状态 `buyNow` 做区分,`buyNow` 等于 1 代表是立即购买,否则是加入购物车 产品需求: 1. 如果点击的是加入购物车,需要将当前商品加入到购物车 2. 如果点击的是立即购买,需要跳转到结算支付页面,立即购买该商品 3. 如果是立即购买,不支持购买多个商品
6.3 加入购物车-关联 Store 对象
思路分析: 当用户点击加入购物车 或者 立即购买时,需要判断用户是否进行了登录。 我们需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。 这时候可以使用 `BehaviorWithStore` 让页面 和 `Store` 对象建立关联。
/behaviors/userBehavior.js`
// 导入 BehaviorWithStore 让页面和 Store 对象建立关联
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
// 导入用户 Store
import { userStore } from '@/stores/userstore'
export const userBehavior = BehaviorWithStore({
storeBindings: {
store: userStore,
fields: ['token']
}
})
/behaviors/userBehavior.js`
import { reqGoodsInfo } from '@/api/goods'
import { reqAddCart } from '@/api/cart'
+ import { userBehavior } from '@/behaviors/userBehavior'
Page({
behaviors: [userBehavior],
// 代码略...
})
6.4 加入购物车和立即购买区分处理
思路分析: 点击加入购物车以及立即购买以后,需要先判断是否进行了登录,如果用户没有登录过,需要先跳转到登录页面进行登录。 如果点击的是 加入购物车,我们只需要调用 [加入购物车](https://apifox.com/apidoc/shared-6ed6c5c4-56c4-4619-8e2a-4817aa140e30/api-136581098) 接口即可 (需要获取商品的 ID 、购买数量、祝福语) 如果点击的是 立即购买,我们需要携带参数跳转到商品结算页面 (获取商品的 ID 以及 祝福语跳转到结算页面) 购买数量的限制有 4 个限制,这 4 个限制直接使用 `Vant` 组件提供的属性进行限制即可: 1. 必须是正整数,最小是`1`,最大是`200` 2. 若输入小于`1`,则重置为`1` 3. 若输入大于`200`,则重置为`200` 4. 若输入的是其他值,则重置为`1` 实现步骤: 1. 给 `Stepper` 步进器组件,通过`value`设置输入值,同时绑定`change`事件,并将值同步到 `data` 中 2. 根据接口文档,导入封装的购物车的接口 API 3. 点击弹框按钮的时候,判断点击的加入购物车还是立即购买,执行不同的操作
/modules/goodsModule/pages/detail/detail.html`
<van-stepper
value="{{ count }}"
+ integer
+ min="1"
+ max="200"
bind:change="onChangeGoodsCount"
/>
/modules/goodsModule/pages/detail/detail.js`
// 监听是否更改了购买数量
onChangeGoodsCount(event) {
// 将最新的购买数量同步到 data
this.setData({
count: Number(event.detail)
})
},
// 弹框的确定按钮
async handleSubmit() {
// 解构获取数据
const { token, count, blessing, buyNow } = this.data
const goodsId = this.goodsId
// 如果没有 token ,让用户新登录
if (!this.data.token) {
wx.navigateTo({
url: '/pages/login/login'
})
return
}
// 将用户输入的值转成 Number 类型
const count = Number(event.detail)
// 验证购买数量的正则
const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
// 使用正则验证
const res = reg.test(count)
// 如果验证没有通过,直接返回,不执行后续的逻辑
if (!res) return
// 加入购物车
if (buyNow === 0) {
// 加入购物车
const res = await reqAddGood({ goodsId, count, blessing })
if (res.code === 200) {
wx.showToast({
title: '加入购物车成功'
})
this.setData({
show: false
})
}
} else {
// 立即购买
wx.navigateTo({
url: `/pages/order/detail/index?goodsId=${goodsId}&blessing=${blessing}`
})
}
}
6.5 加入购物车-展示购物车购买数量
思路分析: 判断用户是否进行了登录。 如果没有登录过,则不展示购物车商品的数量。 如果用户登录过,则需要展示购物车商品的数量,则获取购物车列表数据,通过累加计算得出商品购买数量 实现步骤: 1. 进入商品详情,调用方法,在方法中判断`token`是否存在 2. 如何存在,则获取购物车列表数据,通过累加计算得出商品购买数量,展示购买的数量 3. 不存在,不执行任何逻辑,
/modules/goodsModule/pages/detail/detail.js`
Page({
data: {
allCount: ''
},
async handleSubmit() {
if (!this.data.token) {
wx.navigateTo({
url: '/pages/login/login'
})
return
}
const { count, blessing, allCount } = this.data
const goodsId = this.goodsId
if (this.data.buyNow === 0) {
const res = await reqAddCart({ goodsId, count, blessing })
if (res.code === 200) {
wx.toast({
title: '加入购物车成功',
icon: 'success',
mask: false
})
this.getCartCount()
this.setData({
show: false
})
}
} else {
wx.navigateTo({
url: `/pages/order/detail/detail?goodsId=${goodsId}&blessing=${blessing}`
})
}
},
async getCartCount() {
if (!this.data.token) return
const res = await reqCartList()
if (res.data.length !== 0) {
let allCount = 0
res.data.forEach((item) => {
allCount += item.count
})
this.setData({
allCount: (allCount > 99 ? '99+' : allCount) + ''
})
}
},
onLoad(options) {
this.goodsId = options.goodsId
this.getGoodsInfo()
this.getCartCount()
}
// coding...
})
6.6 购物车-购物车关联 Store 对象
思路分析: 当用户进入购物车页面时时,需要判断用户是否进行了登录来控制页面的展示效果 这时候我们就需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。 因为购物车页面采用的 `Component` 方法进行构建 这时候可以使用 `ComponentWithStore` 让页面 和 `Store` 对象建立关联。
/pages/cart/components/cart.js`
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList } from '@/api/cart'
ComponentWithStore({
storeBindings: {
store: userStore,
fields: ['token']
},
// 组件的初始数据
data: {
cartList: [],
emptyDes: '还没有添加商品,快去添加吧~'
},
// 组件的方法列表
methods: {
// 处理页面的展示
async showTipList() {
// 将 token 进行解构
const { token } = this.data
console.log(token)
},
onShow() {
this.showTipList()
}
}
})
7.结算支付
7.1 配置分包并跳转到结算页面
思路分析:
随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。
因此我们需要将 `结算支付` 功能配置成一个分包,
当用户在访问设置页面时,还预先加载 `结算支付` 所在的分包
app.json`
"subPackages": [
{
"root": "modules/settingModule",
"name": "settingModule",
"pages": [
"pages/address/add/index",
"pages/address/list/index",
"pages/profile/profile"
]
},
{
"root": "modules/goodModule",
"name": "goodModule",
"pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
},
{
"root": "modules/orderPayModule",
"name": "orderPayModule",
"pages": [
"pages/order/detail/detail",
"pages/order/list/list"
]
}
],
"preloadRule": {
"pages/settings/settings": {
"network": "all",
"packages": ["settingModule"]
},
"pages/category/category": {
"network": "all",
"packages": ["goodModule"]
},
"pages/cart/cart": {
"network": "all",
"packages": ["orderPayModule"]
}
}
7.2 封装结算支付的接口 API
/api/orderpay.js`
import http from '@/utils/http'
/**
* @description 获取订单详情
* @returns Promise
*/
export const reqOrderInfo = () => {
return http.get('/order/trade')
}
/**
* @description 获取订单列表
* @param {*} page 页码
* @param {*} limit 每页展示的条数
* @returns Promise
*/
export const reqOrderList = (page, limit) => {
return http.get(`/order/order/${page}/${limit}`)
}
/**
* @description 获取订单收货地址
* @returns Promise
*/
export const reqOrderAddress = () => {
return http.get('/userAddress/getOrderAddress')
}
/**
* @description 获取立即购买商品的详情信息
* @param { Object } params { goodsId: 商品 Id, blessing:祝福语 }
* @returns Promise
*/
export const reqBuyNowGoods = ({ goodsId, ...data }) => {
return http.get(`/order/buy/${goodsId}`, data)
}
/**
* @description 提交订单
* @returns Promise
*/
export const reqSubmitOrder = () => {
return http.post('/order/submitOrder')
}
/**
* @description 获取微信预支付信息
* @param {*} orderNo 订单 ID
* @returns Promise
*/
export const reqPreBuyInfo = (orderNo) => {
return http.get(`/webChat/createJsapi/${orderNo}`)
}
/**
* @description 微信支付状态查询
* @param {*} orderNo
* @returns Promise
*/
export const reqPayStatus = (orderNo) => {
return http.get(`/webChat/queryPayStatus/${orderNo}`)
}
7.3 商品结算-获取收货地址
思路分析: 进入结算支付页面后,需要获取收货地址信息,在获取到收货地址以后,需要进行判断, 如果没有获取到收货地址,需要展示添加收货地址的结构, 如果获取到了收货地址,需要渲染收货地址。 实现步骤: 1. 在进入结算页面的时候,调用接口 `API` 函数,获取数据 2. 然后根据数据并渲染结构
/pages/order/detail/index.js`
import { getTradeAddress } from '../../../api/order'
Page({
data: {
// coding...
orderAddress: {} // 收货地址
},
// 获取收货地址
async getAddress() {
const { data: orderAddress } = await reqOrderAddress()
this.setData({
orderAddress
})
},
// 页面展示时触发的钩子函数
onShow() {
this.getAddress()
}
})
/pages/order/detail/index.wxml`
<!--pages/order/index.wxml-->
<view class="container order">
<view class="address-card">
<!-- 添加收货地址 -->
<view wx:if="{{ !tradeAddress.id }}" class="add-address" bindtap="toAddress">
<van-icon size="22px" name="add" />
<view>添加收货地址</view>
</view>
<view wx:else class="order-address flex">
<view class="address-content">
<view class="title">{{ tradeAddress.fullAddress }}</view>
<view class="info flex">
<text>{{ tradeAddress.name }}</text>
<text>{{ tradeAddress.phone }}</text>
</view>
</view>
<view class="select-address">
<navigator class="navigator" url="/modules/settingModule/pages/address/list/index">
<van-icon color="#bbb" name="arrow" size="22px" />
</navigator>
</view>
</view>
<view class="top-line"></view>
</view>
<view class="order-info">
<!-- coding... -->
</view>
</view>
7.4 商品结算-更新收货地址功能
思路分析: 当用户需要更改收货地址时,我们需要跳转到收货地址页面,重新选择收货地址 当用户点击了某个地址以后,我们需要将该地址显示到商品结算页面中。 更新收货地址功能,采用 `getApp()` 全局共享数据的方式来实现。 实现步骤: 在 `app.js` 中定义全局共享的数据 `globalData.address` 点击箭头,携带参数跳转到收货地址页面,标识是从订单结算页面进入 在选择收货地址成功以后,将数据存储到 `globalData.address`中,然后返回到订单结算页面。 在订单结算页面判断 `globalData.address` 是否存在收货地址数据,如果存在则渲染
app.js`
App({
// 定义全局共享的数据
globalData: {
address: {}
}
})
/pages/address/list/index.html`
<!-- 每一个收货地址 -->
<view
class="info"
bindtap="changeAddress"
data-id="{{ item.id }}"
>
<view class="user-info">
<text>{{ item.name }}</text>
<text>{{ item.phone }}</text>
<text wx:if="{{ item.isDefault === 1 }}" class="default-tag">默认</text>
</view>
<view class="address-info"> {{ item.fullAddress }} </view>
</view>
7.5 商品结算-获取订单详情数据
思路分析: 商品结算页面数据获取收货地址以及商品订单信息 实现步骤: 导入封装的接口 `API` 函数 在进入结算页面的时候,调用接口 `API` 函数,获取数据,然后根据数据并渲染结构即可
/pages/order/detail/index.js`
import { reqOrderAddress, reqOrderInfo } from '@/api/orderpay'
Page({
data: {
// coding...
orderAddress: {}, // 收货地址
orderInfo: {}, // 订单商品详情
},
// 获取订单详情
async getOrderInfo() {
const { data: orderInfo } = await reqOrderInfo()
// 判断是否存在祝福语
// 如果需要购买多个商品,挑选第一个填写了祝福语的商品进行赋值
const orderGoods = orderInfo.cartVoList.find((item) => item.blessing !== '')
this.setData({
orderInfo,
blessing: orderGoods && orderGoods.blessing
})
},
// 在页面展示的时候进行触发
onShow() {
// 获取收货地址
this.getAddress()
// 获取订单结算页面的商品信息
this.getOrderInfo()
},
})
/pages/order/detail/index.wxml`
<!--pages/order/index.wxml-->
<view class="container order">
<view class="address-card">
<!-- 添加收货地址 -->
<!-- coding... -->
</view>
<view class="goods-wraper">
<!-- 商品清单 -->
<view class="goods-list">
<view class="goods-item flex" wx:for="{{ tradeInfo.cartVoList }}" wx:key="goodsId">
<view class="img">
<image src="{{ item.imageUrl }}" />
</view>
<view class="content">
<view class="goods-title">{{ item.name }}</view>
<view class="goods-price">
<view class="price"> ¥ {{ item.price }}</view>
<view>x {{ item.count }}</view>
</view>
</view>
</view>
</view>
</view>
<view class="payment">
<!-- 支付方式 -->
<view class="time-wraper flex">
<image src="/static/images/payment_wxzf.png" />
<view class="title">支付方式</view>
<van-checkbox value="{{true}}"></van-checkbox>
</view>
</view>
<!-- 支付区域 -->
<view class="footer flex">
<view class="left"> ¥ {{ tradeInfo.totalAmount }} </view>
<viwe class="right">结算</viwe>
</view>
<!-- 日期选择弹框 -->
<van-popup show="{{ show }}" round position="bottom" custom-style="height: 50%" bind:close="onClose">
<van-datetime-picker type="date" min-date="{{ minDate }}" model:value="{{ currentDate }}" bind:confirm="onConfirmTimerPicker" bind:cancel="onCancelTimePicker" />
</van-popup>
</view>
7.6 商品结算-获取立即购买数据
思路分析: 当用户从商品详情点击立即购买进入商品结算页面的时候,我们需要在商品结算页面展示立即购买商品的基本信息。 在跳转到商品结算页面的时候,我们已经携带了商品的 `id` 和 `祝福语`。 在结算页面,只需要获取到传递的参数,然后根据传递的参数调用接口即可。 实现步骤: 在页面打开的时候,`onShow` 中接受传递的参数,并赋值给 `data` 中的状态 在 `getOrderInfo` 函数中,判断立即购买商品的 `id` 是否存在,如果存在调用立即购买的接口 获取数据后,然后根据数据并渲染结构即可
/pages/order/detail/index.js`
import {
reqOrderAddress,
reqOrderInfo,
reqBuyNowGoods
} from '@/api/orderpay'
Page({
// 获取订单详情
async getOrderInfo() {
// 从 data 中结构数据
const { goodsId, blessing } = this.data
// 判断是否存在商品 id,
// 如果存在调用立即购买商品详情的接口
// 不存在调用获取订单详情数据接口
const { data: orderInfo } = goodsId
? await reqBuyNowGoods({ goodsId, blessing })
: await reqOrderInfo()
// 判断是否存在祝福语
// 如果需要购买多个商品,挑选第一个填写了祝福语的商品进行赋值
const orderGoods = orderInfo.cartVoList.find((item) => item.blessing !== '')
this.setData({
orderInfo,
orderGoods && orderGoods.blessing
})
}
// 接收立即购买传递的参数
onLoad (options) {
this.setData({
...options
})
},
// 在页面展示的时候进行触发
onShow() {
// 获取收货地址
this.getAddress()
// 获取订单结算页面的商品信息
this.getOrderInfo()
}
})
8.订单列表
8.1 封装订单列表接口 `API`
api/orderpay.js`
/**
* @description 获取订单列表
* @returns Promise
*/
export const reqOrderList = (page, limit) => {
return http.get(`/order/order/${page}/${limit}`)
}
8.2 获取订单列表数据并渲染
思路分析: 当用户从个人中心页面点击进入订单中心的时候,就需要获取到订单中心的数据。 在页面调用 `API` 函数获取订单列表的数据, 在获取到数据以后,使用后端返回的数据对页面进行渲染
modules/orderPayModule/pages/order/list/list.js`
// 导入封装的接口 API 函数
import { reqOrderList } from '@/api/orderpay'
Page({
// 页面的初始数据
data: {
orderList: [1, 2, 3], // 订单列表
page: 1, // 页码
limit: 10, // 每页展示的条数
total: 0 // 订单列表总条数
},
// 获取订单列表
async getOrderList() {
// 解构获取数据
const { page, limit } = this.data
// 调用接口获取订单列表数据
const res = await reqOrderList(page, limit)
if (res.code === 200) {
this.setData({
orderList: res.data.records,
total: res.data.total
})
}
},
// 生命周期函数--监听页面加载
onLoad() {
this.getOrderList()
}
})
modules/orderPayModule/pages/order/list/list.wxml`
<!--pages/order/list/index.wxml-->
<view class="order-container container">
<view class="order-list" wx:if="{{ orderList.length > 0 }}">
<view class="order-item" wx:for="{{ orderList }}" wx:key="index">
<view class="order-item-header list-flex">
<view class="orderno">订单号<text class="no">{{ orderList.orderNo }}</text></view>
<view class="order-status {{ item.orderStatus === 1 ? 'order-active' : '' }}">
{{ item.orderStatus === 1 ? '已支付' : '未支付'}}
</view>
</view>
<view
class="goods-item list-flex"
wx:for="{{ item.orderDetailList }}"
wx:key="id"
wx:for-item="goods"
wx:for-index="goodsIndex"
>
<view class="left">
<image src="{{ goods.imageUrl }}" mode="widthFix" class="img" />
</view>
<view class="mid">
<view class="goods-name">{{ goods.name }}</view>
<view class="goods-blessing">{{ goods.blessing }}</view>
</view>
<view class="right">
<view class="goods-price">¥{{ goods.price }}</view>
<view class="goods-count">x{{ goods.count }}</view>
</view>
</view>
<view class="order-item-footer">
<view class="total-amount list-flex">
<text class="text">实付</text>
<text class="price"><text>¥</text>{{ item.totalAmount }}</text>
</view>
</view>
</view>
</view>
<van-empty wx:else description="还没有购买商品,快去购买吧~" />
</view>
8.3 订单列表上拉加载更多
思路分析: 当用户进行了上拉操作时,需要在 `.js` 文件中声明 `onReachBottom` 方法,用来监听页面的上拉触底行为 当用户上拉时,需要对 `page` 参数进行加 1 即可, 当参数发生改变后,需要重新发送请求,拿最新的 `page` 向服务器要数据 在下一页的商品数据返回以后,需要将下一页的数据和之前的数据进行合并
modules/orderPayModule/pages/order/list/list.js`
// 导入封装的接口 API 函数
import { reqOrderList } from '@/api/orderpay'
Page({
// 页面的初始数据
data: {
orderList: [1, 2, 3], // 订单列表
page: 1, // 页码
limit: 10, // 每页展示的条数
total: 0 // 订单列表总条数
},
// 获取订单列表
async getOrderList() {
// 解构获取数据
const { page, limit } = this.data
// 调用接口获取订单列表数据
const res = await reqOrderList(page, limit)
if (res.code === 200) {
this.setData({
orderList: [...this.data.orderList, ...res.data.records],
total: res.data.total
})
}
},
// 页面上拉触底事件的处理函数
onReachBottom() {
// 解构数据
const { page } = this.data
// 更新 page
this.setData({
page: page + 1
})
// 重新发送请求
this.getOrderList()
},
// 生命周期函数--监听页面加载
onLoad() {
this.getOrderList()
})
8.4 判断数据是否加载完毕
modules/orderPayModule/pages/order/list/list.js`
/ 页面上拉触底事件的处理函数
nReachBottom() {
// 解构数据
const { page, total, orderList } = this.data
// 数据总条数 和 订单列表长度进行对比
if (total === orderList.length) {
return wx.toast({ title: '数据加载完毕' })
}
// 更新 page
this.setData({
page: page + 1
})
// 重新发送请求
this.getOrderList()
8.5 节流阀
节流阀进行列表节流在用户网速很慢的情况下,如果用户在距离底部来回的进行多次滑动,可能会发送一些无意义的请求、造成请求浪费的情况,因此需要给上拉加载添加节流功能。 使用节流阀来给订单列表添加节流功能。在 `data` 中定义节流阀状态 `isLoading`,默认值是 `false`。在请求发送之前,将 `isLoading` 设置为 `true`,表示请求正在发送。在请求结束以后,将 `isLoading` 设置为 `false`,表示请求已经完成。在 `onReachBottom` 事件监听函数中,对 `isLoading` 进行判断,如果数据正在请求中,不请求下一页的数据。
modules/orderPayModule/pages/order/list/list.js`
/ 导入封装的接口 API 函数
mport { reqOrderList } from '@/api/orderpay'
age({
// 页面的初始数据
data: {
orderList: [1, 2, 3], // 订单列表
page: 1, // 页码
limit: 10, // 每页展示的条数
total: 0, // 订单列表总条数
isLoading: false // 判断数据是否记载完毕
},
// 获取订单列表
async getOrderList() {
// 解构获取数据
const { page, limit } = this.data
// 数据正在请求中
this.data.isLoading = true
// 调用接口获取订单列表数据
const res = await reqOrderList(page, limit)
// 数据加载完毕
this.data.isLoading = false
if (res.code === 200) {
this.setData({
orderList: [...this.data.orderList, ...res.data.records],
total: res.data.total
})
}
},
// 页面上拉触底事件的处理函数
onReachBottom() {
// 解构数据
const { page, total, orderList, isLoading } = this.data
// 判断是否加载完毕,如果 isLoading 等于 true
// 说明数据还没有加载完毕,不加载下一页数据
if (isLoading) return
// 数据总条数 和 订单列表长度进行对比
if (total === orderList.length) {
return wx.toast({ title: '数据加载完毕' })
}
// 更新 page
this.setData({
page: page + 1
})
// 重新发送请求
this.getOrderList()
},
// 生命周期函数--监听页面加载
onLoad() {
this.getOrderList()
}
})