主题
完成底部渲染与首页轮播图功能实现(Lesson15 1/2)
本页对应 Lesson15,共 6 段视频,约 1 小时 39 分。
学习目标
- 能够根据当前路由配置控制底部组件的显示与隐藏,使登录、注册页面仅渲染底部局部内容,其他页面渲染完整底部内容。
- 能够使用 Swiper/Vue-Awesome-Swiper 在首页完成轮播图功能,并配置自动播放、循环、分页点、左右切换及图片数据渲染。
- 能够安装并配置 Mock.js,在入口文件中引入 Mock 配置,通过指定请求地址与请求方式拦截 Ajax 请求并返回模拟响应数据。
- 能够封装独立的 mockRequest 请求实例,配置基础地址与请求拦截/响应处理,并在业务组件中调用封装后的请求获取模拟数据。
- 能够按业务类型封装 API 模块,将轮播图接口统一管理并在组件中通过导入 API 方法发起请求。
- 能够为 Vuex 增加 adv 模块,定义 state、mutations、actions,开启命名空间,并通过 dispatch 获取数据、commit 更新状态后渲染轮播图。
知识点
底部渲染的需求与判断思路
为什么需要:登录、注册页面与其它页面的底部不一样。其它页面显示“购物指南、配送方式、支付方式、售后服务、特色服务、帮助中心”等完整底部;登录、注册页面只显示最下面的版权信息。
怎么想:不要把“登录/注册”写死在 Footer 里判断路径,否则以后新增类似页面还要改组件。更稳的做法是:在路由上标记“是否隐藏完整底部”,Footer 只读这个标记。
怎么做:先抽出版权部分为 CopyRight,再在 Footer 中用 v-if 控制完整底部,登录/注册路由的 meta 设置 isHideFooter: true。
易错点:meta 字段名要统一;v-if 要取反,因为字段表示“隐藏”;v-else 必须紧挨 v-if。
抽取 CopyRight 组件
为什么需要:版权部分在完整底部和登录/注册页都会出现,抽成组件可复用,避免复制两份。
怎么想:把原 Footer 中最下面的地址、备案号、帮助链接单独放到 src/components/CopyRight/index.vue,Footer 再引入。
怎么做:新建组件并保留原结构;图片标签补中文 alt。
易错点:组件 name、引入路径、样式 scoped 要对应;笔记中 CopyRight 路径与 Footer 引入路径存在不一致,落地时按实际目录调整。
vue
<template>
<div class="copyright">
<ul class="helpLink">
<li>关于我们
<span class="space"></span>
</li>
<li>联系我们
<span class="space"></span>
</li>
<li>关于我们
<span class="space"></span>
</li>
<li>商家入驻
<span class="space"></span>
</li>
<li>营销中心
<span class="space"></span>
</li>
<li>友情链接
<span class="space"></span>
</li>
<li>关于我们
<span class="space"></span>
</li>
<li>营销中心
<span class="space"></span>
</li>
<li>友情链接
<span class="space"></span>
</li>
<li>关于我们</li>
</ul>
<p>地址:北京市昌平区宏福科技园综合楼6层</p>
<p>京ICP备19006430号</p>
</div>
</template>
<script>
export default {
name: "CopyRight"
}
</script>
<style lang="less" scoped>
.copyright {
padding: 20px;
.helpLink {
text-align: center;
li {
display: inline;
.space {
border-left: 1px solid #666;
width: 1px;
height: 13px;
background: #666;
margin: 8px 10px;
}
}
}
p {
margin: 10px 0;
text-align: center;
}
}
</style>在 Footer 中用 v-if 控制完整底部
为什么需要:完整底部不是所有页面都要显示,登录/注册只要版权部分。
怎么想:Footer 里放两套结构:完整底部 + 版权;用路由 meta.isHideFooter 判断。字段为 true 时隐藏完整底部,只留 CopyRight。
怎么做:完整底部外层写 v-if="!$route.meta.isHideFooter",下面写 <CopyRight v-else></CopyRight>。
易错点:不要漏掉取反 !;v-else 必须紧邻;CopyRight 要注册;图片补中文 alt。
vue
<template>
<!-- 底部 -->
<div v-if="!$route.meta.isHideFooter" class="footer">
<div class="footer-container">
<div class="footerList">
<div class="footerItem">
<h4>购物指南</h4>
<ul class="footerItemCon">
<li>购物流程</li>
<li>会员介绍</li>
<li>生活旅行/团购</li>
<li>常见问题</li>
<li>购物指南</li>
</ul>
</div>
<div class="footerItem">
<h4>配送方式</h4>
<ul class="footerItemCon">
<li>上门自提</li>
<li>211限时达</li>
<li>配送服务查询</li>
<li>配送费收取标准</li>
<li>海外配送</li>
</ul>
</div>
<div class="footerItem">
<h4>支付方式</h4>
<ul class="footerItemCon">
<li>货到付款</li>
<li>在线支付</li>
<li>分期付款</li>
<li>邮局汇款</li>
<li>公司转账</li>
</ul>
</div>
<div class="footerItem">
<h4>售后服务</h4>
<ul class="footerItemCon">
<li>售后政策</li>
<li>价格保护</li>
<li>退款说明</li>
<li>返修/退换货</li>
<li>取消订单</li>
</ul>
</div>
<div class="footerItem">
<h4>特色服务</h4>
<ul class="footerItemCon">
<li>夺宝岛</li>
<li>DIY装机</li>
<li>延保服务</li>
<li>尚品汇E卡</li>
<li>尚品汇通信</li>
</ul>
</div>
<div class="footerItem">
<h4>帮助中心</h4>
<img src="./images/wx_cz.jpg" alt="微信二维码">
</div>
</div>
<CopyRight></CopyRight>
</div>
</div>
<CopyRight v-else></CopyRight>
</template>
<script>
import CopyRight from "@/components/Footer/CopyRight";
export default {
name: "Footer",
components: {CopyRight}
}
</script>在路由 meta 中标记隐藏底部
为什么需要:登录、注册只是少数页面,改路由比在 Footer 里判断多个路径更灵活。
怎么想:默认不写 isHideFooter 就是显示完整底部;只有登录、注册设置 true。
怎么做:在 src/router/index.js 对应路由加 meta。
易错点:meta 是路由配置字段,不是组件 data;$route.meta.isHideFooter 读取当前路由;新增类似页面时只加路由标记即可。
js
{
path:"/login",
component:Login,
meta:{
// 是否隐藏底部信息
isHideFooter:true
}
},
{
path:"/Register",
component:Register,
meta:{
// 是否隐藏底部信息
isHideFooter:true
}
},调试与易错点
先在首页看完整底部是否正常,再点登录、注册,预期只显示版权部分。若没变化,按顺序查:路由是否写了 meta.isHideFooter: true;Footer 是否取反;CopyRight 是否注册成功;引入路径是否与实际目录一致。转写稿里反复强调“为 true 要隐藏,所以要取反”,这是最容易写反的地方。另一个坑是 CopyRight 被写了两份,导致登录页出现重复版权;用 v-if / v-else 后只保留一个分支即可。
为什么用 Swiper 而不是手写轮播图
为什么需要:真实项目里手写轮播图效率低,兼容性、触摸滑动、分页和导航都要自己测,容易出问题。怎么想:用成熟插件 Swiper,再通过 vue-awesome-swiper 在 Vue2 中快速注册组件。怎么做:安装时指定版本,Vue2 对应 vue-awesome-swiper@4,它配套 swiper@5。易错点:不要直接装最新版,最新版多面向 Vue3;版本不匹配会出现组件注册或样式异常。
shell
cnpm install swiper@5 vue-awesome-swiper@4资源入口可参考:Vue 资源列表 https://github.com/vuejs/awesome-vue,awesome 仓库 https://github.com/surmon-china/vue-awesome-swiper,示例 https://v1.github.surmon.me/vue-awesome-swiper/。
先定位并抽出 FocusAdv 组件
为什么需要:首页内容多,轮播图应独立成组件,便于维护和数据接入。怎么想:转写中先在 Home 里搜索 banner1 等图片,定位到 class="center" 的区域,再把这一块抽出去。怎么做:新建 src/pages/Home/FocusAdv/index.vue,先在 Home 中引入并占位,确认图片能显示后再替换成 Swiper。易错点:图片相对路径层级要数清楚;抽组件后路径可能变化,先占位调试最稳。
轮播图组件代码
为什么需要:模板负责结构,脚本负责注册组件和配置,样式负责尺寸。怎么想:Swiper、SwiperSlide 由插件提供,必须注册到当前组件;分页和左右按钮通过 slot 放进 Swiper。怎么做:保留下面代码,图片 alt 已改为中文。易错点:忘记注册组件会报“未知组件”;忘记引入 swiper/css/swiper.css 会导致分页、箭头样式错乱。
vue
<template>
<div class="center">
<swiper class="swiper" :options="swiperOption">
<swiper-slide><img src="../../../assets/images/home/banner1.jpg" alt="首页轮播图一" /></swiper-slide>
<swiper-slide><img src="../../../assets/images/home/banner2.jpg" alt="首页轮播图二" /></swiper-slide>
<swiper-slide><img src="../../../assets/images/home/banner3.jpg" alt="首页轮播图三" /></swiper-slide>
<swiper-slide><img src="../../../assets/images/home/banner4.jpg" alt="首页轮播图四" /></swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
name: "FocusAdv",
components: {
Swiper,
SwiperSlide
},
data() {
return {
swiperOption: {
slidesPerView: 1,
spaceBetween: 1,
loop: false,
pagination: {
el: '.swiper-pagination',
clickable: true
},
autoplay: {
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
}
}
</script>
<style lang="less" scoped>
.center {
box-sizing: border-box;
width: 740px;
height: 100%;
padding: 5px;
float: left;
.swiper{
height:100%;
img{
width:100%;
}
}
}
</style>配置项逐项调试
为什么需要:配置项决定轮播行为,不能只复制不看效果。怎么想:转写中通过改值观察效果来理解配置。怎么做:slidesPerView:1 表示一屏一张;spaceBetween 是屏间距;loop:false 到最后不能继续右走,改 true 才循环;pagination.clickable:true 让小圆点可点击切换;autoplay.delay:3000 是 3 秒切换;disableOnInteraction:true 表示用户触碰、拖动、点分页后停止自动播放,改 false 则操作后重新启动。易错点:分页 el 的类名必须和模板一致;左右按钮不想要就删对应节点和配置;stopOnLastSlide 只在特定场景有意义,别和 loop 混淆。
图片与样式调整
为什么需要:轮播图容器高度、图片宽度不统一会出现空隙。怎么想:转写中先让 .swiper 高度 100%,再让 img 宽度 100%,发现右侧或高度不满再调容器。怎么做:检查 .center 宽高、.swiper 高度、img 宽度,必要时给图片设 display:block 或高度。易错点:scoped 下样式选择器要落在当前组件内;不要只改图片而忽略 Swiper 容器高度。
数据接入:mock、request、api、vuex
为什么需要:接口未出时不能干等,先用 mock 造数据。怎么想:转写中把轮播图图片从 assets 移到 public,让 mock 数据里的 /images/... 能直接访问;再封装 mockRequest、api/adv.js,最后在 Vuex 增加 adv 模块。怎么做:入口文件引入 mock,mock 中按“地址+请求方式”拦截并返回响应体;api 里暴露获取轮播图列表的方法;Vuex 中写 state、mutation、action,组件 dispatch 后渲染 list。易错点:mock 不在入口引入不生效;被 mock 拦截的请求在网络面板里看不到;取数据时注意是响应体的 data;Vuex 模块要注册,开启命名空间后调用要带模块名。
为什么需要 mock
前端开发经常遇到“页面先做、接口没来”的情况。转写稿里说得很直白:UI、后端、前端是并行的,不能一直等接口;没有接口时可以先写虚拟数据,等真接口出来再替换。mockjs 的作用就是帮助前端程序员快速创建 API 接口用于模拟数据。为什么需要?因为要保证页面渲染和请求流程能先跑通。怎么想?先和接口约定好地址、请求方式、响应体结构,再用 mock 顶上去。怎么做?先下载模块:
shell
cnpm install mockjs易错点:mock 只是开发阶段的模拟,不要把它当成正式后端;接口就绪后要能快速替换。
先想清楚 mock 拦截什么
Mock.mock 的第一个参数是地址,第二个参数是请求方式,第三个参数是响应体。只有请求地址和请求方式同时匹配,才会被拦截。转写稿特别提醒:被拦截下来的 ajax 请求(xhr、fetch)在网络面板中是看不到的,但控制台可以打印出返回内容。怎么想?组件里写 axios.get("/my"),mock 里就必须写 "/my" 和 "get"。怎么做?把地址、方法、响应体三者对齐。易错点:组件用 get、mock 写 post,不会命中;地址少写斜杠或路径不一致,也不会命中;网络面板看不到请求不代表 mock 失败,要去看 console.log 的返回值。
新建 mock 目录并编写接口
为什么需要单独建目录?因为模拟接口要集中管理,不能散落在组件里。怎么想?在 src 下新建 mock 目录,入口文件叫 index.js。怎么做?新建 src/mock/index.js:
js
import Mock from "mockjs";
import focusList from "./focus.json";
// 执行之后,那么在你发送ajax请求时会被拦截。
// 拦截的条件:
// 1- 请求地址为/my
// 2- 请求方式为get
// 注意:
// 1- mock接收的第三个参数是响应体,第一个参数是地址,第二个参数是请求方式
// 2- 被拦截下来的ajax请求(xhr,fetch)在网络中是无法查看的。
Mock.mock("/my","get",{
ok:1,
msg:"my->get->success",
data:focusList
})这里 focusList 来自 json 文件,最终作为响应体里的 data 返回。易错点:json 路径要写对;第三个参数是响应体,不要误以为它是配置项;Mock.mock 要被执行到,不能只定义不引入。
在项目入口引入 mock
为什么需要入口引入?因为 Mock.mock(...) 只有执行后才会注册拦截规则。怎么想?入口文件是项目启动必经之路,把 mock 放这里最稳。怎么做?在 src/index.js 中写:
js
import "@/mock";易错点:忘记引入时,mock 文件等于没执行,组件请求会走真实网络或直接失败;路径别名 @ 要确认项目已配置。
在组件里发请求验证
为什么要在组件里验证?因为最终要确认“请求地址 + 请求方式 + 响应体”能串起来。怎么想?在 mounted 里发请求,把返回内容打印出来。怎么做?在 src/views/Home/FocusAdv/index.vue 中:
js
mounted(){
axios.get("/my").then(value=>{
console.log(value.data);
})
}刷新后控制台能看到 { ok:1, msg:"my->get->success", data:... },说明请求被 mock 拦截并返回了模拟响应体。易错点:axios 要先引入;请求方法必须是 get;如果网络面板看不到 /my,不要慌,这是被 mock 拦截后的正常现象,重点看控制台输出。
调试过程与后续扩展
转写稿里的调试思路是:先看控制台有没有响应体,再看请求地址和方法是否与 Mock.mock 一致;如果接口多,可以进一步封装 mockRequest,统一 baseURL,再按业务建 api/adv.js,最后在 vuex 的 action 里调用接口、提交 mutation 渲染轮播图。为什么需要封装?避免每个组件都写完整地址。怎么想?mock 地址可以先写成完整域名,后续替换真接口时只改一处。怎么做?先保证本节基本用法跑通,再封装请求实例和 API 文件。易错点:封装后 baseURL 与 mock 地址不一致;从响应体取数据时忘记取 data 里的业务字段,而不是整个响应对象。
为什么要封装 mockRequest
为什么需要:首页轮播图属于广告位,真实接口还没就绪时不能干等;用 mockjs 先模拟接口,再封装一个独立的 axios 实例,后续换真实接口只改地址和请求模块。
怎么想:mock 负责“造接口”,mockRequest 负责“发请求”,request/index.js 负责“统一出口”,组件只关心调用。
怎么做:先把图片放到 public/images,再写 mock 接口、封装请求、统一导出、组件调用。
易错点:只写 mock 文件但入口没引入,接口不会生效;baseURL 与 Mock.mock 地址不一致也拦不到。
准备图片与 mock 接口
按笔记要求,将轮播图图片放置在 public/images 中,接口里用 /images/xxx 这类路径访问。
为什么需要:mock 返回的数据里包含图片地址,图片必须放在 public 下才能被直接访问。
怎么想:mock 数据是“响应体”,结构要和组件渲染所需结构一致。
怎么做:新建 src/mock/index.js:
js
import Mock from "mockjs";
import focusList from "./focus.json";
// 获取首页中轮播图的图片列表
// 请求方式:get
// 请求地址:
Mock.mock("http://mock.com/adv/focus","get",{
ok:200,
data:focusList
})易错点:Mock.mock 第一个参数是完整地址,第二个是请求方式,第三个才是响应体;请求方式要和组件里的 get 对上。按转写稿强调,还要在入口文件引入 mock,例如 import "@/mock";,否则 mock 代码不执行。
创建 mockRequest 实例
为什么需要:把 mock 请求单独封装,统一 baseURL、超时、进度条和响应体处理,不和真实请求混在一起。
怎么想:baseURL 设为 http://mock.com,组件里只写 /adv/focus,拼接后正好匹配 mock 地址。
怎么做:新建 src/request/mockRequest.js:
js
import axios from 'axios';
import nprogress from "nprogress";
import "nprogress/nprogress.css";
const mockRequest = axios.create({
baseURL:"http://mock.com",
timeout:5000
});
// 请求拦截
mockRequest.interceptors.request.use(config=>{
nprogress.start();// 开启进度条
return config;
});
// 响应拦截
mockRequest.interceptors.response.use(response=>{
nprogress.done();// 结束进度条
return response.data;// 返回响应体
},(err)=>{
nprogress.done();// 结束进度条
alert(err);
return new Promise(()=>{});// 返回一个状态为pending的Promise链
});
export default mockRequest;易错点:响应拦截器已经 return response.data,组件里再取 value.data 时,拿到的是 mock 响应体里的 data 字段;错误处理返回 pending Promise 是为了避免未捕获错误继续往下抛。
统一导出请求模块
为什么需要:组件不应该到处记具体请求文件路径,统一从 @/request 导入。
怎么想:真实请求 sphRequest 和模拟请求 mockRequest 并列导出。
怎么做:src/request/index.js:
js
import sphRequest from "@/request/sphRequest";
import mockRequest from "@/request/mockRequest";
export {
// 调用尚品汇相关接口
sphRequest,
mockRequest
}易错点:导出名和组件导入名要一致;路径别名 @ 要能正常解析。
组件中调用并验证
为什么需要:验证 mock 接口、请求封装、响应体层级是否全链路正确。
怎么想:组件只调用 mockRequest.get("/adv/focus"),打印 value.data 看轮播图数组。
怎么做:在 src/pages/Home/FocusAdv/index.vue 中:
vue
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import {mockRequest} from "@/request";
import 'swiper/css/swiper.css'
export default {
name: "FocusAdv",
components: {
// 将vue-awesome-swiper提供的组件注册为当前组件
Swiper,
SwiperSlide
},
data() {
return {
// 配置项
swiperOption: {
// 显示多少屏
slidesPerView: 1,
// 屏之间的间隔
spaceBetween: 1,
// 是否循环
loop: false,
pagination: {
// 分页
el: '.swiper-pagination',
// 点击小圆点是否进行切换
clickable: true
},
autoplay: {
// 切换的间隔时间为3秒
delay: 3000,
// 如果设置为true,当切换到最后一个slide时停止自动切换
stopOnLastSlide: false,
// 用户操作swiper之后,是否禁止autoplay。默认为true:停止。
// 如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
// 操作包括触碰(touch),拖动,点击pagination等。
disableOnInteraction: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
},
mounted(){
mockRequest.get("/adv/focus").then(value=>{
console.log(value.data);
})
}
}
</script>易错点:控制台能打印出数组,说明 mock 拦截成功;被 mock 拦截的 Ajax 在浏览器 Network 面板里通常看不到。若请求失败,依次检查:入口是否引入 mock、baseURL 是否为 http://mock.com、Mock.mock 地址是否为 http://mock.com/adv/focus、请求方式是否为 get、图片是否已放到 public/images。
为什么要把广告接口单独封装
为什么需要:首页轮播图属于广告位,后面还可能继续出现“品牌广告”“商品推广广告”等接口。如果每个组件里都直接写 mockRequest("/adv/focus"),地址一改就要满项目搜;而且组件会同时知道“请求库”和“接口地址”,职责变重。
怎么想:转写稿里反复强调,页面里大量工作就是“调接口、渲染数据”。组件只应该关心“我要广告轮播数据”,不关心这个数据是 mock 来的还是真实后端来的。所以把广告相关请求集中到 src/api/adv.js,组件只导入函数。
怎么做:在 src/api/adv.js 中从 @/request 引入 mockRequest,导出一个获取首页轮播图列表的函数:
js
// 提供与广告相关的接口
import {mockRequest} from "@/request";
// 获取首页中轮播图的图片列表
export const getFocusList = ()=> mockRequest("/adv/focus") ;易错点:@/request 的 @ 是项目别名,不要写成相对路径却少层级;导出用 export const,组件导入时必须用 { getFocusList };函数体是返回 mockRequest(...) 的结果,不要漏 return 或把大括号写成普通函数体却不返回。
先想清楚:组件只拿数据,不拼接口地址
为什么需要:转写稿中讲到,真实项目里 UI、后端、前端并行推进,接口没出来时前端不能干等,可以用 mock 数据先联调。封装 mockRequest 后,组件调用 /adv/focus,等真实接口确定,只改请求层或 API 层,组件尽量不动。
怎么想:api/adv.js 是“广告接口目录”,只放和广告相关的接口;商品、搜索等不要混进来。getFocusList 命名表达“获取焦点图列表”,返回 Promise。
怎么做:组件里通过 import {getFocusList} from "@/api/adv"; 引入,在 mounted 中调用,用 .then 接收结果。转写稿调试时先 console.log(value.data),确认 mock 拦截成功、响应体里的 data 能取到,再继续渲染。
易错点:不要误以为 value 就是数组;笔记里打印的是 value.data,说明响应体还有一层。地址 /adv/focus 必须和 mock 拦截地址一致,否则请求不会被拦截,控制台会报错或拿不到假数据。
在 FocusAdv 中接入 Swiper 并调用接口
为什么需要:轮播图不自己从零写,转写稿明确说真实项目一般用第三方插件,自己写兼容性和效率都差。这里使用 vue-awesome-swiper,它把 Swiper 封装成 Vue 组件,注册后即可在模板中使用。
怎么想:先注册 Swiper、SwiperSlide,再引入 Swiper 样式;swiperOption 是配置对象,控制显示屏数、间距、分页、自动播放和左右按钮。接口调用放在 mounted,因为此时组件已挂载,适合发请求。
怎么做:保留笔记代码:
vue
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import {getFocusList} from "@/api/adv";
import 'swiper/css/swiper.css'
export default {
name: "FocusAdv",
components: {
// 将vue-awesome-swiper提供的组件注册为当前组件
Swiper,
SwiperSlide
},
data() {
return {
// 配置项
swiperOption: {
// 显示多少屏
slidesPerView: 1,
// 屏之间的间隔
spaceBetween: 1,
// 是否循环
loop: false,
pagination: {
// 分页
el: '.swiper-pagination',
// 点击小圆点是否进行切换
clickable: true
},
autoplay: {
// 切换的间隔时间为3秒
delay: 3000,
// 如果设置为true,当切换到最后一个slide时停止自动切换
stopOnLastSlide: false,
// 用户操作swiper之后,是否禁止autoplay。默认为true:停止。
// 如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
// 操作包括触碰(touch),拖动,点击pagination等。
disableOnInteraction: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
},
mounted(){
getFocusList().then(value=>{
console.log(value.data);
})
}
}
</script>易错点:Swiper、SwiperSlide 没注册会报组件不存在;样式路径要按当前依赖版本写;pagination.el 和 navigation 的选择器必须在模板中存在对应元素;delay: 3000 单位是毫秒;disableOnInteraction: true 表示用户操作后停止自动播放。转写稿调试时先打印数据,不要急着渲染,确认接口层通了再往下做。
这一节要解决什么问题
首页轮播图属于广告位,数据不应写死在组件里。我们需要把“广告数据”放进 Vuex 的独立模块 adv,由 action 调接口、mutation 改状态,组件只负责渲染。这样轮播图数据和组件解耦,后面接真实接口时只改 API 层,不动组件结构。
为什么需要独立模块:首页除了商品数据,还有广告、品牌等数据,全塞进一个 store 会乱。怎么想:广告相关状态、修改方法、异步请求集中到 store/adv.js,再在 store/index.js 注册。怎么做:先写模块,再注册,再在组件里 dispatch。易错点:模块开启 namespaced: true 后,调用 action 必须带模块名,如 adv/getFocusListAsync,不能只写 getFocusListAsync。
定义 adv 模块
adv.js 中 state.focusList 初始为数组,避免组件渲染时取到 undefined。mutations.SAVE_FOCUS_LIST 只做同步赋值;actions.getFocusListAsync 负责异步请求,拿到响应体中的 data 后 commit。注意这里 const { data } = await getFocusList() 解构的是接口返回对象里的 data,不是整个响应。
js
import {getFocusList} from "@/api/adv";
const state = {
// 首页轮播图信息
focusList: []
};
const mutations = {
SAVE_FOCUS_LIST(state,focusList){
state.focusList = focusList;
}
};
const actions = {
async getFocusListAsync({commit}){
const {data} = await getFocusList();
commit("SAVE_FOCUS_LIST",data);
}
}
export default {
namespaced:true,
state,
mutations,
actions
}易错点:模块内部 commit("SAVE_FOCUS_LIST") 不用写 adv/;只有组件 dispatch 时才带命名空间。mutation 名要和 commit 名一致,大小写别写错。
在 store/index.js 注册模块
为什么需要注册:adv.js 只是导出对象,不注册不会进入 store。怎么想:和已有 product 模块并列。怎么做:import adv from "@/store/adv",放进 modules。易错点:路径、文件名大小写要一致;注册后可在 Vue Devtools 的 Vuex 面板看到 adv/focusList。
js
import Vue from "vue";
import Vuex from "vuex";
import product from "@/store/product";
import adv from "@/store/adv";
Vue.use(Vuex);
const store = new Vuex.Store({
modules:{
product,
adv
}
});
export default store;组件中渲染轮播图数据
为什么用 vue-awesome-swiper:转写稿强调真实项目一般不手写轮播图,兼容性和效率都差。Vue2 项目要选对应版本,安装时注意 vue-awesome-swiper 4.x 搭配 swiper 5.x,不要直接装最新版导致只支持 Vue3。组件里注册 Swiper、SwiperSlide,引入 swiper 样式;模板用 v-for 遍历 $store.state.adv.focusList,key 用 item.id,图片地址用 item.imgUrl。
vue
<template>
<div class="center">
<swiper class="swiper" :options="swiperOption">
<swiper-slide v-for="item in $store.state.adv.focusList" :key="item.id">
<img :src="item.imgUrl" />
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
name: "FocusAdv",
components: {
// 将vue-awesome-swiper提供的组件注册为当前组件
Swiper,
SwiperSlide
},
data() {
return {
// 配置项
swiperOption: {
// 显示多少屏
slidesPerView: 1,
// 屏之间的间隔
spaceBetween: 1,
// 是否循环
loop: false,
pagination: {
// 分页
el: '.swiper-pagination',
// 点击小圆点是否进行切换
clickable: true
},
autoplay: {
// 切换的间隔时间为3秒
delay: 3000,
// 如果设置为true,当切换到最后一个slide时停止自动切换
stopOnLastSlide: false,
// 用户操作swiper之后,是否禁止autoplay。默认为true:停止。
// 如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
// 操作包括触碰(touch),拖动,点击pagination等。
disableOnInteraction: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
},
mounted(){
this.$store.dispatch("adv/getFocusListAsync");
}
}
</script>怎么触发:mounted 中 dispatch("adv/getFocusListAsync"),action 请求成功后 commit,state 更新,模板自动重渲染。易错点:没注册 Swiper/SwiperSlide 会报未知组件;没引入 swiper CSS 分页和箭头样式会丢;容器没有宽高时图片可能不显示;focusList 为空时先检查 Vuex 面板和接口返回;图片 404 要检查 imgUrl 路径。转写稿还提到 mock 阶段请求可能被拦截,在网络面板看不到,可用 console 看返回;mock 入口要在项目入口引入,否则代码不执行。最后在 Devtools 看到 adv.focusList 有数据,再回到页面确认轮播图正常切换即可。
今日推荐的数据接口:先用 Mock 造出广告位数据
为什么需要:今日推荐是轮播图底部的广告推荐商品区,结构是“时钟图标 + 4 张商品图”。真实接口没就绪时,不能干等后台;先用 Mock 造出与最终接口一致的数据,页面就能先渲染、先联调样式。
怎么想:这个区域本质还是“调接口 → 拿数组 → v-for 渲染图片”。接口地址定为 /adv/topToday,请求方式 GET,响应体里给 ok:200 和 data 数组,每个元素包含 id、imgUrl。转写稿里反复强调:Mock 的拦截条件是“请求地址 + 请求方式”都匹配,所以 Mock 地址、api 封装地址、请求方法要一致。
怎么做:在 src/mock/index.js 中写:
js
// 获取轮播图底部的广告推荐商品
Mock.mock("http://mock.com/adv/topToday","get",{
ok:200,
data:[
{
id:1,
imgUrl:"https://img14.360buyimg.com/n7/jfs/t1/160926/36/39154/42521/64cb95fbF17e9f5e2/46825eec5d36a9ac.jpg"
},
{
id:2,
imgUrl:"https://img12.360buyimg.com/n7/jfs/t1/114451/20/35763/328474/64cc52aeF75e02e83/a679d35a86500c90.png"
},
{
id:3,
imgUrl:"https://img10.360buyimg.com/n7/jfs/t1/118684/21/39853/31096/649182eaF26d07904/02445a2475df50e1.jpg"
},{
id:4,
imgUrl:"https://img11.360buyimg.com/n7/jfs/t1/131081/19/39374/78410/64c77f21F7c291b6a/cb18f952320466fd.jpg"
}
]
})易错点:Mock 方法名写 "get",不要写成 "GET";Mock 文件必须在项目入口引入执行,否则不会拦截;请求地址要和 mockRequest 的 baseURL 拼出来一致,否则网络面板里看不到请求,页面也拿不到数据。
api 层:把“今日推荐”请求收口
为什么需要:组件里不要直接写请求地址和请求库。转写稿的推导是:接口按业务分文件,广告相关接口放 api/adv.js,以后换真实域名只改封装层。
怎么想:getTopToday 只负责“发请求”,不负责存数据、不负责渲染。它返回 Promise,store 的 action 里 await 它。
怎么做:在 src/api/adv.js 中:
js
// 获取轮播图底部的今日推荐
export const getTopToday = ()=> mockRequest("/adv/topToday");易错点:笔记省略了 mockRequest 的导入,实际要按你项目封装位置引入;如果 mockRequest 的 baseURL 是 http://mock.com,这里写 /adv/topToday 才能匹配 Mock。
Vuex 的 adv 模块:存列表、改列表、发请求
为什么需要:今日推荐列表是跨组件共享的广告数据,放进 Vuex 后,组件只负责 dispatch 和渲染,数据流清晰。
怎么想:state 存 topTodayList;mutation 只做同步赋值;action 发异步请求,拿到响应体中的 data 后 commit。转写稿特别提醒:const {data} = await getTopToday() 里的 data 是“响应体里的 data”,不是整个响应对象。
怎么做:在 src/store/adv.js 中:
js
import {getFocusList, getTopToday} from "@/api/adv";
const state = {
// 今日推荐
topTodayList:[],
};
const mutations = {
SAVE_TOP_TODAY_LIST(state,topTodayList){
state.topTodayList = topTodayList;
}
};
const actions = {
async getTopTodayAsync({commit}){
const {data} = await getTopToday();
commit("SAVE_TOP_TODAY_LIST",data);
}
}
export default {
namespaced:true,
state,
mutations,
actions
}易错点:namespaced:true 后,组件里必须写 adv/getTopTodayAsync;还要在 store/index.js 的 modules 里注册 adv,否则 dispatch 报 unknown action;mutation 名要和 commit 一致。
组件渲染:mounted 触发请求,v-for 渲染图片
为什么需要:今日推荐区域要固定显示时钟标题,再动态渲染商品图。用 v-for 渲染 topTodayList,以后接口换图不用改模板。
怎么想:mounted 是组件挂载后触发一次请求;模板里第一个 li 是时钟,后面的 li.banner 循环列表;key 用 item.id,图片地址绑定 item.imgUrl。
怎么做:src/pages/Home/TodayRecomment.vue:
vue
<template>
<div class="today-recommend">
<div class="py-container">
<ul class="recommend">
<li class="clock">
<div class="time">
<img src="../images/clock.png" alt="时钟图标" />
<h3>今日推荐</h3>
</div>
</li>
<li class="banner" v-for="item in $store.state.adv.topTodayList" :key="item.id">
<img :src="item.imgUrl" alt="今日推荐商品图片" />
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "TodayRecommend",
mounted(){
this.$store.dispatch("adv/getTopTodayAsync")
}
}
</script>
<style lang="less" scoped>
.today-recommend {
.py-container {
width: 1200px;
margin: 0 auto;
.recommend {
height: 165px;
background-color: #eaeaea;
margin: 10px 0;
display: flex;
.clock {
width: 16.67%;
background-color: #5c5251;
color: #fff;
font-size: 18px;
text-align: center;
.time {
padding: 30px 0;
}
h3 {
margin: 9px 0;
font-weight: 700;
font-size: 18px;
line-height: 30.06px;
}
}
.banner {
width: 20.83%;
img {
width: 100%;
height: 100%;
transition: all 400ms;
&:hover {
opacity: 0.8;
}
}
}
}
}
}
</style>易错点:mounted 里 dispatch 的命名空间不能漏;v-for 必须加 :key;imgUrl 是绝对地址,直接绑定即可;组件名 TodayRecommend 和文件名 TodayRecomment.vue 不完全一致时,引入/注册以实际文件路径为准,不要因为拼写差异找不到组件。
本节目标与整体拆分
这一节要完成“底部品牌广告”相关功能:一是底部品牌区的按页面显示控制,二是首页轮播广告的数据渲染。先带做底部和轮播图,再把 mock、请求封装、Vuex 串起来。为什么需要:真实项目中不能等接口和 UI 全齐再写;怎么想:先用静态结构和假数据跑通,再替换成接口数据;怎么做:拆组件、配路由 meta、用 Swiper、造 mock、封 api、进 Vuex;易错点:不要把登录/注册逻辑写死在 Footer 里,不要手写轮播图。
底部品牌广告:用路由 meta 控制显示
为什么需要:登录、注册页只显示底部一小部分品牌信息,其他页面显示完整底部;如果每个页面写死,新增页面就要改 Footer。怎么想:登录/注册是少数页面,改少数页面更划算,所以在路由上标记。怎么做:给登录、注册路由加 meta.hideBrand,Footer 里用 v-if 取反控制完整品牌区,v-else 显示简版。易错点:meta 默认是 undefined,判断时要取反;不要把完整底部复制两份后忘记拆子组件。
js
// router/index.js
{
path: '/login',
component: Login,
meta: { hideBrand: true }
}vue
<!-- Footer.vue -->
<template>
<div class="footer">
<BrandFull v-if="!$route.meta.hideBrand" />
<BrandMini v-else />
</div>
</template>轮播图:用 vue-awesome-swiper 而不是手写
为什么需要:手写轮播图要处理自动播放、循环、分页、左右箭头、兼容性,效率低且易出问题。怎么想:Vue2 项目用 vue-awesome-swiper 4.x,搭配 Swiper 5.x;不要直接装最新版,最新版可能只支持 Vue3。怎么做:安装指定版本,注册 Swiper、SwiperSlide,配置 slidesPerView、spaceBetween、loop、autoplay、pagination、navigation。易错点:组件没注册会报“未知组件”;loop 为 false 时最后一张不能继续右切;autoplay.disableOnInteraction 为 true 时用户操作后停止自动播放。
vue
<template>
<swiper :options="swiperOption">
<swiper-slide v-for="item in bannerList" :key="item.id">
<img :src="item.imageUrl" alt="轮播广告图" />
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
export default {
components: { Swiper, SwiperSlide },
data() {
return {
swiperOption: {
slidesPerView: 1,
spaceBetween: 30,
loop: true,
autoplay: { delay: 3000, disableOnInteraction: false },
pagination: { el: '.swiper-pagination', clickable: true },
navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' }
}
}
}
}
</script>mock 的基本使用:先造数据不空等
为什么需要:接口没出来时不能干等,否则页面无法推进。怎么想:用 Mock.js 拦截指定地址和请求方式的 XHR,返回假数据。怎么做:建 mock/index.js,用 Mock.mock(url, method, response);在入口 main.js 引入 ./mock。易错点:地址和请求方式必须完全匹配;mock 请求在 Network 里看不到;忘记在入口引入等于代码没执行。
js
// mock/index.js
import Mock from 'mockjs'
import bannerList from './bannerList.json'
Mock.mock('http://mock.com/adv/banner', 'get', {
code: 200,
data: bannerList
})js
// main.js
import './mock'封装 mockRequest 与 api 层
为什么需要:每个组件直接写完整地址会散乱,后期换真实域名难改。怎么想:用 axios 实例统一 baseURL,api 文件按业务分类。怎么做:建 utils/mockRequest.js,再建 api/adv.js 暴露 getBannerList。易错点:baseURL 和 mock 地址拼接要一致;不要在 api 里重复写 http://mock.com。
js
// utils/mockRequest.js
import axios from 'axios'
export default axios.create({ baseURL: 'http://mock.com' })js
// api/adv.js
import mockRequest from '@/utils/mockRequest'
export const getBannerList = () => mockRequest.get('/adv/banner')Vuex adv 模块与组件渲染
为什么需要:轮播数据要被组件消费,也方便后续扩展广告业务。怎么想:按模块管理,adv 模块存 bannerList,action 调 api,mutation 改状态。怎么做:store/modules/adv.js 写 state/mutations/actions,store/index.js 注册并开启 namespaced;组件 dispatch('adv/getBannerList'),用 mapState('adv', ['bannerList']) 渲染。易错点:开启命名空间后 dispatch 必须带模块名;mock 返回结构是 res.data.data,别少取一层;组件挂载时再发请求。
js
// store/modules/adv.js
import { getBannerList } from '@/api/adv'
export default {
namespaced: true,
state: { bannerList: [] },
mutations: {
SET_BANNER_LIST(state, list) { state.bannerList = list }
},
actions: {
async getBannerList({ commit }) {
const res = await getBannerList()
commit('SET_BANNER_LIST', res.data.data)
}
}
}js
computed: {
...mapState('adv', ['bannerList'])
},
mounted() {
this.$store.dispatch('adv/getBannerList')
}调试顺序与易错点汇总
先静态结构,再抽 FocusAdv 组件;替换 Swiper 后如果报错,先查组件注册;页面无图先查图片路径和 mock 地址;Vuex 无数据先查模块是否注册、是否开启命名空间、dispatch 是否带 adv/。反复强调:这类页面 80% 是“调接口、渲染数据”,不要害怕,按“结构→假数据→接口→状态管理”的顺序推进即可。
课堂画面






代码示例
示例 1:底部版权组件 src/components/Footer/CopyRight/index.vue
vue
<template>
<div class="copyright">
<ul class="helpLink">
<li>关于我们
<span class="space"></span>
</li>
<li>联系我们
<span class="space"></span>
</li>
<li>关于我们
<span class="space"></span>
</li>
<li>商家入驻
<span class="space"></span>
</li>
<li>营销中心
<span class="space"></span>
</li>
<li>友情链接
<span class="space"></span>
</li>
<li>关于我们
<span class="space"></span>
</li>
<li>营销中心
<span class="space"></span>
</li>
<li>友情链接
<span class="space"></span>
</li>
<li>关于我们</li>
</ul>
<p>地址:北京市昌平区宏福科技园综合楼6层</p>
<p>京ICP备19006430号</p>
</div>
</template>
<script>
export default {
name: "CopyRight"
}
</script>
<style lang="less" scoped>
.copyright {
padding: 20px;
.helpLink {
text-align: center;
li {
display: inline;
.space {
border-left: 1px solid #666;
width: 1px;
height: 13px;
background: #666;
margin: 8px 10px;
}
}
}
p {
margin: 10px 0;
text-align: center;
}
}
</style>要点解释与易错点
- 这个组件是「登录/注册页」和「其它页面」共用的那一小部分底部,所以必须单独抽成组件,不能在 Footer 里复制两份 DOM。
- 最后一个
<li>后面不能再跟<span class="space">,否则会出现多余竖线;这也是笔记里最后一个关于我们单独写的原因。 .space是行内元素,width/height对行内元素不生效,实际竖线是靠border-left撑出来的,别指望改宽高调间距,调margin即可。- 目录名要和引入路径一致:笔记中 Footer 里写的是
import CopyRight from "@/components/Footer/CopyRight",组件就应放在src/components/Footer/CopyRight/index.vue,路径写错会直接报「找不到模块」。
示例 2:底部组件 src/components/Footer/index.vue
vue
<template>
<!-- 底部 -->
<div v-if="!$route.meta.isHideFooter" class="footer">
<div class="footer-container">
<div class="footerList">
<div class="footerItem">
<h4>购物指南</h4>
<ul class="footerItemCon">
<li>购物流程</li>
<li>会员介绍</li>
<li>生活旅行/团购</li>
<li>常见问题</li>
<li>购物指南</li>
</ul>
</div>
<div class="footerItem">
<h4>配送方式</h4>
<ul class="footerItemCon">
<li>上门自提</li>
<li>211限时达</li>
<li>配送服务查询</li>
<li>配送费收取标准</li>
<li>海外配送</li>
</ul>
</div>
<div class="footerItem">
<h4>支付方式</h4>
<ul class="footerItemCon">
<li>货到付款</li>
<li>在线支付</li>
<li>分期付款</li>
<li>邮局汇款</li>
<li>公司转账</li>
</ul>
</div>
<div class="footerItem">
<h4>售后服务</h4>
<ul class="footerItemCon">
<li>售后政策</li>
<li>价格保护</li>
<li>退款说明</li>
<li>返修/退换货</li>
<li>取消订单</li>
</ul>
</div>
<div class="footerItem">
<h4>特色服务</h4>
<ul class="footerItemCon">
<li>夺宝岛</li>
<li>DIY装机</li>
<li>延保服务</li>
<li>尚品汇E卡</li>
<li>尚品汇通信</li>
</ul>
</div>
<div class="footerItem">
<h4>帮助中心</h4>
<img src="./images/wx_cz.jpg">
</div>
</div>
<CopyRight></CopyRight>
</div>
</div>
<CopyRight v-else></CopyRight>
</template>
<script>
import CopyRight from "@/components/Footer/CopyRight";
export default {
name: "Footer",
components: {CopyRight}
}
</script>配套的路由元信息(src/router/index.js,登录/注册两条路由):
js
{
path: "/login",
component: Login,
meta: {
// 是否隐藏底部信息
isHideFooter: true
}
},
{
path: "/Register",
component: Register,
meta: {
// 是否隐藏底部信息
isHideFooter: true
}
}要点解释与易错点
- 核心是
v-if="!$route.meta.isHideFooter":取反是关键。isHideFooter为true表示隐藏完整底部,只留CopyRight。 - 为什么用
isHideFooter而不是isShowFooter?因为「显示完整底部」的页面是多数,隐藏的只有登录/注册两个;默认undefined即false(不隐藏),其它页面一行都不用改。如果反过来设计,所有页面都得加meta,极易漏写。 v-if/v-else必须紧挨着,中间不能插其它元素或注释,否则v-else会失效。$route是全局注入的,不用 import;但如果你要监听路由变化做逻辑,需改用watch: { $route() {} }。- 样式(
.footer、.footerList、.footerItem)笔记中未给出,沿用项目里已有的底部样式表即可;<img src="./images/wx_cz.jpg">用的是相对路径,图片必须放在本组件同级images目录下,否则编译报错。
示例 3:首页轮播图 src/pages/Home/FocusAdv/index.vue
vue
<template>
<div class="center">
<swiper class="swiper" :options="swiperOption">
<swiper-slide v-for="item in $store.state.adv.focusList" :key="item.id">
<img :src="item.imgUrl" />
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
name: "FocusAdv",
components: {
// 将vue-awesome-swiper提供的组件注册为当前组件
Swiper,
SwiperSlide
},
data() {
return {
// 配置项
swiperOption: {
// 显示多少屏
slidesPerView: 1,
// 屏之间的间隔
spaceBetween: 1,
// 是否循环
loop: false,
pagination: {
// 分页
el: '.swiper-pagination',
// 点击小圆点是否进行切换
clickable: true
},
autoplay: {
// 切换的间隔时间为3秒
delay: 3000,
// 如果设置为true,当切换到最后一个slide时停止自动切换
stopOnLastSlide: false,
// 用户操作swiper之后,是否禁止autoplay。默认为true:停止。
// 如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
// 操作包括触碰(touch),拖动,点击pagination等。
disableOnInteraction: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
},
mounted(){
this.$store.dispatch("adv/getFocusListAsync");
}
}
</script>
<style lang="less" scoped>
.center {
box-sizing: border-box;
width: 740px;
height: 100%;
padding: 5px;
float: left;
.swiper{
height:100%;
img{
width:100%;
}
}
}
</style>配套的 Vuex 模块 src/store/adv.js 与接口 src/api/adv.js:
js
// src/api/adv.js
import {mockRequest} from "@/request";
export const getFocusList = () => mockRequest("/adv/focus");
// src/store/adv.js
import {getFocusList} from "@/api/adv";
const state = {
// 首页轮播图信息
focusList: []
};
const mutations = {
SAVE_FOCUS_LIST(state, focusList){
state.focusList = focusList;
}
};
const actions = {
async getFocusListAsync({commit}){
const {data} = await getFocusList();
commit("SAVE_FOCUS_LIST", data);
}
}
export default {
namespaced: true,
state,
mutations,
actions
}要点解释与易错点
- 版本必须锁死:
cnpm install swiper@5 vue-awesome-swiper@4。直接install装到的是支持 Vue3 的最新版,在 Vue2 项目里会报错或组件不渲染。 Swiper/SwiperSlide必须先在components里注册,否则模板里的<swiper>会被当成未知标签,控制台报「did you register the component correctly」。slot="pagination"、slot="button-prev"、slot="button-next"这三个 slot 名是固定写法,同时pagination.el: '.swiper-pagination'、navigation.nextEl/prevEl的类名必须与模板中的 class 完全一致,写错就不显示小圆点或箭头。autoplay写成对象形式({delay: 3000}),不要写成autoplay: true;disableOnInteraction: true表示用户手动拖动后停止自动播放,改成false才会操作完继续轮播。- 渲染数据的根路径是
$store.state.adv.focusList,adv就是模块的namespaced: true加在store/index.js的modules里注册的键名;dispatch("adv/getFocusListAsync")同理,命名空间前缀不能漏,否则 action 找不到。 const {data} = await getFocusList()中的data是mockRequest响应拦截器return response.data之后响应体里的 data,不是 axios 的响应体本身,解构层级容易写错导致focusList变成undefined。item.imgUrl用的是/images/xxx.jpg这种绝对路径时,图片要放到public/images下,而不是src/assets;放错位置会 404。
常见错误
- 登录/注册页仍显示完整底部 → 原因:
meta.isHideFooter未设置、字段名不一致,或Footer中v-if判断取反写错 → 修法:在/login、/Register的meta中设isHideFooter:true,并核对v-if="!$route.meta.isHideFooter"与v-else的CopyRight分支。 - 轮播图报
Unknown custom element: swiper/swiper-slide或样式错乱 → 原因:vue-awesome-swiper与swiper版本不匹配、组件未注册、CSS 未引入,或容器无宽高 → 修法:使用swiper@5 + vue-awesome-swiper@4,局部注册Swiper/SwiperSlide,引入swiper/css/swiper.css,给.swiper和图片设置宽高。 - mock 接口不生效,仍请求真实地址或 404 → 原因:入口未
import "@/mock",或Mock.mock的地址/方法与mockRequest的baseURL + 路径不一致 → 修法:入口引入 mock,保证Mock.mock("http://mock.com/adv/focus","get",...)与请求层完全匹配。 - Network 面板看不到 mock 请求,误判接口失败 → 原因:mock 拦截的 XHR/fetch 不会在网络面板中显示 → 修法:用响应数据、
console.log或 Vuex 状态验证,不要只依赖 Network。 - 轮播图图片 404 → 原因:mock 返回的
imgUrl以/开头,但图片未放到public/images或路径不一致 → 修法:将图片放入public/images,按实际 public 路径返回,如/images/home/banner1.jpg。 - Vuex 中 adv 数据不渲染 → 原因:模块未注册、未开启
namespaced却按adv/xxx调用、mutation 名或字段名不一致、组件未 dispatch → 修法:在store/index.js注册adv,设置namespaced:true,核对dispatch("adv/getFocusListAsync")、mutation 与模板字段。
练习
- 实现登录/注册页只显示
CopyRight,其他页面显示完整Footer;再新增一个“仅显示 CopyRight”的页面,要求用最少改动完成。 - 用 mock 提供
/adv/focus与/adv/topToday两个接口,封装mockRequest和api/adv.js,要求baseURL可统一切换。 - 将首页轮播图数据接入 Vuex 的
adv模块,要求开启namespaced,组件只dispatch,不直接写 axios。 - 调整 Swiper 配置:自动播放 3 秒、循环、用户操作后继续自动播放、隐藏左右箭头但保留分页。
- 排查题:轮播图空白且控制台提示
Unknown custom element,请写出排查步骤与修复方案。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 完成底部渲染 | 16:22 |
| 2 | 首页轮播图功能实现 | 38:27 |
| 3 | mock的基本使用 | 17:47 |
| 4 | 封装mockRequest | 10:28 |
| 5 | 封装api | 04:56 |
| 6 | vuex增加adv模块并渲染轮播图数据 | 11:01 |
小结
- 底部差异显示应由路由
meta控制,Footer内部用条件分支切换完整版与精简版。 - 轮播图优先使用成熟插件,Vue2 项目必须严格匹配
swiper与vue-awesome-swiper版本。 - mock 生效依赖入口引入、地址匹配、请求方式匹配,三者缺一不可。
- 请求层应统一封装
baseURL、拦截器、进度条和响应体返回,页面不要散写 axios。 - API 层按业务域拆分,组件通过 API 或 Vuex 取数,便于后续替换真实接口。
- Vuex 模块必须注册并注意
namespaced调用格式,action 调 API、mutation 改 state。 - 静态资源放
public时要使用匹配的绝对路径,避免图片 404。 - 调试 mock 不要只看 Network,应结合响应数据、控制台与 Vuex 状态定位问题。