Skip to content

完成分类的业务与微信小程序授权登录业务(Day4 1/2) ​

本页对应 day4,共 5 段视频,约 1 小时 29 分。

学习目标 ​

  • 能够基于商品分页接口,在商品列表页完成首屏商品数据的请求、存储与卡片组件渲染。
  • 能够使用页面触底事件及页码、加载状态变量实现加载更多,并正确合并新旧商品数组。
  • 能够根据接口返回数量判断是否还有更多数据,并在无更多数据时展示“没有更多”分割线。
  • 能够从首页“查看更多”和分类二级分类进入商品列表页,通过路由参数携带二级分类 ID 并请求对应商品。
  • 能够根据商品数组长度切换商品列表与空状态组件,正确展示空数据提示。
  • 能够搭建购物车静态页面并调用购物车接口,完成勾选状态、图片、标题、单价、数量等字段的数据渲染。

知识点 ​

商品列表页面的入口与路由 ​

为什么需要:首页的“猜你喜欢”“热门推荐”只能展示少量商品,分类页也需要进入某个二级分类下的全部商品,所以必须有一个统一的商品列表页面承接“查看更多”和“分类点击”。

怎么想:先建页面,再配路由;入口有两个:首页按钮跳转不带分类参数,分类点击跳转要带二级分类 ID。跳转路径必须以 / 开头。

怎么做:在 pages 下新建商品列表页面(转写中称 goods 页面),检查 app.json 路由是否注册。首页两个“查看更多”绑定跳转方法:

js
wx.navigateTo({
  url: '/pages/goods/goods'
})

分类页点击右侧二级分类时,从 dataset 取 category2Id,拼到 query 上:

js
wx.navigateTo({
  url: `/pages/goods/goods?category2Id=${e.currentTarget.dataset.category2Id}`
})

易错点:路由路径漏写开头的 /;分类点击事件没有通过 dataset 取 ID;变量名写错,如把 category2Id 写成 categoary2Id。

商品列表接口与分页参数 ​

为什么需要:商品列表不是一次性拿完,而是分页加载。首页进入时只拿第一页;分类进入时还要限定某个二级分类。

怎么想:接口是 GET 商品分页列表。page 表示当前第几页,limit 表示每页几条,这两个必带;如果从分类模块进入,还要带二级分类 ID。

怎么做:页面 onLoad 时初始化 page: 1、limit: 10、category2Id: '',并调用获取列表方法。分类进入时从 options 接收参数:

js
onLoad(options) {
  if (options.category2Id) {
    this.setData({ category2Id: options.category2Id })
  }
  this.getGoodsList()
}

请求示意:

js
const res = await request({
  url: '/api/goods/list',
  data: {
    page: this.data.page,
    limit: this.data.limit,
    category2Id: this.data.category2Id
  }
})

易错点:从首页进入时误带分类 ID;从分类进入时忘记接收 options.category2Id;分页参数名和接口文档不一致。

首页商品列表渲染与加载更多 ​

为什么需要:第一页数据拿到后要先渲染;用户触底时继续加载下一页,并把新数据追加到旧数据后面。

怎么想:页面加载先请求第一页,把返回数组存入 goodsArr,传给卡片组件。监听 onReachBottom,只有状态为“还有更多”时才让 page + 1 并再次请求。

怎么做:存储数据并渲染:

js
this.setData({
  goodsArr: res.data.records
})

触底逻辑:

js
onReachBottom() {
  if (this.data.status === 'more') {
    this.setData({ page: this.data.page + 1 })
    this.getGoodsList()
  }
}

合并新旧数据必须展开:

js
const oldList = this.data.goodsArr
this.setData({
  goodsArr: [...oldList, ...res.data.records]
})

判断是否还有更多:如果本次返回条数小于 limit,说明没有下一页:

js
if (res.data.records.length < this.data.limit) {
  this.setData({ status: 'noMore' })
}

易错点:直接 goodsArr: res.data.records 会覆盖旧数据,页面永远只显示 10 条;合并时忘记展开,会变成二维数组;触底不判断状态,没数据后仍反复请求。转写中调试时出现“一直是 10 个”,原因就是新数据替换了旧数据,改成 [...oldList, ...newList] 后才变成 20、30、40 条。无更多时才显示“没有更多”分割线,有更多时不显示。

分类商品列表与空状态 ​

为什么需要:不同二级分类商品数量不同,有的分类可能没有商品;没有数据时不能只留空白。

怎么想:分类入口已经把 category2Id 带到商品列表页,页面接收后存入 data,再随请求发出。渲染时判断 goodsArr.length > 0,有数据展示卡片,否则展示空状态组件。

怎么做:

html
<view wx:if="{{goodsArr.length > 0}}">
  <card list="{{goodsArr}}"></card>
</view>
<view wx:else>
  <empty></empty>
</view>

使用 empty 组件前要在页面 json 中注册,并在 wxml 引入使用。

易错点:只引入组件但忘记在 json 注册;空状态判断写成 >= 0 导致永远显示商品区域;分类 ID 没存进 data,请求时仍拿空串。

购物车静态结构与数据渲染 ​

为什么需要:购物车是电商小程序的核心页面,需要展示商品、勾选状态、数量、删除、结算等。先搭静态结构,再拿接口数据渲染。

怎么想:购物车页面访问前提是登录,但转写中先拿数据渲染,登录判断最后补。静态结构可复用已有 card 相关结构和样式,同时把用到的 van-checkbox、van-stepper、图标、按钮等组件在 json 中注册。

怎么做:页面加载时请求购物车列表:

js
async getCart() {
  const res = await request({ url: '/api/cart/list' })
  this.setData({ cartArr: res.data })
}
onLoad() {
  this.getCart()
}

渲染时遍历 cartArr,复选框绑定 item.isCheck,图片、标题、价格、数量按接口字段绑定:

html
<van-checkbox checked="{{item.isCheck}}"></van-checkbox>
<image src="{{item.image}}"></image>
<text>{{item.title}}</text>
<text>{{item.price}}</text>
<van-stepper value="{{item.count}}"></van-stepper>

易错点:组件未注册就直接使用;字段名写错,如 isCheck 写成 isChecke;删除商品后忘记重新请求购物车数据;合计金额和总数量可后续用 reduce 计算,先保证基础数据渲染正确。

导航栏自定义:先解决“顶部样式隐藏” ​

为什么需要:商品列表、购物车等页面往往需要自定义顶部区域,如果继续使用小程序默认导航栏,页面结构会被默认标题栏占位,视觉上不统一。
怎么想:先把页面顶部交给自定义结构控制,再在页面配置中关闭默认导航栏。
怎么做:在对应页面的 JSON 中配置:

json
{
  "navigationStyle": "custom"
}

易错点:配置位置要写在页面 JSON 或按需写在 app.json;写在全局会影响所有页面。改完后要重新编译确认页面顶部是否还有默认标题栏。

Vant Weapp 的使用:UI 组件库不是直接拿来就用 ​

为什么需要:商品列表、购物车需要复选框、计数器、空态、分割线等组件,手写成本高,使用 Vant Weapp 能快速搭建。
怎么想:小程序可以使用 JS 函数库,但 node_modules 依赖包不能直接使用,必须先经过开发者工具构建 npm。
怎么做:参考 Vant 官网:https://vant-contrib.gitee.io/vant-weapp/#/home,安装后执行“工具 → 构建 npm”,再在页面 JSON 中注册要用的组件。
易错点:不构建 npm 就直接引用会报错;组件未在 JSON 中注册也不能用;如果样式不是预期效果,转写稿中的调试办法是先清缓存,再重新构建 npm。

商品列表页面:首页“查看更多”和分类都要能进来 ​

为什么需要:首页的“猜你喜欢”“热门推荐”只展示少量商品,用户需要进入完整商品列表;分类页点击二级分类也要进入同一列表。
怎么想:新建一个商品列表页,首页按钮跳转时不带分类参数,分类入口跳转时携带二级分类 ID。
怎么做:新建 pages/goods/goods 并确认路由;首页“查看更多”使用:

js
wx.navigateTo({
  url: '/pages/goods/goods'
})

分类点击时从 dataset 取 category2Id,拼到 URL 上:

js
wx.navigateTo({
  url: `/pages/goods/goods?category2Id=${e.currentTarget.dataset.category2Id}`
})

易错点:跳转路径必须以 / 开头;页面要在 app.json 中注册;分类参数名前后要一致,转写稿中就出现过 category2Id 拼写不一致的调试过程。

商品列表接口与分页参数:page、limit、category2Id ​

为什么需要:列表不是一次性全部返回,而是分页加载;从分类进入时还必须告诉接口要看哪个二级分类。
怎么想:接口是 GET 请求,page 表示当前第几页,limit 表示每页几条,这两个必带;从分类进入时再带 category2Id,从首页进入时该参数为空。
怎么做:页面 onLoad 中初始化 page=1、limit=10、category2Id='',并读取路由参数:

js
onLoad(options) {
  if (options.category2Id) {
    this.setData({ category2Id: options.category2Id })
  }
  this.getGoodsList()
}

请求地址按 ?category2Id=... 形式拼接。
易错点:page、limit 参数名写错;首页进入时误传分类 ID;分类参数从 options 取而不是从 data 猜。

触底加载更多:用状态控制“还要不要发请求” ​

为什么需要:用户滑到底部时要加载下一页,直到没有更多数据为止。
怎么想:维护一个 status,可取 more 或 noMore。每次请求后判断返回数组长度:等于 limit 说明可能还有下一页,小于 limit 就置为 noMore。
怎么做:在 onReachBottom 中先判断:

js
if (this.data.status !== 'more') return
this.setData({ page: this.data.page + 1 })
this.getGoodsList()

合并数据时不能直接覆盖,要展开旧数组:

js
this.setData({
  goodsArr: [...this.data.goodsArr, ...res.data.records]
})

易错点:直接赋值会导致只剩第二页数据;push 不展开会变成二维数组;没有更多时仍继续请求;最后一页不足 limit 条要正确置为 noMore。转写稿中专门调试过“永远只显示10条”的问题,原因就是新数据覆盖了旧数据。

分类入口与空态:没有商品时要展示占位 ​

为什么需要:不同二级分类下商品数量不同,有的分类可能没有数据,直接空白不美观。
怎么想:列表有数据时渲染商品,没有数据时用 van-empty 展示空态。
怎么做:模板中先判断:

html
<block wx:if="{{ goodsArr.length > 0 }}">
  <!-- 商品列表 -->
</block>
<van-empty wx:else description="暂无更多商品" />

同时“没有更多”分割线只在 status === 'noMore' 时显示。
易错点:van-empty、van-divider 未在 JSON 中注册;判断条件写成 >=0 导致空数组也渲染;分类参数没存进 data 就发请求。

购物车:先静态结构,再获取数据渲染 ​

为什么需要:购物车是电商小程序的核心页面,包含全选、商品勾选、数量修改、删除、合计等结构。
怎么想:先把静态结构和样式搭出来,再调用“获取购物车列表”接口渲染。转写稿明确提醒:购物车访问前提是登录,登录判断最后做,先拿数据渲染。
怎么做:页面 onLoad 中封装 getCartList,请求成功后保存 cartArr;模板中 v-for 渲染商品,复选框绑定 item.isCheck,图片、标题、价格、数量分别绑定接口字段;数量用 van-stepper。
易错点:未登录时接口拿不到数据;字段名写错导致图片或价格不显示;Vant 组件未注册;样式异常时先清缓存并重新构建 npm。转写稿中购物车静态结构复制后,还需要同步复制样式和 JSON 组件注册,不能只抄 WXML。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:商品列表页(首页/分类共用,上拉加载更多) ​

pages/goods_list/goods_list.json

json
{
  "navigationBarTitleText": "商品列表展示",
  "usingComponents": {
    "van-card": "@vant/weapp/card/index",
    "van-divider": "@vant/weapp/divider/index",
    "van-empty": "@vant/weapp/empty/index"
  }
}

api/index.js(节选)

js
import request from './request'

// 获取商品分页列表:page/limit 必传,category2Id 从分类页进入时才带
export const reqGoodsList = ({ page, limit, category2Id = '' }) => {
  return request({
    url: `/api/product/page?page=${page}&limit=${limit}&category2Id=${category2Id}`,
    method: 'GET'
  })
}

pages/goods_list/goods_list.js

js
import { reqGoodsList } from '../../api/index'

Page({
  data: {
    goodsArr: [],                              // 已加载的全部商品
    config: { page: 1, limit: 10 },            // 当前第几页、每页几条
    category2Id: '',                           // 二级分类ID,首页进来为空
    status: 'more'                             // more=还有更多  noMore=没有更多
  },

  onLoad(options) {
    // 分类页跳过来会带 category2Id,首页查看更多不带
    if (options.category2Id) {
      this.setData({ category2Id: options.category2Id })
    }
    this.getGoodsList()
  },

  // 封装:获取商品列表
  async getGoodsList() {
    const { page, limit } = this.data.config
    const result = await reqGoodsList({
      page,
      limit,
      category2Id: this.data.category2Id
    })
    const records = result.data.records

    // 关键:老数据 + 新数据合并,不能直接覆盖
    const oldArr = this.data.goodsArr
    oldArr.push(...records)
    this.setData({ goodsArr: oldArr })

    // 返回条数不足一页 => 没有更多了
    if (records.length < limit) {
      this.setData({ status: 'noMore' })
    } else {
      this.setData({ status: 'more' })
    }
  },

  // 页面触底
  onReachBottom() {
    if (this.data.status === 'noMore') return   // 没有更多就不再发请求
    this.setData({ 'config.page': this.data.config.page + 1 })
    this.getGoodsList()
  }
})

pages/goods_list/goods_list.wxml

xml
<view class="goods-list">
  <block wx:if="{{goodsArr.length > 0}}">
    <van-card
      wx:for="{{goodsArr}}"
      wx:key="id"
      price="{{item.price}}"
      title="{{item.name}}"
      desc="{{item.subtitle}}"
      thumb="{{item.image}}"
    />
  </block>
  <van-empty wx:else description="暂无商品" />

  <van-divider wx:if="{{status === 'noMore'}}">没有更多了</van-divider>
</view>

要点解释 / 易错点

  • onReachBottom 是页面配置项里“上拉触底”的事件,不是组件事件,直接写在 Page({}) 里即可。
  • status 用 'more' / 'noMore' 两个状态控制是否继续发请求,避免到底后还无限重复请求。
  • 判断“有没有下一页”用 records.length < limit,等于 limit 时仍要尝试下一页(最后一页刚好满 10 条的情况)。
  • oldArr.push(...records) 里的 ... 是展开运算符:不展开会变成“数组里套数组”,渲染直接崩掉。
  • 注意 setData({ 'config.page': ... }) 这种带路径的写法,不能写成 setData({config.page: ...})。
  • wx:key="id" 必须有,否则列表追加时会出渲染复用错乱。
  • 路径必须 / 开头:wx.navigateTo({ url: '/pages/goods_list/goods_list' })。

示例二:分类页点击二级分类,带参数跳转商品列表 ​

pages/category/category.js

js
Page({
  data: {
    categoryList: []   // 当前一级分类下的二级分类列表
  },

  // 点击右侧二级分类
  toGoodsList(e) {
    // 自定义属性全小写:data-category2id => dataset.category2id
    const { category2id } = e.currentTarget.dataset
    wx.navigateTo({
      url: `/pages/goods_list/goods_list?category2Id=${category2id}`
    })
  }
})

pages/category/category.wxml

xml
<view class="category">
  <!-- 左侧一级分类 -->
  <scroll-view scroll-y class="left">
    <view
      wx:for="{{categoryList}}"
      wx:key="id"
      class="left-item"
      data-id="{{item.id}}"
      bindtap="changeCategory"
    >{{item.name}}</view>
  </scroll-view>

  <!-- 右侧二级分类 -->
  <scroll-view scroll-y class="right">
    <view
      wx:for="{{categoryList}}"
      wx:key="id"
      class="right-item"
      data-category2id="{{item.id}}"
      bindtap="toGoodsList"
    >
      <image class="right-img" src="{{item.image}}" />
      <text>{{item.name}}</text>
    </view>
  </scroll-view>
</view>

要点解释 / 易错点

  • 小程序事件不能直接传参,只能通过 data-* 自定义属性传,再在 e.currentTarget.dataset 里取。
  • data-category2id 在 dataset 里会变成全小写 category2id;写成 data-category2Id 取到的也是 category2id,但强烈建议统一写全小写,避免踩坑。
  • 跳转路径必须以 / 开头,带参数用 ?key=value,多个参数用 & 连接。
  • 商品列表页 onLoad(options) 收到的 key 就是 category2Id,要判空后再 setData,否则首页进入时会把 undefined 存进去。
  • 商品列表页的 category2Id 必须在 onLoad 里就存进 data,因为第一次 getGoodsList() 就要带这个参数。

示例三:购物车数据获取 + 静态渲染 ​

pages/cart/cart.json

json
{
  "navigationBarTitleText": "购物车",
  "usingComponents": {
    "van-checkbox": "@vant/weapp/checkbox/index",
    "van-stepper": "@vant/weapp/stepper/index",
    "van-submit-bar": "@vant/weapp/submit-bar/index",
    "van-empty": "@vant/weapp/empty/index",
    "van-icon": "@vant/weapp/icon/index"
  }
}

api/index.js(节选)

js
// 获取购物车列表(必须登录后才拿得到)
export const reqCartList = () => {
  return request({ url: '/api/cart/cartList', method: 'GET' })
}

pages/cart/cart.js

js
import { reqCartList } from '../../api/index'

Page({
  data: {
    cartArr: []
  },

  onShow() {
    this.getCartList()
  },

  async getCartList() {
    const result = await reqCartList()
    if (result.code === 200) {
      this.setData({ cartArr: result.data })
    }
  },

  // 删除某个商品(后续业务,此处仅留入口)
  async deleteGoods(e) {
    const { goodsid } = e.currentTarget.dataset
    await request({ url: `/api/cart/delete?goodsId=${goodsid}`, method: 'GET' })
    this.getCartList()   // 删除成功后要重新拉一次购物车
  }
})

pages/cart/cart.wxml

xml
<view class="cart-container">
  <!-- 有数据:渲染列表 -->
  <view class="cart-list" wx:if="{{cartArr.length > 0}}">
    <view class="cart-item" wx:for="{{cartArr}}" wx:key="goodsId">
      <van-checkbox
        checked="{{item.isChecked === 1}}"
        checked-color="#ff9402"
      />

      <image class="goods-img" src="{{item.image}}" mode="aspectFill" />

      <view class="goods-info">
        <view class="goods-title">{{item.name}}</view>
        <view class="goods-bottom">
          <text class="goods-price">¥{{item.price}}</text>
          <view class="goods-right">
            <van-icon
              name="delete-o"
              size="18"
              data-goodsid="{{item.goodsId}}"
              bindtap="deleteGoods"
            />
            <van-stepper value="{{item.count}}" min="1" max="99" />
          </view>
        </view>
      </view>
    </view>
  </view>

  <!-- 没数据:空状态 -->
  <van-empty wx:else description="购物车空空如也" />

  <van-submit-bar price="{{0}}" button-text="提交订单" />
</view>

要点解释 / 易错点

  • 微信小程序能用 JS 库,但 node_modules 不能直接用,必须先 工具 -> 构建 npm,否则 van-* 组件全部报“未找到”。
  • 每个用到的 vant 组件都必须在页面 .json 的 usingComponents 里注册,不注册直接写标签就是无效标签。
  • 如果 vant 样式“不是预期样子”,先清缓存,再重新构建 npm。
  • 用 wx:if / wx:else 控制空状态,不要用 wx:if 同时渲染列表和空图。
  • van-checkbox 的勾选状态是 checked 布尔属性,接口给的是 0/1,要写 checked="{{item.isChecked === 1}}",直接写 0/1 会不生效。
  • 购物车数据建议放 onShow 而不是 onLoad,从商品详情页加购返回时才能刷新。
  • 删除等写操作之后一定要重新 getCartList(),否则页面和数据库不同步。
  • van-submit-bar 是固定在底部的,页面需要给列表留出底部 padding,否则最后一条商品会被遮住。

常见错误 ​

  1. 现象:页面里用了 Vant 组件但不显示/报错。原因:只安装了依赖,未在开发者工具执行“构建 npm”,或页面/组件未注册。修法:先构建 npm,再在使用页面的 json 中正确注册组件。
  2. 现象:点击“查看更多”或分类后跳转失败。原因:wx.navigateTo 路径未以 / 开头,或分类参数未拼接到 URL。修法:统一用绝对路径,分类场景拼接 ?category2Id=...。
  3. 现象:分类进入商品列表后拿不到二级分类 ID。原因:点击事件未通过 dataset 传 ID,或 options 参数名与 URL 不一致。修法:事件中取 e.currentTarget.dataset,页面 onLoad(options) 中按同名参数接收并存储。
  4. 现象:触底加载后列表仍只有 10 条,或出现二维数组。原因:新数据直接 push 整个数组,或用新数组覆盖旧数组。修法:合并旧数据与新数据时展开新数组,如 [...oldList, ...newList]。
  5. 现象:触底后重复发请求、页码错乱。原因:未用状态判断是否还有更多,或页码未在请求前递增。修法:维护 status/hasMore 与 page,无更多时直接 return,有更多时先更新页码再请求。
  6. 现象:空购物车/空商品列表仍展示列表结构,“没有更多”提前或永不出现。原因:条件判断写反或只判断数组存在未判断长度。修法:用 list.length > 0 控制列表与空态,用明确状态控制“没有更多”的展示。
  7. 现象:购物车未登录也能发请求或页面报错。原因:遗漏登录前提校验。修法:进入购物车先判断登录态,未登录走授权/登录流程,登录成功后再获取购物车数据。

练习 ​

  1. 从首页“猜你喜欢/热门推荐”的“查看更多”进入商品列表,补齐路由跳转与页面标题配置,并说明为什么路径要以 / 开头。
  2. 实现商品列表分页:首页进入默认第 1 页 10 条;触底加载下一页;无更多时停止请求并展示“没有更多”。
  3. 从分类页点击某个二级分类进入商品列表,携带 category2Id,在商品列表页接收并用于请求;验证不同二级分类返回不同数据。
  4. 为商品列表补充空态:当接口返回空数组时展示空态图,有数据时展示卡片列表,并检查“没有更多”的显示条件。
  5. 完成购物车静态结构与数据渲染:使用 Vant 复选框、计数器、空态等组件,渲染商品勾选状态、图片、标题、价格、数量,并保证未登录时先走登录流程。

原视频对照 ​

#视频时长
1商品列表的数据进行首页渲染24:19
2商品列表加载更多的效果30:33
3分类商品列表的展示09:55
4购物车静态查看14:40
5获取购物车的数据并且渲染10:06

小结 ​

  1. 商品列表要同时覆盖两个入口:首页“查看更多”和分类二级分类,入口不同决定是否携带分类参数。
  2. 分页列表的核心不是“触底就请求”,而是页码、是否还有更多、数据合并三者配合。
  3. 数组追加要区分“追加一个数组”和“追加数组中的元素”,否则很容易出现二维数组或覆盖旧数据。
  4. 分类跳转必须解决参数传递与接收一致性问题,dataset 命名和 options 字段要逐字核对。
  5. 空态和“没有更多”是列表体验的一部分,必须由明确的数据长度/状态条件控制。
  6. 购物车业务的前置条件是登录态,数据获取应放在登录判断之后,避免无效请求和异常。
  7. Vant 在小程序中使用的前提是构建 npm 与组件注册,样式异常时优先排查构建和注册链路。