Skip to content

微信小程序授权登录业务与登录流程(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;不处理登录取消;回跳后不刷新购物车数据。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:购物车总价 / 总个数 / 全选状态的计算(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 跳登录;登录成功返回后主动触发购物车数据加载与相关计算。

练习 ​

  1. 用 reduce 计算购物车中已勾选商品的总价,并说明为什么 return prev 不能只写在 if 内。
  2. 实现购物车全选状态计算,要求兼容空购物车、部分勾选、全部勾选三种情况。
  3. 完成商品数量修改:计算新旧值差值,差值为 0 不发请求,非 0 时请求成功后刷新购物车并重算总价/总个数。
  4. 完成删除商品流程:确认弹窗、删除请求、删除成功后刷新数据,并验证删除到空购物车时全选状态正确。
  5. 在小程序中按需使用 lodash 的某个方法,写出安装、源码兼容处理、构建 npm、引入和调用的完整步骤。

原视频对照 ​

#视频时长
1购物车商品的总价与总个数的计算reduce15:32
2完成商品的勾选状态修改13:42
3完成商品商品的业务14:44
4修改商品的数量35:34
5完成购物车数量修改之使用lodash28:13

小结 ​

  • 购物车的总价、总个数、全选状态都依赖同一份列表数据,数据变更后必须统一重算并刷新视图。
  • reduce、every 等数组方法要重点处理返回值、空数组和边界条件,避免“看起来能跑”的隐性错误。
  • 勾选、删除、改数量都属于服务端状态变更,成功后应重新拉取购物车数据,不能只改本地显示。
  • 事件传参优先用 data-*,从 currentTarget.dataset 取值;接口要求的布尔值/0-1 值要显式转换。
  • 修改数量接口通常传差值而非新值,差值为 0 要拦截;高频输入场景应加防抖。
  • 小程序使用第三方依赖要先构建 npm;涉及浏览器环境的库需按需引入并做源码兼容处理。
  • 购物车需做登录态拦截;从登录页返回后要主动刷新数据,不能依赖页面首次 onLoad。
  • 每完成一个交互都要验证:接口是否成功、页面是否刷新、总价/总个数/全选是否同步、空数据是否正常。