主题
完成分类的业务与微信小程序授权登录业务(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。
课堂画面





代码示例
示例一:商品列表页(首页/分类共用,上拉加载更多)
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,否则最后一条商品会被遮住。
常见错误
- 现象:页面里用了 Vant 组件但不显示/报错。原因:只安装了依赖,未在开发者工具执行“构建 npm”,或页面/组件未注册。修法:先构建 npm,再在使用页面的 json 中正确注册组件。
- 现象:点击“查看更多”或分类后跳转失败。原因:
wx.navigateTo路径未以/开头,或分类参数未拼接到 URL。修法:统一用绝对路径,分类场景拼接?category2Id=...。 - 现象:分类进入商品列表后拿不到二级分类 ID。原因:点击事件未通过
dataset传 ID,或options参数名与 URL 不一致。修法:事件中取e.currentTarget.dataset,页面onLoad(options)中按同名参数接收并存储。 - 现象:触底加载后列表仍只有 10 条,或出现二维数组。原因:新数据直接
push整个数组,或用新数组覆盖旧数组。修法:合并旧数据与新数据时展开新数组,如[...oldList, ...newList]。 - 现象:触底后重复发请求、页码错乱。原因:未用状态判断是否还有更多,或页码未在请求前递增。修法:维护
status/hasMore与page,无更多时直接 return,有更多时先更新页码再请求。 - 现象:空购物车/空商品列表仍展示列表结构,“没有更多”提前或永不出现。原因:条件判断写反或只判断数组存在未判断长度。修法:用
list.length > 0控制列表与空态,用明确状态控制“没有更多”的展示。 - 现象:购物车未登录也能发请求或页面报错。原因:遗漏登录前提校验。修法:进入购物车先判断登录态,未登录走授权/登录流程,登录成功后再获取购物车数据。
练习
- 从首页“猜你喜欢/热门推荐”的“查看更多”进入商品列表,补齐路由跳转与页面标题配置,并说明为什么路径要以
/开头。 - 实现商品列表分页:首页进入默认第 1 页 10 条;触底加载下一页;无更多时停止请求并展示“没有更多”。
- 从分类页点击某个二级分类进入商品列表,携带
category2Id,在商品列表页接收并用于请求;验证不同二级分类返回不同数据。 - 为商品列表补充空态:当接口返回空数组时展示空态图,有数据时展示卡片列表,并检查“没有更多”的显示条件。
- 完成购物车静态结构与数据渲染:使用 Vant 复选框、计数器、空态等组件,渲染商品勾选状态、图片、标题、价格、数量,并保证未登录时先走登录流程。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 商品列表的数据进行首页渲染 | 24:19 |
| 2 | 商品列表加载更多的效果 | 30:33 |
| 3 | 分类商品列表的展示 | 09:55 |
| 4 | 购物车静态查看 | 14:40 |
| 5 | 获取购物车的数据并且渲染 | 10:06 |
小结
- 商品列表要同时覆盖两个入口:首页“查看更多”和分类二级分类,入口不同决定是否携带分类参数。
- 分页列表的核心不是“触底就请求”,而是页码、是否还有更多、数据合并三者配合。
- 数组追加要区分“追加一个数组”和“追加数组中的元素”,否则很容易出现二维数组或覆盖旧数据。
- 分类跳转必须解决参数传递与接收一致性问题,
dataset命名和options字段要逐字核对。 - 空态和“没有更多”是列表体验的一部分,必须由明确的数据长度/状态条件控制。
- 购物车业务的前置条件是登录态,数据获取应放在登录判断之后,避免无效请求和异常。
- Vant 在小程序中使用的前提是构建 npm 与组件注册,样式异常时优先排查构建和注册链路。