主题
底部品牌广告与楼层(Lesson15 2/2)
本页对应 Lesson15,共 5 段视频,约 1 小时 57 分。
学习目标
- 能够基于 mock 配置与 API 封装规则,完成今日推荐、楼层、商品排行接口的定义与前端调用,并通过控制台验证返回数据结构正确。
- 能够将首页中的今日推荐、楼层、商品排行拆分为独立 Vue 组件,在父页面中完成引入、注册与渲染,并保证样式和交互正常。
- 能够分别实现楼层组件两种数据获取方案:父组件获取数组后通过
v-for传参渲染,以及子组件内自行dispatch获取数据并渲染,并能说明两者适用场景。 - 能够实现商品排行两种数据策略:一次性获取全部排行数据并切换显示,以及切换分类时按类型重新发送请求,并通过选中态和加载效果验证切换结果。
- 能够使用 Vuex 的
state/mutations/actions完成接口数据存储与更新,并能按接口、action、mutation、组件取值的顺序定位并修复数据未渲染问题。 - 能够完成 Swiper 轮播、动态
class、点击事件、watch配合immediate等交互,并通过样式调整使图片、楼层和排行布局符合页面效果。
知识点
底部品牌广告:先把“接口—封装—状态—组件”串起来
为什么需要:首页内容多,底部品牌广告、今日推荐这类模块如果都堆在 home/index.vue 里,阅读性差、维护困难。转写稿里明确说:首页内容多,要按展现内容划分模块,每个模块就是一个组件。
怎么想:先不管页面,先把数据链路打通:mock 提供接口 → API 统一封装 → Vuex 保存数据 → 组件渲染。转写稿反复强调“先完成接口,再封装,再进 store,再渲染”,并且“写一点测一点,不要全写完再调”。
怎么做:在 mock 的 index.js 里增加今日推荐接口,请求方式必须是 GET,地址要和前端最终请求地址一致。图片资源放到 public/images/home 下,页面里用 /images/... 访问。示意代码:
js
// mock/index.js
{
url: '/adv/today/list',
type: 'get',
response: () => ({
code: 200,
data: [
{ id: 1, imgUrl: '/images/home/today01.png' },
{ id: 2, imgUrl: '/images/home/today02.png' }
]
})
}js
// api/adv.js
export const reqTodayList = () =>
mockRequest({ url: '/adv/today/list', method: 'get' })js
// store/adv.js
state: { topTodayList: [] },
mutations: { SET_TODAY_LIST(state, list) { state.topTodayList = list } },
actions: {
async getTodayList({ commit }) {
const res = await reqTodayList()
commit('SET_TODAY_LIST', res.data)
}
}易错点:mock 请求在 Network 里可能看不到,因为被 mock 拦下来了;不要因此以为没发请求。接口地址、请求方式、baseURL 拼接要一致。图片不要写 src 目录相对路径,放 public 后用绝对路径。
今日推荐组件拆分与渲染
为什么需要:今日推荐只是首页一块,拆成 TodayRecommend/index.vue 后,home 只负责组合,结构更清晰。
怎么想:先把静态结构和样式搬进组件,再在组件里 mounted 调 Vuex action,最后用 v-for 渲染列表。转写稿里说“只保留一个 li”,因为列表项结构相同。
怎么做:
vue
<!-- TodayRecommend/index.vue -->
<template>
<div class="today-recommend">
<ul>
<li v-for="item in topTodayList" :key="item.id">
<img :src="item.imgUrl" alt="今日推荐商品图" />
</li>
</ul>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'TodayRecommend',
mounted() {
this.$store.dispatch('getTodayList')
},
computed: {
...mapState(['topTodayList'])
}
}
</script>父组件中引入即可。易错点:样式没搬全导致错位;图片路径少写 /;v-for 没写 key;数据没出来时先看 Vuex 里有没有,再查 action、mutation 是否写对。
楼层数据:父组件取数,子组件传参
为什么需要:家用电器、手机通讯等楼层结构一样,不能复制多份。
怎么想:接口返回数组,每个元素描述一个楼层;父组件 v-for 渲染 Floor 组件,把 item 传给子组件。转写稿也给了第二种方案:在 Floor 组件内部取数并自身遍历。两种都能做,建议先用父组件取数传参,职责更清楚。
怎么做:
vue
<!-- home/index.vue -->
<Floor v-for="item in floorList" :key="item.id" :item="item" />js
// Floor/index.vue
props: ['item']js
// store/product.js
async getFloorList({ commit }) {
const res = await reqFloorList()
commit('SET_FLOOR_LIST', res.data)
}易错点:楼层图片要放 public,路径用 /images/...;key 用唯一 id;数据没出来先看 Vuex 的 floorList,再沿 action、mutation、组件遍历一层层查。转写稿里说“沿着模块 action 看解对了没有”,这就是排查顺序。
商品排行:两种数据策略
为什么需要:热卖、特价、新品要切换。数据一次性拿还是点一次拿一次,体验不同。
怎么想:方案一,接口一次返回全部排行,前端用 activeIndex 控制显示,切换快但首屏数据多;方案二,每次切换发请求,首屏快但有请求等待。转写稿明确说“看接口怎么给你返回”。
怎么做:方案一保存 rankList,用 v-for 渲染三个标题和内容区,activeIndex 控制选中和显示。方案二用 type 区分 0/1/2,watch type 重新 dispatch,初始 mounted 先取 0。
js
watch: {
type() {
this.getRankList()
}
},
mounted() {
this.getRankList()
}易错点:结构赋值和已定义变量冲突时要加括号,如 ({ data } = res);点击后只改 type 不重新请求,页面不会变;方案二初始值要立即调用一次,否则首屏没数据。
轮播图与图片调试
为什么需要:底部品牌广告常用轮播图展示。
怎么想:复用已有轮播组件和 mock 的 banner 数据,不要重复造。转写稿里说“之前已经做过轮播图,把数据拿过来,循环 carouselList”。
怎么做:
vue
<swiper :options="swiperOption">
<swiper-slide v-for="item in carouselList" :key="item.id">
<img :src="item.imgUrl" alt="底部品牌广告轮播图" />
</swiper-slide>
</swiper>易错点:public 图片路径不能写成 src 相对路径;图片宽高样式要控制,否则撑破布局;需要循环时设置 loop: true;不需要小圆点可删掉分页器,但左右箭头可保留。转写稿里还提醒:图片尺寸不合适时,调 img 的宽高或外层样式,不要只改数据。
先明确目标:一个楼层就是数组里的一个对象
为什么需要:首页里“家用电器”“手机通讯”等楼层结构相同,如果每个楼层都复制一遍模板,代码会非常臃肿,后续维护困难。
怎么想:先看数据。楼层接口返回的是数组,数组里每个元素描述一个楼层;每个楼层的结构一致,所以应该“封装一个楼层组件”,再在 Home 中循环渲染,把每个楼层对象通过 props 传给组件。
怎么做:按“完成 mock 接口 → 封装 API → store 获取并保存 → 封装楼层组件 → Home 调用数据并循环渲染”的顺序做。
易错点:不要把楼层数组当成单个对象;不要每个楼层写一份模板;循环时记得加 :key。
完成 mock 接口:地址和请求方式必须匹配
为什么需要:前端要先有数据才能渲染,mock 用来模拟服务端接口。
怎么想:转写稿里强调,mock 能生效要满足两个条件:请求方式一致、请求地址一致。前端经过 mockRequest 的 baseURL 拼接后,最终地址要和 mock 里写的地址对应。
怎么做:在 src/mock/index.js 中增加楼层数据接口:
js
// 楼层数据
Mock.mock("http://mock.com/product/floorList","get",{
ok:200,
data:floorList
})易错点:地址写成 /product/floorList 还是完整地址,要看 mockRequest 的 baseURL;请求方式写成 post 也匹配不到。转写稿提到 mock 请求在 Network 里可能看不到,因为请求已被 mock 拦截,不要误以为没发请求。
封装 API:统一管理请求
为什么需要:组件里不直接写请求地址,便于集中修改和复用。
怎么想:楼层属于商品相关,放到 src/api/product.js。
怎么做:
js
// 获取楼层信息
export const getFloorList = ()=>mockRequest("/product/floorList");易错点:函数名和 store 中引入的名字要一致;不要漏掉 mockRequest 的引入。
store 获取并保存数据:state、mutations、actions 三步走
为什么需要:楼层数据要被 Home 页面使用,放进 Vuex 便于统一管理。
怎么想:先定义 state.floorList,再写 mutation 保存,最后在 action 中发请求并 commit。转写稿里反复说“先看有没有数据,再看有没有 commit”,调试要一层一层找。
怎么做:
js
import {getBaseCategoryList, getFloorList} from "@/api/product";
const state = {
floorList:[],// 楼层数据信息
}
const mutations = {
SAVE_FLOOR_LIST(state,floorList){
state.floorList = floorList;
}
}
const actions = {
async getFloorListAsync({commit}){
const {data} = await getFloorList();
commit("SAVE_FLOOR_LIST",data);
}
}
export default {
namespaced:true,
state,
mutations,
actions
}易错点:忘记 commit 时 state 不会更新;data 解构错层级会拿到 undefined;命名空间开启后调用要写 product/getFloorListAsync。
封装楼层组件:props 接收一个楼层对象
为什么需要:楼层结构重复,抽成组件后 Home 只负责循环。
怎么想:组件只关心“一个楼层对象”怎么渲染,因此用 props:["floorInfo"] 接收。标题、导航、关键词、轮播、推荐位都从 floorInfo 中取。
怎么做:在 src/pages/Home/Floor/index.vue 中写模板,核心代码如下:
vue
<template>
<div class="floor">
<div class="py-container">
<div class="title clearfix">
<h3 class="fl">{{floorInfo.name}}</h3>
<div class="fr">
<ul class="nav-tabs clearfix">
<li :class="{active:index===0}" v-for="(item,index) in floorInfo.navList">
<a :href="item.url" data-toggle="tab">{{item.text}}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<div class="tab-pane">
<div class="floor-1">
<div class="blockgary">
<ul class="jd-list">
<li v-for="(item,index) in floorInfo.keywords" :key="index">{{item}}</li>
</ul>
<img :src="floorInfo.imgUrl" />
</div>
<div class="floorBanner">
<swiper class="swiper" :options="swiperOption">
<swiper-slide v-for="item in floorInfo.carouselList" :key="item.id">
<img :src="item.imgUrl" />
</swiper-slide>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[0]" />
</div>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[1]" />
</div>
</div>
<div class="split center">
<img :src="floorInfo.bigImg" />
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[2]" />
</div>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[3]" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
name: "Floor",
props:["floorInfo"],
components: {
Swiper,
SwiperSlide
},
data() {
return {
swiperOption: {
slidesPerView: 1,
spaceBetween: 1,
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true
},
autoplay: {
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
},
}
</script>易错点:忘记注册 Swiper、SwiperSlide;忘记引入 swiper 样式;recommendList 下标写错;导航 active 直接写成 index===0 只会第一项高亮,若要做切换需另加当前索引;图片路径要注意 public 目录与相对路径。
Home 调用数据并循环渲染
为什么需要:Home 是页面入口,负责取楼层列表并决定渲染几个楼层组件。
怎么想:在 Home 中派发 action,拿到 $store.state.product.floorList,用 v-for 循环。转写稿也提到另一种方案:把请求放进楼层组件内部,由组件自己循环;笔记采用的是 Home 获取、组件接收 props 的方案。
怎么做:
vue
<Floor v-for="item in $store.state.product.floorList" :floorInfo="item" :key="item.id"></Floor>易错点:忘记在 Home 注册/引入 Floor;循环变量名和 :floorInfo 写错;key 用 index 虽能跑但不推荐。调试时先看 Vuex 里有没有 floorList,再看组件 props 是否收到,最后看模板字段是否写对。
本节目标:把楼层请求放进 Floor 组件
本节是“楼层方案二”。方案一是在 Home/index.vue 里先 dispatch 获取楼层数据,再用 v-for 遍历 floorList,把每个 floorInfo 传给 Floor 子组件。方案二则反过来:Home 只负责放置 <Floor></Floor>,楼层数据的请求和遍历都放进 Floor/index.vue 内部完成。
为什么需要:首页内容多,如果所有请求、循环、结构都堆在 Home 里,可读性差,楼层组件也不够独立。
怎么想:楼层结构一致,只是数据不同,因此让 Floor 自己取 store 中的 product.floorList,自己循环渲染多个楼层。
怎么做:Home 中只保留组件入口;Floor 的 mounted 中派发 product/getFloorListAsync,模板中循环 $store.state.product.floorList。
易错点:方案二不要在 Home 里重复 dispatch,否则会重复请求;Floor 内循环时外层要有包裹元素,不能把 v-for 直接乱挂在根节点上。
Home 页面只保留组件入口
为什么需要:方案二把数据请求下沉到楼层组件,父页面不再关心楼层数据怎么来。
怎么想:Home 只负责组合页面模块,楼层模块交给 Floor。
怎么做:在 src/pages/Home/index.vue 中写入:
vue
<Floor></Floor>易错点:如果这里还保留方案一的 v-for 和 dispatch,就会和 Floor 内部请求重复;同时要确认 Floor 已正确注册或引入。
Floor 组件内请求并遍历楼层
为什么需要:楼层数据是一个数组,每个元素描述一个楼层,结构相同,适合在组件内部统一循环。
怎么想:先把 floorList 请求回来,再在模板里用 v-for="floorInfo in $store.state.product.floorList" 渲染。转写稿里强调:如果直接在最外层写 v-for 不合适,就在外面加一个 div 包裹,再在内部循环。
怎么做:保留笔记中的完整组件代码:
vue
<template>
<div>
<div class="floor" v-for="floorInfo in $store.state.product.floorList">
<div class="py-container">
<div class="title clearfix">
<h3 class="fl">{{floorInfo.name}}</h3>
<div class="fr">
<ul class="nav-tabs clearfix">
<li :class="{active:index===0}" v-for="(item,index) in floorInfo.navList">
<a :href="item.url" data-toggle="tab">{{item.text}}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<div class="tab-pane">
<div class="floor-1">
<div class="blockgary">
<ul class="jd-list">
<li v-for="(item,index) in floorInfo.keywords" :key="index">{{item}}</li>
</ul>
<img :src="floorInfo.imgUrl" />
</div>
<div class="floorBanner">
<swiper class="swiper" :options="swiperOption">
<swiper-slide v-for="item in floorInfo.carouselList" :key="item.id">
<img :src="item.imgUrl" />
</swiper-slide>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[0]" />
</div>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[1]" />
</div>
</div>
<div class="split center">
<img :src="floorInfo.bigImg" />
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[2]" />
</div>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[3]" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
name: "Floor",
props:["floorInfo"],
components: {
Swiper,
SwiperSlide
},
data() {
return {
swiperOption: {
slidesPerView: 1,
spaceBetween: 1,
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true
},
autoplay: {
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
},
mounted(){
this.$store.dispatch("product/getFloorListAsync");
}
}
</script>易错点:mounted 中只派发一次请求;路径必须是 product/getFloorListAsync;模板中取的是 $store.state.product.floorList,不要写成别的模块。方案二不再依赖父组件传 floorInfo,props 保留也不影响,但不要误以为数据来自父组件。
调试过程:逐层排查数据
转写稿里强调“写一点测一点,不要全写完再看”。方案二调试顺序是:先在 Vue Devtools 中看 product.floorList 是否有数据;没有数据就检查 mounted 是否执行、dispatch 的 action 名是否写对;再检查 API 封装地址和 mock 接口地址是否一致;最后检查模板中 v-for 的路径和字段名。若页面空白,优先看控制台报错和 store 数据,不要一上来乱改样式。
小结
方案二把楼层请求放进 Floor,Home 只保留 <Floor></Floor>。这样楼层组件更独立,首页结构更清晰。核心是:mounted 派发 product/getFloorListAsync,模板循环 $store.state.product.floorList,每个 floorInfo 渲染一个楼层。注意避免重复请求、路径写错和 v-for 外层缺少包裹元素。
一、先定方案:接口一次性返回所有排行数据
为什么需要:商品排行有“热卖排行、特价排行、新品排行”等多组数据。如果接口一次性全部返回,用户拿到数据后切换 tab 只改前端状态,不再发请求,体验更流畅;缺点是第一次加载数据较多,首屏可能慢。本节先实现这种“一次性返回”的方案。
怎么想:把排行数据设计成数组,每个元素描述一个排行类型,里面再放 productList。组件只负责渲染和切换,不关心数据从哪来。
怎么做:在 src/mock/rank.json 中准备排行数据,保留笔记代码如下:
json
[
{
"id": 1,
"typeName": "热卖排行",
"productList": [
{
"id": 11,
"name": "农心(NONGSHIM)方便面经典多口味8连包 辛拉面辣白菜拉面多连包 8口味 经典八连包",
"imgUrl": "https://img12.360buyimg.com/seckillcms/s140x140_jfs/t1/112674/1/39503/144543/64af92b0Fdd48decc/b471ad6197a21ff1.jpg.avif",
"price": 28.8
},
{
"id": 12,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
},
{
"id": 13,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
},
{
"id": 14,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
}
]
},
{
"id": 2,
"typeName": "特价排行",
"productList": [
{
"id": 21,
"name": "千柏山 云南正宗黑糖500g大姨妈经期产妇月子手工老红糖块纯甘蔗熬制 云南黑糖500g",
"imgUrl": "https://img11.360buyimg.com/seckillcms/s140x140_jfs/t1/117890/25/37901/164674/64aeabddF5db4d94d/949501067d0c385b.jpg.avif",
"price": 11.8
},
{
"id": 22,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
},
{
"id": 23,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
},
{
"id": 24,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
}
]
},
{
"id": 3,
"typeName": "新品排行",
"productList": [
{
"id": 31,
"name": "千丝板栗味馅饼整箱320g办公室点心休闲早餐糕点小零食品年货 板栗味馅饼320g【约8个】",
"imgUrl": "https://img12.360buyimg.com/seckillcms/s140x140_jfs/t1/108157/15/41592/119490/64c6303eF0bebfe72/10fed046bebf8946.jpg.avif",
"price": 6
},
{
"id": 32,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
},
{
"id": 33,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
},
{
"id": 34,
"name": "【官网价直降1100】Apple iPhone 8 Plus 256GB 银色 移动联通电信4G手机",
"imgUrl": "/images/1.jpg",
"price": 100.98
}
]
}
]易错点:不要把“一次性返回”误写成“每次切换都请求”;首屏慢是这种方案的代价,切换流畅是它的收益。
二、在 Mock 中注册排行接口
为什么需要:后端接口还没好时,前端要先用 Mock 造数据,保证组件能跑通。
怎么想:Mock 的匹配条件是“请求方式 + 请求地址”。地址要和 axios 拼接后的地址一致,否则匹配不到。
怎么做:在 src/mock/index.js 中注册:
js
// 排行数据
Mock.mock("http://mock.com/product/rankList","get",{
ok:200,
data:rankList
});易错点:转写稿里强调,Mock 拦截后 Network 里可能看不到真实请求,不要以为请求没发;地址不一致时,要检查 baseURL 与接口路径拼接后的结果。请求方式必须是 get。
三、封装 API 与 Vuex 状态
为什么需要:组件不直接写请求,接口统一放 api,数据统一放 store,页面只取状态,后续维护更方便。
怎么想:API 只负责发请求;Vuex 按“state 存数据、mutation 改数据、action 发请求”三步走。
怎么做:src/api/product.js:
js
// 获取商品排行数据
export const getRankList = ()=>mockRequest("/product/rankList");src/store/product.js:
js
import {getBaseCategoryList, getFloorList, getRankList} from "@/api/product";
const state = {
rankList:[]
}
const mutations = {
SAVE_RANK_LIST(state,rankList){
state.rankList = rankList;
}
}
const actions = {
async getRankListAsync({commit}){
const {data} = await getRankList();
commit("SAVE_RANK_LIST",data);
}
}
export default {
namespaced:true,
state,
mutations,
actions
}易错点:action 里要解构出 data 再提交;mutation 名要一致;namespaced:true 后组件派发要写 product/getRankListAsync。转写稿中的调试思路是:先在 Vue Devtools 看 product/rankList 有没有值,再沿 action → api → mock 地址逐层排查。
四、组件渲染与 tab 切换
为什么需要:首页内容多,把排行拆成 RankList 组件,首页可读性更好。
怎么想:rankList 渲染 tab,activeIndex 记录当前选中项;内容区用 v-show 控制显示;mounted 时派发 action 取数据。
怎么做:src/pages/Home/RankList/index.vue:
vue
<template>
<div class="rank">
<div class="tab">
<div class="tab-tit clearfix">
<a @click="activeIndex=index" v-for="(item,index) in rankList" :key="item.id"
href="javascript:;"
:class="{on:activeIndex===index}">
<p class="img">
<i></i>
</p>
<p class="text">{{item.typeName}}</p>
</a>
</div>
</div>
<div v-show="activeIndex===index" class="content" v-for="(item,index) in rankList">
<ul>
<li>
<div v-for="info in item.productList" :key="info.id" class="img-item">
<p class="tab-pic">
<a href="#">
<img :src="info.imgUrl" />
</a>
</p>
<div class="tab-info">
<div class="info-title">
<a href="#">{{info.name}}</a>
</div>
<p class="info-price">定金:{{info.price}}</p>
</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import {mapState} from "vuex";
export default {
name: "RankList",
computed:mapState("product",["rankList"]),
data(){
return {
activeIndex:0
}
},
mounted() {
this.$store.dispatch("product/getRankListAsync");
}
}
</script>易错点:activeIndex 初始要为 0,否则首屏没有选中项;v-for 要写 :key;选中样式用 :class="{on:activeIndex===index}",不要写成普通 class;本节方案切换 tab 不再发请求,这是正常的。转写稿里还提醒:写一点测一点,先确认 Vuex 有数据,再看模板渲染,最后调样式和图片路径。
方案二的核心:切一次,取一次
为什么需要:商品排行有热卖、特价、新品三栏。方案一是进入页面就把三份数据全部取回,再用 v-if/v-show 切换;优点是切换快,缺点是初次请求数据多、渲染慢。方案二改为:默认只取热卖,用户点特价再取特价,点新品再取新品。这样初次渲染速度快,但每次切换都会发请求。
怎么想:把“当前选中类型”作为唯一状态 type,0 热卖、1 特价、2 新品;type 一变就 dispatch 对应 action,拿到最新数据后提交 mutation 更新 rankList2。
怎么做:先准备三份 Mock 数据,再注册三个 GET 接口,然后封装 API、写 Vuex,最后在 RankList.vue 中用 watch 监听 type 并加 immediate: true,保证首次进入也请求热卖。
先造 Mock 数据与接口
三个 JSON 结构一致,字段为 id/name/imgUrl/price。下面保留本节数据文件的关键内容(tejia.json、xinpin.json、remai.json 分别对应特价、新品、热卖):
json
// src/mock/tejia.json
[
{ "id": 21, "name": "千柏山 云南正宗黑糖500g...", "imgUrl": "https://img11.360buyimg.com/...jpg.avif", "price": 11.8 },
{ "id": 22, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 },
{ "id": 23, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 },
{ "id": 24, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 }
]json
// src/mock/xinpin.json
[
{ "id": 31, "name": "千丝板栗味馅饼整箱320g...", "imgUrl": "https://img12.360buyimg.com/...jpg.avif", "price": 6 },
{ "id": 32, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 },
{ "id": 33, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 },
{ "id": 34, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 }
]json
// src/mock/remai.json
[
{ "id": 11, "name": "农心(NONGSHIM)方便面经典多口味8连包...", "imgUrl": "https://img12.360buyimg.com/...jpg.avif", "price": 28.8 },
{ "id": 12, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 },
{ "id": 13, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 },
{ "id": 14, "name": "【官网价直降1100】Apple iPhone 8 Plus 256GB...", "imgUrl": "/images/1.jpg", "price": 100.98 }
]js
// src/mock/index.js
// 热卖排行
Mock.mock("http://mock.com/product/remai", "get", { ok: 200, data: remai });
// 新品排行
Mock.mock("http://mock.com/product/xinpin", "get", { ok: 200, data: xinpin });
// 特价排行
Mock.mock("http://mock.com/product/tejia", "get", { ok: 200, data: tejia });易错点:Mock 注册的地址必须和 mockRequest 拼接后的最终地址一致;请求方式必须是 get。转写稿里反复说“茂哥/Mock”“接头发/接口”,本质就是核对 Mock 拦截地址与 baseURL 拼接结果。
API 与 Vuex 的职责
为什么需要:组件不直接写请求地址,API 层集中封装;Vuex 统一保存排行数据,组件只负责展示和触发。
js
// src/api/product.js
export const getRemaiList = () => mockRequest("/product/remai");
export const getTeJiaList = () => mockRequest("/product/tejia");
export const getXinPinList = () => mockRequest("/product/xinpin");js
// src/store/product.js
import { getRemaiList, getTeJiaList, getXinPinList } from "@/api/product";
const state = {
rankList2: [] // 第二种方案
};
const mutations = {
SAVE_RANK_LIST2(state, rankList) {
state.rankList2 = rankList;
}
};
const actions = {
async getRankList2Async({ commit }, type) {
// type:0-remai 1-tejia 2-xinpin
let data;
if (type === 0) ({ data } = await getRemaiList());
else if (type === 1) ({ data } = await getTeJiaList());
else ({ data } = await getXinPinList());
commit("SAVE_RANK_LIST2", data);
}
};
export default { namespaced: true, state, mutations, actions };易错点:await 返回的是 { ok, data },要解构 data;mutation 名和提交名要一致;转写稿中曾出现“数据有了但页面没有”,最后发现是 rankList2 赋值写错,调试时要先看 Vuex 里 product.rankList2 是否真的有值。
RankList 的切换与监听
vue
<template>
<div class="rank">
<div class="tab">
<div class="tab-tit clearfix">
<a @click="type = 0" href="javascript:;" :class="{ on: type === 0 }">
<p class="img"><i></i></p><p class="text">热卖排行</p>
</a>
<a @click="type = 1" href="javascript:;" :class="{ on: type === 1 }">
<p class="img"><i></i></p><p class="text">特价排行</p>
</a>
<a @click="type = 2" href="javascript:;" :class="{ on: type === 2 }">
<p class="img"><i></i></p><p class="text">新品排行</p>
</a>
</div>
</div>
<div class="content">
<ul>
<li>
<div v-for="item in $store.state.product.rankList2" :key="item.id" class="img-item">
<p class="tab-pic">
<a href="#"><img :src="item.imgUrl" alt="商品图片" /></a>
</p>
<div class="tab-info">
<div class="info-title"><a href="#">{{ item.name }}</a></div>
<p class="info-price">定金:{{ item.price }}</p>
</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "RankList",
data() {
return { type: 0 };
},
watch: {
type: {
handler() {
this.$store.dispatch("product/getRankList2Async", this.type);
},
immediate: true
}
}
};
</script>易错点:immediate: true 不能漏,否则首次没有数据;v-for 要加 :key;on 类名用 type 判断;图片路径要能访问到。调试顺序是:看 type 是否变化 → 看 action 是否执行 → 看接口地址是否被 Mock 拦截 → 看 Vuex 是否有数据 → 最后看模板字段。这样写,就实现了“每次切换排行,均会发送请求获取最新的数据,初次渲染速度快”的方案二。
今日推荐:先让 mock 接口和请求地址对上
为什么需要:首页“今日推荐”的图片不能写死在页面里,否则后面换数据、换图片都要改模板。怎么想:先让 mock 提供一个“获取今日推荐”的接口,再让前端按接口请求;请求能通,再谈渲染。怎么做:在 mock/index.js 里加接口,请求方式必须是 get,地址要和前端最终发出的地址一致。转写中前端通过 axios 的 baseURL 与请求路径拼接,所以 mock 里看到的地址和代码里写的短路径可能“不一样”,但拼接后要相同。可整理为:
js
// mock/index.js
Mock.mock('/adv/today/list', 'get', {
code: 200,
data: [
{ id: 1, imgUrl: '/images/today01.png' },
{ id: 2, imgUrl: '/images/today02.png' },
{ id: 3, imgUrl: '/images/today03.png' },
{ id: 4, imgUrl: '/images/today04.png' }
]
})易错点:mock 地址与真实请求地址不一致;baseURL 拼接后路径多了或少了斜线;打开 Network 看不到 mock 请求,是因为 mock 已经把请求拦下来了,不是没发请求。
从 mock 到 API 封装:请求地址要能被追踪
为什么需要:组件里直接写请求地址,后期接口一改就要满项目搜。怎么想:所有广告相关请求集中到 src/api/adv.js,商品相关放到 src/api/product.js。怎么做:按转写整理:
js
// src/api/adv.js
export const reqTodayList = () => request.get('/adv/today/list')然后在 Vuex 的 adv 模块里写 state、mutations、actions,action 里 dispatch 到 API,拿到数据后 commit 更新状态。易错点:action 名、mutation 名、API 名三者写混;commit 时忘了传数据;把请求直接写在模板里,导致难调试。
组件拆分:今日推荐为什么单独成组件
为什么需要:首页内容越来越多,全塞在 home/index.vue 里可读性差。怎么想:按页面区块拆,今日推荐是一个区块,就建 today-recommend/index.vue。怎么做:把今日推荐的结构和样式剪到组件里,在首页引入;组件内 dispatch('getTodayList'),模板里 v-for 渲染图片:
vue
<div class="item" v-for="item in todayList" :key="item.id">
<img :src="item.imgUrl" alt="今日推荐商品">
</div>易错点:样式没跟着搬导致布局错;图片路径仍写旧目录;v-for 忘写 :key;组件拆分后还去父组件取数据。
楼层渲染:两种方案与数据流
为什么需要:家用电器、手机通讯等楼层结构一样,不能复制两遍模板。怎么想:数据是数组,每个元素描述一个楼层,就做一个 Floor 组件,父级遍历后传 item。怎么做:方案一在 home 里请求 floorList,再:
vue
<Floor v-for="item in floorList" :key="item.id" :item="item" />方案二把请求放进 Floor 组件内部,组件自己 dispatch 并遍历。转写强调两种都能用,前者数据流清晰,后者组件更独立。易错点:楼层图片放在 src 下但模板用绝对路径找不到,应放 public/images;v-for 的 key 用下标;子组件接收属性名与父组件传参名不一致。
商品排行:一次性返回与按需请求
为什么需要:热卖、特价、新品三个排行如果都写死,切换时没法更新。怎么想:方案一接口一次性返回全部排行,前端用 activeIndex/type 控制显示隐藏;方案二每次切换都发请求。怎么做:方案一在模板里遍历标题和商品,点击时改 type,内容区按 type 显示;方案二用 watch 监听 type,变化后 dispatch 对应接口:
js
watch: {
type() {
this.getRankList()
}
}易错点:一次性返回时忘了用 type 控制显示;按需请求时初始值没触发请求;结构赋值时变量重名,转写中通过加括号解决;mutation 名写错,导致 action 执行了但数据没进 state。
调试过程:写一点测一点
转写里反复强调:不要一口气写完再看。先看 action 有没有执行,再看 API 地址对不对,再看 mutation 有没有提交,最后看组件有没有拿到数据。图片不显示时,先确认资源是否在 public/images,再确认 imgUrl 拼接;排行不切换时,先看点击事件有没有改 type,再看 watch 是否触发。这样一层层找,比“全部写完再猜”快得多。
课堂画面





代码示例
示例一:楼层(方案一)——Home 里取数据,给 Floor 传 props
js
// src/mock/index.js
import Mock from "mockjs";
import floorList from "./floorList.json";
// 楼层数据
Mock.mock("http://mock.com/product/floorList", "get", {
ok: 200,
data: floorList
});js
// src/api/product.js
import mockRequest from "@/utils/mockRequest";
// 获取楼层信息
export const getFloorList = () => mockRequest("/product/floorList");js
// src/store/product.js
import { getBaseCategoryList, getFloorList } from "@/api/product";
const state = {
floorList: [] // 楼层数据信息
};
const mutations = {
SAVE_FLOOR_LIST(state, floorList) {
state.floorList = floorList;
}
};
const actions = {
async getFloorListAsync({ commit }) {
const { data } = await getFloorList();
commit("SAVE_FLOOR_LIST", data);
}
};
export default {
namespaced: true,
state,
mutations,
actions
};vue
<!-- src/pages/Home/Floor/index.vue -->
<template>
<div class="floor">
<div class="py-container">
<div class="title clearfix">
<h3 class="fl">{{ floorInfo.name }}</h3>
<div class="fr">
<ul class="nav-tabs clearfix">
<li :class="{ active: index === 0 }" v-for="(item, index) in floorInfo.navList" :key="index">
<a :href="item.url" data-toggle="tab">{{ item.text }}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<div class="tab-pane">
<div class="floor-1">
<div class="blockgary">
<ul class="jd-list">
<li v-for="(item, index) in floorInfo.keywords" :key="index">{{ item }}</li>
</ul>
<img :src="floorInfo.imgUrl" />
</div>
<div class="floorBanner">
<swiper class="swiper" :options="swiperOption">
<swiper-slide v-for="item in floorInfo.carouselList" :key="item.id">
<img :src="item.imgUrl" />
</swiper-slide>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[0]" />
</div>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[1]" />
</div>
</div>
<div class="split center">
<img :src="floorInfo.bigImg" />
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[2]" />
</div>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[3]" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from "vue-awesome-swiper";
import "swiper/css/swiper.css";
export default {
name: "Floor",
props: ["floorInfo"],
components: {
// 将 vue-awesome-swiper 提供的组件注册为当前组件
Swiper,
SwiperSlide
},
data() {
return {
// 配置项
swiperOption: {
// 显示多少屏
slidesPerView: 1,
// 屏之间的间隔
spaceBetween: 1,
// 是否循环
loop: true,
pagination: {
// 分页
el: ".swiper-pagination",
// 点击小圆点是否进行切换
clickable: true
},
autoplay: {
// 切换的间隔时间为3秒
delay: 3000,
// 如果设置为 true,当切换到最后一个 slide 时停止自动切换
stopOnLastSlide: false,
// 用户操作 swiper 之后,是否禁止 autoplay。默认为 true:停止。
disableOnInteraction: true
},
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev"
}
}
};
}
};
</script>vue
<!-- src/pages/Home/index.vue -->
<template>
<div>
<Floor v-for="item in $store.state.product.floorList" :floorInfo="item" :key="item.id"></Floor>
</div>
</template>
<script>
import Floor from "./Floor/index.vue";
export default {
name: "Home",
components: { Floor },
mounted() {
this.$store.dispatch("product/getFloorListAsync");
}
};
</script>关键行与易错点
Mock.mock("http://mock.com/product/floorList","get",...)的地址必须和请求拦截器里baseURL + 路径拼出来的最终地址一致,转写稿里特别强调过这点:mockRequest("/product/floorList")经过baseURL拼接后才是完整地址,两边对不上 mock 就拦不到,而且 mock 是拦在请求层的,Network 面板里看不到。props: ["floorInfo"]与父级:floorInfo="item"必须同名;楼层是数组,v-for一定要写:key="item.id",笔记里的 mock 数据每个楼层都有唯一id。- 模板里用到的字段名要和 JSON 完全一致:
name、navList[{url,text}]、keywords、imgUrl、carouselList[{id,imgUrl}]、recommendList[0..3]、bigImg,少一个就是空白或报错。 Swiper、SwiperSlide必须注册到components,并且import "swiper/css/swiper.css"不能漏,否则轮播图不动、箭头不显示。- 楼层有两个,结构完全一样,所以只写一个组件、循环渲染;数据在 Home 里 dispatch 一次即可。
示例二:商品排行方案一——接口一次性返回全部排行
js
// src/mock/index.js
import Mock from "mockjs";
import rankList from "./rank.json";
// 排行数据
Mock.mock("http://mock.com/product/rankList", "get", {
ok: 200,
data: rankList
});js
// src/api/product.js
// 获取商品排行数据
export const getRankList = () => mockRequest("/product/rankList");js
// src/store/product.js
import { getBaseCategoryList, getFloorList, getRankList } from "@/api/product";
const state = {
rankList: []
};
const mutations = {
SAVE_RANK_LIST(state, rankList) {
state.rankList = rankList;
}
};
const actions = {
async getRankListAsync({ commit }) {
const { data } = await getRankList();
commit("SAVE_RANK_LIST", data);
}
};
export default {
namespaced: true,
state,
mutations,
actions
};vue
<!-- src/pages/Home/RankList/index.vue -->
<template>
<div class="rank">
<div class="tab">
<div class="tab-tit clearfix">
<a
@click="activeIndex = index"
v-for="(item, index) in rankList"
:key="item.id"
href="javascript:;"
:class="{ on: activeIndex === index }"
>
<p class="img"><i></i></p>
<p class="text">{{ item.typeName }}</p>
</a>
</div>
</div>
<div v-show="activeIndex === index" class="content" v-for="(item, index) in rankList" :key="item.id">
<ul>
<li>
<div v-for="info in item.productList" :key="info.id" class="img-item">
<p class="tab-pic">
<a href="#"><img :src="info.imgUrl" /></a>
</p>
<div class="tab-info">
<div class="info-title">
<a href="#">{{ info.name }}</a>
</div>
<p class="info-price">定金:{{ info.price }}</p>
</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "RankList",
computed: mapState("product", ["rankList"]),
data() {
return {
activeIndex: 0
};
},
mounted() {
this.$store.dispatch("product/getRankListAsync");
}
};
</script>关键行与易错点
computed: mapState("product", ["rankList"])里第一个参数是模块名(store 里namespaced:true起的名字),写成mapState(["rankList"])就拿不到数据。- 切换用
v-show而不是v-if:方案一的意义就是数据已全部拿到、只做显示隐藏,用v-if反复销毁重建反而浪费。 :class="{ on: activeIndex === index }"是选中态,activeIndex初始 0,所以默认选中“热卖排行”;模板里的index来自v-for="(item,index)",两处v-for都要写:key。href="javascript:;"防止 a 标签跳转导致页面刷新丢状态。- 缺点写在方案里:首次加载数据量大,渲染慢;优点是一旦拿到,切换无请求、体验流畅。
示例三:商品排行方案二——每次切换发一次请求
js
// src/mock/index.js
import Mock from "mockjs";
import remai from "./remai.json";
import xinpin from "./xinpin.json";
import tejia from "./tejia.json";
// 热卖排行
Mock.mock("http://mock.com/product/remai", "get", { ok: 200, data: remai });
// 新品排行
Mock.mock("http://mock.com/product/xinpin", "get", { ok: 200, data: xinpin });
// 特价排行
Mock.mock("http://mock.com/product/tejia", "get", { ok: 200, data: tejia });js
// src/api/product.js
export const getRemaiList = () => mockRequest("/product/remai");
export const getTeJiaList = () => mockRequest("/product/tejia");
export const getXinPinList = () => mockRequest("/product/xinpin");js
// src/store/product.js
import {
getBaseCategoryList,
getFloorList,
getRankList,
getRemaiList,
getTeJiaList,
getXinPinList
} from "@/api/product";
const state = {
rankList2: [] // 第二种方案
};
const mutations = {
SAVE_RANK_LIST2(state, rankList) {
state.rankList2 = rankList;
}
};
const actions = {
async getRankList2Async({ commit }, type) {
// type:0-remai 1- tejia 2-xinpin
let data;
if (type === 0) ({ data } = await getRemaiList());
else if (type === 1) ({ data } = await getTeJiaList());
else ({ data } = await getXinPinList());
commit("SAVE_RANK_LIST2", data);
}
};
export default {
namespaced: true,
state,
mutations,
actions
};vue
<!-- src/pages/Home/RankList/index.vue -->
<template>
<div class="rank">
<div class="tab">
<div class="tab-tit clearfix">
<a @click="type = 0" href="javascript:;" :class="{ on: type === 0 }">
<p class="img"><i></i></p>
<p class="text">热卖排行</p>
</a>
<a @click="type = 1" href="javascript:;" :class="{ on: type === 1 }">
<p class="img"><i></i></p>
<p class="text">特价排行</p>
</a>
<a @click="type = 2" href="javascript:;" :class="{ on: type === 2 }">
<p class="img"><i></i></p>
<p class="text">新品排行</p>
</a>
</div>
</div>
<div class="content">
<ul>
<li>
<div v-for="item in $store.state.product.rankList2" :key="item.id" class="img-item">
<p class="tab-pic">
<a href="#"><img :src="item.imgUrl" /></a>
</p>
<div class="tab-info">
<div class="info-title">
<a href="#">{{ item.name }}</a>
</div>
<p class="info-price">定金:{{ item.price }}</p>
</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "RankList",
data() {
return {
type: 0
};
},
watch: {
type: {
handler() {
this.$store.dispatch("product/getRankList2Async", this.type);
},
immediate: true
}
}
};
</script>关键行与易错点
({ data } = await getRemaiList());外面这对括号不能省:前面已经let data,直接写{ data } = ...会被解析成语句块而报错,转写稿里专门演示了这个“已声明变量再解构赋值”的场景。- 用
watch监听type而不是在mounted里 dispatch:immediate: true让首次进入就用type=0拉一次热卖数据,之后点哪个 tab 就按哪个type发请求,逻辑只有一份。 - 三个分支都要把结果赋给同一个
data变量再commit("SAVE_RANK_LIST2", data),这样模板里只需要渲染rankList2一个数组,不用再按 tab 分开放。 - 每次切换都会重新发请求,所以会有短暂空白/加载感;这就是方案二的取舍——初次渲染快,但切换有延迟。要保留切换动画可以再套
vue-awesome-swiper或加 loading 态。 - 模板里
:key="item.id"用的是商品 id(11/12/13/14、21...),mock 数据里每个商品都有唯一 id,别用 index 当 key 造成复用错乱。
常见错误
请求 404,mock 不生效 现象:控制台报 404,或者返回的数据一直是 undefined。 原因:
mockRequest内部会拼接baseURL,而 mock 文件里注册的是完整地址http://mock.com/product/floorList,两处地址对不上,于是拦截规则匹配不到。 修法:mock 只认完整地址,api 层只传/product/floorList;改完地址后,先用console.log(data)验证一次,再往下写。打开 Network 面板却看不到请求 现象:以为请求没发出去,其实是 mock 已经拦掉了。 原因:Mock.js 重写了
XMLHttpRequest,请求不会走真实网络。 修法:判断数据是否到位,要看 Vuex Devtools 里的 state 或console.log,不要依赖 Network 面板;要排查地址问题,就临时注释掉 mock 再请求一次。楼层里有两个轮播,箭头只会控制第一个 现象:点第二个楼层的左右箭头,第一个楼层跟着切换;或者箭头全挤在第一个楼层。 原因:
navigation.nextEl: '.swiper-button-next'是全局选择器,页面上有多个 swiper 时,每个实例都会匹配到同一个 DOM。 修法:给每个 swiper 配唯一选择器(或用 ref 拿到实例),或者用<template v-slot:button-next>作用域插槽把按钮写进组件内部。引入 swiper 样式直接编译报错 现象:
Can't resolve 'swiper/css/swiper.css'。 原因:swiper/css/swiper.css是 swiper 5.x 的路径,装了 swiper 3/4 就对不上;vue-awesome-swiper的主版本和 swiper 主版本是绑定的。 修法:锁定vue-awesome-swiper@4+swiper@5;如果使用老版本,样式路径是swiper/dist/css/swiper.css。楼层图片全是裂图 现象:楼层渲染出来了,但图片全是 404。 原因:数据里给的是
/images/xxx.jpg这种以斜杠开头的路径,它指向 public 目录,而不是src/assets;或者服务端返回的图片本地根本没有。 修法:把图片统一放进public/images,路径保持/images/xxx.jpg;数据里缺图,就自己补图或复用已有图,不要在组件里写require去猜路径。改成方案二后
Cannot read property 'name' of undefined现象:模板里一访问floorInfo.name就报错。 原因:方案二在 Floor 组件内部自己 dispatch 取数据,父组件已经不传floorInfo了,但props: ["floorInfo"]和模板里的floorInfo还留着。 修法:两种方案不要混用;方案二要删掉 props,模板改成v-for="floorInfo in $store.state.product.floorList"并包一层根 div。排行区域进页面空白,点一下 tab 才出数据 现象:首屏是空的,手动点“特价”再点回“热卖”才显示。 原因:方案二靠
watch: { type() {...} }触发请求,而type初始值没有变化,watch 不会触发。 修法:watch加上immediate: true,或者把type写成对象形式{ handler, immediate: true };也可以在mounted里先手动 dispatch 一次。切换 tab 数据不更新,state 一直是空数组 现象:
dispatch没报错,但页面就是没数据。 原因:action 里commit的 mutation 名和mutations里定义的拼写不一致(比如SAVE_RANK_LIST/SAVE_RANK_LIST2混用),或者 dispatch 漏了模块名前缀product/。 修法:开启namespaced: true后,dispatch 必须带模块名;沿着 action → mutation → state 的顺序,一层层用 Devtools 打印排查,别一次写完再运行。解构赋值报语法错误 现象:
{data} = await getFloorList()报Unexpected token。 原因:变量已声明过,再解构赋值必须整体加括号,否则会被当成代码块。 修法:写成({ data } = await getFloorList());更稳妥的做法是const res = await ...; const data = res.data。
练习
把“今日推荐”按楼层方案二改造成组件自取数据:父组件只写
<TodayRecommend />,组件内mounted里 dispatch。改完后说明:删掉了哪几个 props、模板根节点需要怎么处理。让页面上的两个楼层轮播互不干扰:不用全局 class,给每个 swiper 配独立的选择器或 ref,验证点击任意一个楼层的箭头只切换自身。
把排行方案二的
type参数从数字改成字符串'remai' | 'tejia' | 'xinpin',用一张映射表替代 if-else 分支,并补上请求失败时的兜底(接口返回异常时不清空上一次数据)。写一个价格格式化方法(
28.8→¥28.80),在排行和楼层组件里复用,要求不重复定义、不污染全局。把 mock 里的楼层/排行数据从“本地图片路径”改成“完整网络图片地址”,改完检查:哪些文件必须动、哪些文件完全不用动?用一句话说明接口层和组件层的边界在哪。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 今日推荐 | 22:28 |
| 2 | 完成楼层 | 38:21 |
| 3 | 楼层方案二 | 04:12 |
| 4 | 商品排行一 | 27:23 |
| 5 | 商品排行二 | 24:42 |
小结
- mock 里写的是完整地址,api 层写的是相对路径,
baseURL负责拼起来;只要这两处有一处写错,就是 404,先对地址再怀疑组件。 - mock 拦掉的请求在 Network 里看不到,调试入口应该是 Vuex Devtools 和
console.log,不是浏览器网络面板。 - 楼层组件有两种做法:父组件传 props,或者子组件自取数据。选一种贯彻到底,混用就会出现
undefined和重复请求。 - 多实例组件(swiper)的配置里,凡是“选择器”都是全局的,页面上有第二个实例时必然冲突,要么唯一化,要么走作用域插槽。
- 数据驱动的页面排查顺序固定:接口 → api → action → mutation → state → 组件,逐层打印,写几行验一次,别写完一整块再跑。
- 切换类需求(排行 tab)本质是两种权衡:一次全量取,切换流畅但首屏重;按需取,首屏轻但每次切换都要请求,选哪种取决于接口怎么给。
- 图片这类静态资源,接口返回的是路径不是文件,路径落在 public 还是 assets 决定了能不能访问到,改数据前先确认文件在不在。
- 组件拆分的标准是“结构重复且数据独立”,不是“代码太长”;拆完父页面只负责编排,不应该再关心楼层内部长什么样。