主题
微信小程序授权登录业务与登录流程(Day4 2/2)
本页对应 day4,共 5 段视频,约 1 小时 47 分。
学习目标
- 能使用
reduce计算购物车中已勾选商品的总价与总数量,并将结果正确渲染到页面。 - 能实现商品勾选/取消勾选状态更新,携带商品 ID 与勾选状态发起请求,并在成功后重新获取购物车数据。
- 能实现购物车商品删除功能,包括删除确认弹窗、携带商品 ID 发起删除请求、删除成功后刷新购物车列表。
- 能实现商品数量修改功能,按新旧数量差值发起更新请求,并处理差值不为 0 的判断与成功后数据刷新。
- 能使用 lodash 的防抖方法优化数量修改请求,完成按需安装、源码适配、重新构建并在小程序中验证防抖效果。
- 能实现购物车访问登录拦截:未登录时跳转授权登录页,登录成功后返回购物车并重新加载购物车数据。
知识点
先说明本节范围
本节笔记标题虽写作“微信小程序授权登录业务”,但笔记与转写稿实际集中在:自定义导航栏、Vant Weapp 的构建使用、购物车金额/数量计算、勾选与删除、数量修改及防抖、购物车登录判断。下面按笔记原意整理,不额外编造业务。
自定义导航栏:隐藏原生顶部样式
为什么需要:页面要做自定义顶部时,原生导航栏会占位并显示默认标题,需要先隐藏。
怎么想:这是页面/全局配置项,不是 JS 逻辑。
怎么做:在对应 JSON 中加入笔记里的配置:
json
{
"navigationStyle": "custom"
}易错点:配置后原生导航栏不显示,页面顶部要按自定义布局处理;不要只写样式却忘了加配置。
Vant Weapp:先“构建 npm”再使用
为什么需要:小程序项目里用 UI 组件库能减少重复写结构。
怎么想:微信小程序可以使用 JS 函数库,但 node_modules 依赖包不能直接使用;必须经过开发者工具构建。
怎么做:参考 Vant 官网:
https://vant-contrib.gitee.io/vant-weapp/#/home
安装后执行:工具 → 构建 npm,构建成功后再在页面 JSON 中引入组件。
易错点:没构建 npm 就直接引用,常出现组件找不到或 undefined;不要把浏览器项目的引入方式直接照搬到小程序。
购物车总价、总数量与全选:reduce/every 的时机
为什么需要:购物车要显示合计金额、结算商品数和全选状态。
怎么想:只统计 isChecked === 1 的商品;金额用“单价×数量”累加,数量直接累加;全选用 every 判断。关键是:必须等购物车数据回来后再计算。
怎么做:
js
calculateTotalPrice() {
const totalPrice = this.data.goods.reduce((prev, item) => {
if (item.isChecked === 1) {
prev += item.price * item.count
}
return prev
}, 0)
this.setData({ totalPrice })
}数量同理,把 prev += item.count 放进勾选判断。全选状态:
js
const selectAll = this.data.goods.length > 0 &&
this.data.goods.every(item => item.isChecked === 1)易错点:return prev 必须放在 if 外面,否则最后一项未勾选时会返回错误;空数组 every 会返回 true,所以要加 length > 0;计算函数要在拿到购物车数据之后执行。
勾选、删除、改数量:请求成功后重新拉购物车
为什么需要:勾选、删除、数量都对应服务器数据,只改本地刷新后会还原。
怎么想:事件里先拿 goodsId 和状态;勾选接口按转写稿要传 1/0,不是布尔值;删除先用 Dialog 确认;数量修改接口传“差值”,即新值减旧值。
怎么做:勾选后调用更新接口,成功后再次获取购物车数据;删除确认后发删除请求,再拉购物车;数量变化时计算 diff = newValue - oldValue,diff === 0 不发请求,成功后重新拉取。
易错点:只 setData 不发请求;勾选状态传成 true/false;数量接口误传新值而不是差值;删除后不刷新,页面仍显示旧数据。
数量修改防抖与 lodash 在小程序中的坑
为什么需要:输入框连续输入会频繁发请求,需要防抖,只在最后一次输入后延时执行。
怎么想:防抖类似“回城”,最后操作完等一会儿再执行;节流是单位时间内只执行一次。转写稿明确本项目数量修改用防抖。
怎么做:使用 lodash.debounce,但小程序不能直接整包引入 lodash。转写稿的做法是按需安装 lodash.debounce,修改其源码中的 root,再执行构建 npm;事件绑定按转写稿改为 ES5 写法。
易错点:直接 import lodash from 'lodash' 会报 Cannot read property ... of undefined,根因是小程序环境没有 window;顺序不能错:先安装依赖、改源码,再构建 npm。
购物车访问的登录判断
为什么需要:未登录不应直接进入购物车。
怎么想:进入购物车先读本地 token;没有 token 跳登录页,登录成功返回后还要重新拉购物车。
怎么做:
js
const token = wx.getStorageSync('token')
if (!token) {
wx.navigateTo({ url: '/pages/login/login' })
}易错点:只在 onLoad 判断和拉数据,登录返回后 onLoad 不一定再执行,页面可能没有购物车数据;应在 onShow 中补充判断/重新获取,登录页返回用 navigateBack 回到购物车。
本节目标与边界
本节笔记只给出登录流程三步:wx.login 获取临时凭证 code;用 code 请求后端换取身份标识 token;再根据 token 获取用户信息。原笔记在“老版本的用户授权流程”处应有配图,但当前文本未提供图片文件,本文不虚构图片;若原图存在,alt 应改为“老版本用户授权流程图”。原笔记也未附代码,因此下面不编造具体接口路径和请求头,只讲清顺序与判断。
第一步:wx.login 获取临时凭证 code
为什么需要:小程序不能在前端直接拿到可信身份。wx.login 拿到的是临时登录凭证 code,它用于让后端向微信侧换取登录态,前端不能把 code 当成长久身份标识。
怎么想:先区分“临时凭证”和“业务身份”。code 是一次性、短时效的;真正用于后续业务请求的是后端返回的 token。所以第一步只做一件事:拿到 code,马上交给后端。
怎么做:调用 wx.login,在成功回调里取出 code;若失败,给用户提示并允许重试。不要把登录流程写死在页面渲染前,应该由“需要登录的动作”或“受保护页面进入时”触发。
易错点:把 code 存进本地缓存当登录态;重复使用旧 code;不处理失败回调;在 code 还没拿到时就发起换 token 请求。
第二步:通过请求 code 获取 token
为什么需要:code 只是换取身份的凭据,不能直接用于业务鉴权。后端用 code 换取用户身份后,返回业务系统可识别的 token,后续请求才能知道“当前是谁”。
怎么想:前端只负责传递 code 和接收 token,不参与敏感身份计算。登录成功后,token 要按项目约定持久化,供后续请求和页面登录态判断使用。
怎么做:用 wx.request 把 code 发给后端登录接口;成功后判断返回结构,取出 token 并写入本地缓存;失败则清掉旧登录态并提示重新登录。若页面需要用户信息,再进入第三步,而不是把用户信息当登录凭证。
易错点:token 为空也当成功;把 token 放在页面 data 里而不是持久化;登录失败后仍继续请求用户信息;并发点击导致重复登录。
第三步:根据 token 获取用户信息
为什么需要:登录解决“是谁”,用户信息解决“展示什么”。头像、昵称等资料要通过 token 鉴权后获取,不能只靠本地缓存判断用户已登录。
怎么想:token 是请求身份,用户信息是业务数据。先有 token,再请求用户信息;请求失败要区分“网络失败”和“登录失效”。若后端判定 token 失效,应清理本地 token 并回到登录流程。
怎么做:按后端约定携带 token 请求用户信息;成功后渲染页面;失败时给出提示,必要时清除登录态。页面进入受保护区域前,先读本地 token 做初判。
易错点:把用户信息缓存当成登录成功;token 失效后不清理;未登录直接请求用户信息;把“授权弹窗”误认为“登录完成”。
老版本授权流程只作对比
为什么需要:笔记提到“老版本的用户授权流程”,目的是和新流程区分,避免把旧授权方式混进现在的登录链路。
怎么想:老版本流程的具体接口原笔记未展开,不能补造。学习时只记住:现在的主线是 wx.login → code → token → 用户信息,旧流程仅作历史对比。
怎么做:遇到旧代码时,先找它是否仍在用旧授权方式;新功能按当前三步走。若项目必须兼容旧流程,要以项目文档和后端约定为准。
易错点:把旧授权弹窗当成登录;新旧流程混用;没有 token 却以为已经登录。
用转写稿补登录态校验与回跳调试
转写稿后半段有一个相关调试:进入购物车要先判断有没有 token。纠正同音字后,思路是:用 wx.getStorageSync 读取 token;没有就 wx.navigateTo 去登录页;登录成功后用 navigateBack 回购物车。调试发现返回后购物车 onLoad 不再执行,导致数据不刷新,所以要在 onShow 或登录成功回调里重新拉取数据。
为什么需要:只做登录、不做页面拦截,未登录用户仍可能进入购物车;只依赖 onLoad,从登录页返回后不会重新取数。
怎么想:登录流程的终点不是“拿到 token”,而是“受保护页面能正确判断登录态并刷新数据”。
怎么做:受保护页面进入时读 token;无 token 跳登录;登录成功回跳;回跳后触发数据刷新。注意 navigateTo 与 navigateBack 的页面栈关系。
易错点:把 wx.getStorageSync 写错;返回后只等 onLoad;不处理登录取消;回跳后不刷新购物车数据。
课堂画面





代码示例
示例一:购物车总价 / 总个数 / 全选状态的计算(reduce + every)
js
// pages/cart/cart.js
Page({
data: {
cart: [], // 购物车商品列表
totalPrice: 0, // 结算商品总价
totalCount: 0, // 结算商品总个数
selectAll: false // 底部全选状态
},
onShow() {
this.getUserCart()
},
// 先拿数据,再算
async getUserCart() {
const res = await reqGetCart()
this.setData({ cart: res.data })
// 必须等数据有了之后再计算,顺序不能反
this.computeTotalPrice()
this.computeTotalCount()
this.computeSelectAll()
},
// 计算商品总价
computeTotalPrice() {
const totalPrice = this.data.cart.reduce((prev, item) => {
if (item.isChecked === 1) {
prev += item.price * item.num
}
return prev // 一定要写在 if 外面
}, 0)
this.setData({ totalPrice })
},
// 计算结算商品的总个数
computeTotalCount() {
const totalCount = this.data.cart.reduce((prev, item) => {
if (item.isChecked === 1) {
prev += item.num
}
return prev
}, 0)
this.setData({ totalCount })
},
// 计算底部全选状态
computeSelectAll() {
const selectAll =
this.data.cart.length > 0 &&
this.data.cart.every(item => item.isChecked === 1)
this.setData({ selectAll })
}
})html
<!-- pages/cart/cart.wxml -->
<view class="footer">
<van-checkbox checked="{{selectAll}}" bind:change="changeSelectAll">全选</van-checkbox>
<view class="total">
合计:¥{{totalPrice}},共 {{totalCount}} 件
</view>
</view>要点解释 / 易错点
reduce的累加结果一定要return prev,而且必须写在if的外面;如果只写在if里,当最后一个商品未勾选时,回调就会返回undefined,最终结果就会出错。- 计算时机:不能放在
onLoad或setData之前,必须等购物车数据回来之后再计算,否则数组是空的,算出来就是 0。 isChecked是数字1 / 0,不是布尔值,判断时要用=== 1。every对空数组会返回true,所以必须加上this.data.cart.length > 0这一条件,否则购物车被删空后底部会错误地显示“全选”。- 计算完必须通过
setData把结果写回data,视图才会更新。
示例二:勾选状态修改 + 商品删除(vant Dialog)
js
// pages/cart/cart.js
import Dialog from '@vant/weapp/dialog/dialog'
import { reqUpdateChecked, reqDeleteGood } from '../../utils/request'
Page({
// 勾选 / 取消勾选
async changeChecked(e) {
const goodsId = e.currentTarget.dataset.goodsId
const isChecked = e.detail ? 1 : 0 // 接口要的是 1 / 0,不是布尔值
try {
await reqUpdateChecked({ goodsId, isChecked })
this.getUserCart() // 成功后必须重新拉一次购物车
} catch (err) {
console.log(err)
}
},
// 删除商品
onDelete(e) {
const goodsId = e.currentTarget.dataset.goodsId
Dialog.confirm({
title: '提示',
message: '确定要删除该商品吗?'
})
.then(async () => {
await reqDeleteGood(goodsId)
this.getUserCart() // 删除成功后再次获取购物车数据
})
.catch(() => {}) // 点取消什么都不做
}
})html
<!-- pages/cart/cart.wxml -->
<van-checkbox
checked="{{item.isChecked === 1}}"
data-goods-id="{{item.goods_id}}"
bind:change="changeChecked" />
<van-icon
name="delete-o"
data-goods-id="{{item.goods_id}}"
bindtap="onDelete" />要点解释 / 易错点
Dialog是 JS 调用的组件,必须在 JS 里import;.then对应“确定”,.catch对应“取消”。- 自定义属性在 WXML 里写
data-goods-id(中划线),JS 里取e.currentTarget.dataset.goodsId(驼峰),这是最常见的数据取不到的原因。 - 复选框的
change事件里,e.detail为true表示勾上,为false表示取消;但接口参数isChecked要的是数字1 / 0,直接传布尔值后端会存错。 - 更新成功 / 删除成功后必须再调用一次
getUserCart(),否则页面上的数据只是本地的,刷新后又回到原样。 - 删除接口需要带商品 ID 作为路径参数,ID 通过
data-goods-id传,别忘记插值{{}}。
示例三:商品数量修改(lodash.debounce)+ 购物车登录拦截
js
// pages/cart/cart.js
import debounce from 'lodash.debounce'
import { reqAddOrUpdateCart } from '../../utils/request'
Page({
onShow() {
// 用 getStorageSync 取 token,不是 getStorage
const token = wx.getStorageSync('token')
if (!token) {
wx.navigateTo({ url: '/pages/login/login' })
return
}
this.getUserCart()
},
// 注意:这里必须用 ES5 的对象属性写法,才能被 debounce 包住
changeNum: debounce(async function (e) {
const goodsId = e.currentTarget.dataset.goodsId
const oldNum = e.currentTarget.dataset.oldNum
const newNum = e.detail
const diff = newNum - oldNum // 接口要的是差值,不是新值
if (diff === 0) return // 没有变化不发请求
await reqAddOrUpdateCart({ goodsId, diff })
this.getUserCart()
}, 300)
})html
<!-- pages/cart/cart.wxml -->
<van-stepper
value="{{item.num}}"
data-goods-id="{{item.goods_id}}"
data-old-num="{{item.num}}"
bind:change="changeNum" />要点解释 / 易错点
van-stepper的change事件在点击加号、减号以及文本框输入时都会触发,只处理这一个事件即可;它自身会拦截非法输入(如非数字、小于最小值)。- 接口要的是差值 diff(新值 − 旧值),不是新数量;旧值通过
data-old-num传进去,e.detail是新值。文档没写清楚时必须与后端确认。 diff === 0时不发请求,避免输完又改回原值导致的无意义请求。- 防抖要写成
changeNum: debounce(function(){}, 300)这种 ES5 形式;用changeNum(e) {}方法简写无法被debounce包装。 - 小程序不能直接
import lodash(源码里用到了window,会报Cannot read property ... of undefined)。要用哪个方法就单独安装哪个包(如lodash.debounce),按官方说明修改源码中的root相关代码,再执行「工具 → 构建 npm」,构建完再改源码无效。 - 登录拦截放在
onShow而不是onLoad:从登录页navigateBack回来时onLoad不会再执行,购物车数据就不会重新加载。
常见错误
- 现象:购物车总价偶发为 0 或漏算后续商品。→ 原因:
reduce回调在if分支里return prev,未勾选商品时丢失累加值。→ 修法:累加逻辑放分支内,return prev放分支外,确保每次回调都有返回值。 - 现象:所有商品取消勾选后,全选状态仍为真。→ 原因:空数组调用
every返回true。→ 修法:先判断购物车列表长度,空列表直接返回false,再执行every。 - 现象:勾选、删除、改数量后页面数据不更新,刷新才正确。→ 原因:只更新了服务端,未重新拉取购物车数据。→ 修法:请求成功后再次调用获取购物车数据的方法,并同步重算总价、总个数、全选状态。
- 现象:修改数量时接口数量越改越错。→ 原因:把输入框新值当差值传给后端。→ 修法:用新值减旧值得到差值;差值为 0 时不发请求。
- 现象:复选框事件里拿不到商品 ID 或勾选状态。→ 原因:未通过
data-*自定义属性传参,或误把布尔值直接传给需要 0/1 的接口。→ 修法:模板上用data-goods-id等传参,事件中从currentTarget.dataset取值,并按接口要求转换勾选状态。 - 现象:小程序中直接
import lodash from 'lodash'报undefined或属性读取错误。→ 原因:未构建 npm,且 lodash 源码依赖小程序不存在的window/root等环境。→ 修法:按需安装独立方法包,修改对应源码兼容项后重新构建 npm,再按需引入。 - 现象:数量输入框每输入一个字符就发一次请求。→ 原因:
change事件直接触发请求,未做防抖。→ 修法:用防抖包装更新数量方法,合理设置等待时间,并处理this与事件参数。 - 现象:未登录也能进入购物车,或登录返回后购物车无数据。→ 原因:页面未做登录态拦截;从登录页返回时页面未重新执行
onLoad。→ 修法:进入购物车先读本地 token,无 token 跳登录;登录成功返回后主动触发购物车数据加载与相关计算。
练习
- 用
reduce计算购物车中已勾选商品的总价,并说明为什么return prev不能只写在if内。 - 实现购物车全选状态计算,要求兼容空购物车、部分勾选、全部勾选三种情况。
- 完成商品数量修改:计算新旧值差值,差值为 0 不发请求,非 0 时请求成功后刷新购物车并重算总价/总个数。
- 完成删除商品流程:确认弹窗、删除请求、删除成功后刷新数据,并验证删除到空购物车时全选状态正确。
- 在小程序中按需使用 lodash 的某个方法,写出安装、源码兼容处理、构建 npm、引入和调用的完整步骤。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 购物车商品的总价与总个数的计算reduce | 15:32 |
| 2 | 完成商品的勾选状态修改 | 13:42 |
| 3 | 完成商品商品的业务 | 14:44 |
| 4 | 修改商品的数量 | 35:34 |
| 5 | 完成购物车数量修改之使用lodash | 28:13 |
小结
- 购物车的总价、总个数、全选状态都依赖同一份列表数据,数据变更后必须统一重算并刷新视图。
reduce、every等数组方法要重点处理返回值、空数组和边界条件,避免“看起来能跑”的隐性错误。- 勾选、删除、改数量都属于服务端状态变更,成功后应重新拉取购物车数据,不能只改本地显示。
- 事件传参优先用
data-*,从currentTarget.dataset取值;接口要求的布尔值/0-1 值要显式转换。 - 修改数量接口通常传差值而非新值,差值为 0 要拦截;高频输入场景应加防抖。
- 小程序使用第三方依赖要先构建 npm;涉及浏览器环境的库需按需引入并做源码兼容处理。
- 购物车需做登录态拦截;从登录页返回后要主动刷新数据,不能依赖页面首次
onLoad。 - 每完成一个交互都要验证:接口是否成功、页面是否刷新、总价/总个数/全选是否同步、空数据是否正常。