前端Vue小兔鲜儿电商项目实战Day03
一、Home - 整体结构搭建和分类实现
1. 页面结构
①按照结构新增5个组件,准备最简单的模板,分别在Home模块的入口组件中引入
src/views/Home/components/
-
HomeCategory.vue
-
HomeBanner.vue
-
HomeNew.vue
-
HomeHot.vue
-
HomeProduct.vue
<script setup>
</script><template><div> HomeCategory </div>
</template>
②在Home模块入口组件index.vue中引入并渲染
<script setup>
import HomeCategory from './components/HomeCategory.vue'
import HomeBanner from './components/HomeBanner.vue'
import HomeNew from './components/HomeNew.vue'
import HomeHot from './components/HomeHot.vue'
import homeProduct from './components/HomeProduct.vue'
</script><template><div class="container"><HomeCategory /><HomeBanner /></div><HomeNew /><HomeHot /><homeProduct />
</template>
2. 分类实现
①静态结构搭建 - src/views/Home.components/HomeCategory.vue
<script setup></script><template><div class="home-category"><ul class="menu"><li v-for="item in 9" :key="item"><RouterLink to="/">居家</RouterLink><RouterLink v-for="i in 2" :key="i" to="/">南北干货</RouterLink><!-- 弹层layer位置 --><div class="layer"><h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4><ul><li v-for="i in 5" :key="i"><RouterLink to="/"><img alt="" /><div class="info"><p class="name ellipsis-2">男士外套</p><p class="desc ellipsis">男士外套,冬季必选</p><p class="price"><i>¥</i>200.00</p></div></RouterLink></li></ul></div></li></ul></div>
</template><style scoped lang='scss'>
.home-category {width: 250px;height: 500px;background: rgba(0, 0, 0, 0.8);position: relative;z-index: 99;.menu {li {padding-left: 40px;height: 55px;line-height: 55px;&:hover {background: $xtxColor;}a {margin-right: 4px;color: #fff;&:first-child {font-size: 16px;}}.layer {width: 990px;height: 500px;background: rgba(255, 255, 255, 0.8);position: absolute;left: 250px;top: 0;display: none;padding: 0 15px;h4 {font-size: 20px;font-weight: normal;line-height: 80px;small {font-size: 16px;color: #666;}}ul {display: flex;flex-wrap: wrap;li {width: 310px;height: 120px;margin-right: 15px;margin-bottom: 15px;border: 1px solid #eee;border-radius: 4px;background: #fff;&:nth-child(3n) {margin-right: 0;}a {display: flex;width: 100%;height: 100%;align-items: center;padding: 10px;&:hover {background: #e3f9f4;}img {width: 95px;height: 95px;}.info {padding-left: 10px;line-height: 24px;overflow: hidden;.name {font-size: 16px;color: #666;}.desc {color: #999;}.price {font-size: 22px;color: $priceColor;i {font-size: 16px;}}}}}}}// 关键样式 hover状态下的layer盒子变成block&:hover {.layer {display: block;}}}}
}
</style>
②数据渲染
<script setup>
import { useCategoryStore } from '@/stores/category.js'
const categoryStore = useCategoryStore()
</script><template><div class="home-category"><ul class="menu"><li v-for="item in categoryStore.categoryList" :key="item.id"><RouterLink to="/">{{ item.name }}</RouterLink><RouterLink v-for="i in item.children.slice(0, 2)" :key="i" to="/">{{i.name}}</RouterLink><!-- 弹层layer位置 --><div class="layer"><h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4><ul><li v-for="i in item.goods" :key="i.id"><RouterLink to="/"><img :src="i.picture" alt="" /><div class="info"><p class="name ellipsis-2">{{ i.name }}</p><p class="desc ellipsis">{{ i.desc }}</p><p class="price"><i>¥</i>{{ i.price }}</p></div></RouterLink></li></ul></div></li></ul></div>
</template>
二、Home - banner轮播图功能实现
Carousel 走马灯 | Element Plus
1. 组件静态结构搭建
src/views/Home/components/HomeBanner.vue
<script setup></script><template><div class="home-banner"><el-carousel height="500px"><el-carousel-item v-for="item in 4" :key="item"><imgsrc="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-15/6d202d8e-bb47-4f92-9523-f32ab65754f4.jpg"alt=""/></el-carousel-item></el-carousel></div>
</template><style scoped lang="scss">
.home-banner {width: 1240px;height: 500px;position: absolute;left: 0;top: 0;z-index: 98;img {width: 100%;height: 500px;}
}
</style>
2. 获取数据渲染组件
①封装接口 - src/apis/home.js
import instance from '@/utils/http.js'
export function getBannerAPI() {return instance({url: 'home/banner'})
}
②获取数据渲染模板 - HomeBanner.vue
<script setup>
import { getBannerAPI } from '@/apis/home'
import { ref } from 'vue'const bannerList = ref([])
const getBanner = async () => {const res = await getBannerAPI()// console.log(res)bannerList.value = res.result
}getBanner()
</script><template><div class="home-banner"><el-carousel height="500px"><el-carousel-item v-for="item in bannerList" :key="item.id"><img :src="item.imgUrl" alt="" /></el-carousel-item></el-carousel></div>
</template>
三、Home - 面板组件封装
1. 场景说明
组件封装解决了什么问题?
答:①复用问题;②业务维护问题
新鲜好物和人气推荐模块,在结构上非常相似,只是内容不同,通过组件封装可以实现复用结构的效果。
2. 组件封装
核心思路:把可复用的结构只写一次,把可能发生变化的部分抽象成组件参数(props / 插槽)
实现步骤:
①不做任何抽象,准备静态模板
②抽象可变的部分
- 主标题和副标题是纯文本,可以抽象成props传入
- 主体内容是复杂的模板,抽象成插槽传入
src/views/Home/components/HomePanel.vue
<script setup>
defineProps({title: {type: String,default: ''},subTitle: {type: String,default: ''}
})
</script><template><div class="home-panel"><div class="container"><div class="head"><!-- 主标题和副标题 --><h3>{{ title }}<small>{{ subTitle }}</small></h3></div><!-- 主体内容区域 --><slot></slot></div></div>
</template><style scoped lang="scss">
.home-panel {background-color: #fff;.head {padding: 40px 0;display: flex;align-items: flex-end;h3 {flex: 1;font-size: 32px;font-weight: normal;margin-left: 6px;height: 35px;line-height: 35px;small {font-size: 16px;color: #999;margin-left: 20px;}}}
}
</style>
四、Home - 新鲜好物和人气推荐实现
1. 新鲜好物
1. 准备模板
<script setup>
import HomePanel from './HomePanel.vue'
</script><template><home-panel title="新鲜好物" subTitle="新鲜好物 好多商品"> </home-panel><!-- 下面是插槽主体内容模版<ul class="goods-list"><li v-for="item in newList" :key="item.id"><RouterLink to="/"><img :src="item.picture" alt="" /><p class="name">{{ item.name }}</p><p class="price">¥{{ item.price }}</p></RouterLink></li></ul>-->
</template><style scoped lang="scss">
.goods-list {display: flex;justify-content: space-between;height: 406px;li {width: 306px;height: 406px;background: #f0f9f4;transition: all 0.5s;&:hover {transform: translate3d(0, -3px, 0);box-shadow: 0 3px 8px rgb(0 0 0 / 20%);}img {width: 306px;height: 306px;}p {font-size: 22px;padding-top: 12px;text-align: center;text-overflow: ellipsis;overflow: hidden;white-space: nowrap;}.price {color: $priceColor;}}
}
</style>
2. 准备接口 - src/apis/home.js
// 获取新鲜好物
export const getNewAPI = () => {return instance({url: '/home/new'})
}
3. 获取数据渲染模板 - HomeNew.vue
<script setup>
import HomePanel from './HomePanel.vue'
import { getNewAPI } from '@/apis/home'
import { ref } from 'vue'const newList = ref([])
const getNewList = async () => {const res = await getNewAPI()newList.value = res.result
}getNewList()
</script><template><home-panel title="新鲜好物" subTitle="新鲜好物 好多商品"><!-- 具名插槽 --><template #main><!-- 下面是插槽主体内容模版 --><ul class="goods-list"><li v-for="item in newList" :key="item.id"><RouterLink to="/"><img :src="item.picture" alt="" /><p class="name">{{ item.name }}</p><p class="price">¥{{ item.price }}</p></RouterLink></li></ul></template></home-panel>
</template>
把HomePanel.vue里的插槽改成 具名插槽
2. 人气推荐
1. 封装接口 - src/apis/home.js
// 获取人气推荐
export const getHotAPI = () => {return instance.get('home/hot')
}
2. 获取数据渲染模板 - src/views/Home/components/HomeHot.vue
<script setup>
import HomePanel from './HomePanel.vue'
import { getHotAPI } from '@/apis/home'
import { ref } from 'vue'const hotList = ref([])
const getHotList = async () => {const res = await getHotAPI()hotList.value = res.result
}
getHotList()
</script><template><HomePanel title="人气推荐" sub-title="人气爆款 不容错过"><!-- 具名插槽 --><template #main><ul class="goods-list"><li v-for="item in hotList" :key="item.id"><RouterLink to="/"><img :src="item.picture" alt="" /><p class="name">{{ item.title }}</p><p class="desc">{{ item.alt }}</p></RouterLink></li></ul></template></HomePanel>
</template><style scoped lang="scss">
.goods-list {display: flex;justify-content: space-between;height: 426px;li {width: 306px;height: 406px;transition: all 0.5s;&:hover {transform: translate3d(0, -3px, 0);box-shadow: 0 3px 8px rgb(0 0 0 / 20%);}img {width: 306px;height: 306px;}p {font-size: 22px;padding-top: 12px;text-align: center;}.desc {color: #999;font-size: 18px;}}
}
</style>
五、Home - 图片懒加载指令实现
1. 场景和指令用法
场景:电商网站的首页通常会很长,用户不一定会访问到页面靠下面的图片,这类图片通过懒加载优化手段可以做到只有进入视口区域才发送图片请求
指令用法:
<img v-img-lazy="item.picture" />
在图片img身上绑定指令,该图片只有在正式进入到视口区域时才会发送图片网络请求
2. 实现思路和步骤
核心原理:图片进入视口才发送资源请求
自定义指令:自定义指令 | Vue.js
useIntersectionObserver:useIntersectionObserver | VueUse
①定义全局指令 - main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'import App from './App.vue'
import router from './router'
// 引入初始化样式文件
import '@/styles/common.scss'
import { useIntersectionObserver } from '@vueuse/core'const app = createApp(App)app.use(createPinia())
app.use(router)app.mount('#app')// 定义全局指令
app.directive('img-lazy', {mounted(el, binding) {// el: 指定绑定的元素 img// binding: binding.value 指令等于号后面绑定的表达式的值 图片urlconsole.log(el, binding.value)useIntersectionObserver(el, ([{ isIntersecting }]) => {// console.log(isIntersecting)if (isIntersecting) {// 进入视口区域el.src = binding.valuestop()}})}
})
②组件中使用指令 - HomeHot.vue
<img v-img-lazy="item.picture" :src="item.picture" alt="" />
六、Home - 懒加载指令优化
问题1:逻辑书写位置不合理
问:懒加载指令的逻辑直接写到入口文件中,合理吗?
答:不合理,入口文件通常只做一些初始化的事情,不应该包含太多的逻辑代码,可以通过插件的方法把懒加载指令封装为插件,main.js入口文件只需要负责注册插件即可
①src/direactives/index.js
import { useIntersectionObserver } from '@vueuse/core'// 定义懒加载插件
export const lazyPlugin = {install(app) {// 懒加载指令逻辑// 定义全局指令app.directive('img-lazy', {mounted(el, binding) {// el: 指定绑定的元素 img// binding: binding.value 指令等于号后面绑定的表达式的值 图片urlconsole.log(el, binding.value)useIntersectionObserver(el, ([{ isIntersecting }]) => {// console.log(isIntersecting)if (isIntersecting) {// 进入视口区域el.src = binding.valuestop()}})}})}
}
②mian.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'import App from './App.vue'
import router from './router'
// 引入初始化样式文件
import '@/styles/common.scss'
// 引入懒加载指令插件并注册
import { lazyPlugin } from '@/direactives'const app = createApp(App)app.use(createPinia())
app.use(router)
app.use(lazyPlugin)app.mount('#app')
问题2:重复监听问题
useIntersectionObserver对于元素的监听是一直存在的,除非手动停止监听,存在内存浪费
解决思路:在监听的图片第一次完成加载之后就停止监听
const { stop } = useIntersectionObserver(el,([{ isIntersecting }]) => {if( isIntersecting) {el.src = binding.valuestop()}}
}
七、Home - Product产品列表实现
1. Product产品列表
Product产品列表是一个常规的列表渲染,实现步骤如下:
①封装接口 - src/apis/home.js
// 获取所有商品模块
export const getGoodsAPI = () => {return instance({url: '/home/goods'})
}
②获取数据并渲染 - src/views/Home/components/HomeProduct.vue
<script setup>
import HomePanel from './HomePanel.vue'
import { getGoodsAPI } from '@/apis/home'
import { ref } from 'vue'const goodsProduct = ref([])
const getGoods = async () => {const res = await getGoodsAPI()console.log(res)goodsProduct.value = res.result
}
getGoods()
</script><template><div class="home-product"><HomePanel v-for="cate in goodsProduct" :key="cate.id" :title="cate.name"><template #main><div class="box"><RouterLink class="cover" to="/"><!-- <img :src="cate.picture" /> --><img v-img-lazy="cate.picture" /><strong class="label"><span>{{ cate.name }}馆</span><span>{{ cate.saleInfo }}</span></strong></RouterLink><ul class="goods-list"><li v-for="good in cate.goods" :key="good.id"><RouterLink to="/" class="goods-item"><!-- <img :src="good.picture" alt="" /> --><img v-img-lazy="good.picture" alt="" /><p class="name ellipsis">{{ good.name }}</p><p class="desc ellipsis">{{ good.desc }}</p><p class="price">¥{{ good.price }}</p></RouterLink></li></ul></div></template></HomePanel></div>
</template><style scoped lang="scss">
.home-product {background: #fff;margin-top: 20px;.sub {margin-bottom: 2px;a {padding: 2px 12px;font-size: 16px;border-radius: 4px;&:hover {background: $xtxColor;color: #fff;}&:last-child {margin-right: 80px;}}}.box {display: flex;.cover {width: 240px;height: 610px;margin-right: 10px;position: relative;img {width: 100%;height: 100%;}.label {width: 188px;height: 66px;display: flex;font-size: 18px;color: #fff;line-height: 66px;font-weight: normal;position: absolute;left: 0;top: 50%;transform: translate3d(0, -50%, 0);span {text-align: center;&:first-child {width: 76px;background: rgba(0, 0, 0, 0.9);}&:last-child {flex: 1;background: rgba(0, 0, 0, 0.7);}}}}.goods-list {width: 990px;display: flex;flex-wrap: wrap;li {width: 240px;height: 300px;margin-right: 10px;margin-bottom: 10px;&:nth-last-child(-n + 4) {margin-bottom: 0;}&:nth-child(4n) {margin-right: 0;}}}.goods-item {display: block;width: 220px;padding: 20px 30px;text-align: center;transition: all 0.5s;&:hover {transform: translate3d(0, -3px, 0);box-shadow: 0 3px 8px rgb(0 0 0 / 20%);}img {width: 160px;height: 160px;}p {padding-top: 10px;}.name {font-size: 16px;}.desc {color: #999;height: 29px;}.price {color: $priceColor;font-size: 20px;}}}
}
</style>
八、Home - GoodsItem组件封装
1. 为什么要封装GoodsItem组件
在小兔鲜项目的很多个业务模块中都需要用到同样的商品展示模块,没必要重复定义,封装起来,方便复用。
2. 如何封装
核心思想:把要显示的数据对象设计为props参数,传入什么数据对象就显示什么数据
①封装组件 - src/views/Home/components/GoodsItem.vue
<script setup>
defineProps({good: {type: Object,default: () => {}}
})
</script><template><RouterLink to="/" class="goods-item"><img v-img-lazy="good.picture" alt="" /><p class="name ellipsis">{{ good.name }}</p><p class="desc ellipsis">{{ good.desc }}</p><p class="price">¥{{ good.price }}</p></RouterLink>
</template><style lang="scss" scoped>
.goods-item {display: block;width: 220px;padding: 20px 30px;text-align: center;transition: all 0.5s;&:hover {transform: translate3d(0, -3px, 0);box-shadow: 0 3px 8px rgb(0 0 0 / 20%);}img {width: 160px;height: 160px;}p {padding-top: 10px;}.name {font-size: 16px;}.desc {color: #999;height: 29px;}.price {color: $priceColor;font-size: 20px;}
}
</style>
②导入使用组件 - src/views/Home/components/HomeProduct.vue
<script setup>
import HomePanel from './HomePanel.vue'
import { getGoodsAPI } from '@/apis/home'
import { ref } from 'vue'
import GoodsItem from './GoodsItem.vue'const goodsProduct = ref([])
const getGoods = async () => {const res = await getGoodsAPI()console.log(res)goodsProduct.value = res.result
}
getGoods()
</script><template><div class="home-product"><HomePanel v-for="cate in goodsProduct" :key="cate.id" :title="cate.name"><template #main><div class="box"><RouterLink class="cover" to="/"><!-- <img :src="cate.picture" /> --><img v-img-lazy="cate.picture" /><strong class="label"><span>{{ cate.name }}馆</span><span>{{ cate.saleInfo }}</span></strong></RouterLink><ul class="goods-list"><li v-for="good in cate.goods" :key="good.id"><goods-item :good="good"></goods-item></li></ul></div></template></HomePanel></div>
</template><style scoped lang="scss">
.home-product {background: #fff;margin-top: 20px;.sub {margin-bottom: 2px;a {padding: 2px 12px;font-size: 16px;border-radius: 4px;&:hover {background: $xtxColor;color: #fff;}&:last-child {margin-right: 80px;}}}.box {display: flex;.cover {width: 240px;height: 610px;margin-right: 10px;position: relative;img {width: 100%;height: 100%;}.label {width: 188px;height: 66px;display: flex;font-size: 18px;color: #fff;line-height: 66px;font-weight: normal;position: absolute;left: 0;top: 50%;transform: translate3d(0, -50%, 0);span {text-align: center;&:first-child {width: 76px;background: rgba(0, 0, 0, 0.9);}&:last-child {flex: 1;background: rgba(0, 0, 0, 0.7);}}}}.goods-list {width: 990px;display: flex;flex-wrap: wrap;li {width: 240px;height: 300px;margin-right: 10px;margin-bottom: 10px;&:nth-last-child(-n + 4) {margin-bottom: 0;}&:nth-child(4n) {margin-right: 0;}}}}
}
</style>
九、一级分类 - 整体认识和路由配置
1. 准备分类组件
src/views/Category/index.vue
<template><div>我是分类页</div>
</template>
2. 配置路由
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',component: Layout,children: [{path: '',component: Home},{path: 'category/:id',component: Category}]},{path: '/login',component: Login}]
})export default router
3. 配置导航区域链接
src/views/Layout/components/LayoutHeader.vue 以及 LayoutFixed.vue
<!-- 导航区域 --><ul class="app-header-nav"><liclass="home"v-for="item in categoryStore.categoryList":key="item.id"><RouterLink :to="`/category/${item.id}`">{{ item.name }}</RouterLink></li></ul>
十、一级分类 - 面包屑导航渲染
Breadcrumb 面包屑 | Element Plus
1. 封装接口 - src/apis/category.js
import instance from '@/utils/http.js'// 获取分类数据
export const getTopCategoryAPI = (id) => {return instance({url: 'category',params: { id }})
}
2. 渲染面包屑导航 - src/views/Category/index.vue
<script setup>
import { getTopCategoryAPI } from '@/apis/category.js'
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'const categoryData = ref({})
const route = useRoute()const getCategory = async () => {const res = await getTopCategoryAPI(route.params.id)categoryData.value = res.result
}
getCategory()// 监听路由的变化
watch(route, () => {// 路由变化时,重新发请求getCategory()
})
</script><template><div class="top-category"><div class="container m-top-20"><!-- 面包屑 --><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item></el-breadcrumb></div></div></div>
</template><style scoped lang="scss">
.top-category {h3 {font-size: 28px;color: #666;font-weight: normal;text-align: center;line-height: 100px;}.sub-list {margin-top: 20px;background-color: #fff;ul {display: flex;padding: 0 32px;flex-wrap: wrap;li {width: 168px;height: 160px;a {text-align: center;display: block;font-size: 16px;img {width: 100px;height: 100px;}p {line-height: 40px;}&:hover {color: $xtxColor;}}}}}.ref-goods {background-color: #fff;margin-top: 20px;position: relative;.head {.xtx-more {position: absolute;top: 20px;right: 20px;}.tag {text-align: center;color: #999;font-size: 20px;position: relative;top: -20px;}}.body {display: flex;justify-content: space-around;padding: 0 40px 30px;}}.bread-container {padding: 25px 0;}
}
</style>
十一、一级分类 - banner轮播图实现
1. 分类轮播图实现
分类轮播图和首页轮播图的区别只有一个,接口参数不同,其余逻辑完全一致
①接口适配 - src/apis/home.js
// 获取banner
export function getBannerAPI(params = {}) {// 默认为1 商品为2const { distributionSite = '1' } = paramsreturn instance({url: 'home/banner',params: {distributionSite}})
}
②迁移首页Banner逻辑 - src/views/Category/index.vue
<script setup>
import { getTopCategoryAPI } from '@/apis/category.js'
import { getBannerAPI } from '@/apis/home.js'
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'const categoryData = ref({})
const route = useRoute()const getCategory = async () => {const res = await getTopCategoryAPI(route.params.id)categoryData.value = res.result
}
getCategory()// 监听路由的变化
watch(route, () => {// 路由变化时,重新发请求getCategory()
})// 获取banner
const bannerList = ref([])
const getBanner = async () => {const res = await getBannerAPI({distributionSite: '2'})console.log(res)bannerList.value = res.result
}getBanner()
</script><template><div class="top-category"><div class="container m-top-20"><!-- 面包屑 --><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item></el-breadcrumb></div><!-- 轮播图 --><div class="home-banner"><el-carousel height="500px"><el-carousel-item v-for="item in bannerList" :key="item.id"><img :src="item.imgUrl" alt="" /></el-carousel-item></el-carousel></div></div></div>
</template><style scoped lang="scss">
.top-category {h3 {font-size: 28px;color: #666;font-weight: normal;text-align: center;line-height: 100px;}.sub-list {margin-top: 20px;background-color: #fff;ul {display: flex;padding: 0 32px;flex-wrap: wrap;li {width: 168px;height: 160px;a {text-align: center;display: block;font-size: 16px;img {width: 100px;height: 100px;}p {line-height: 40px;}&:hover {color: $xtxColor;}}}}}.ref-goods {background-color: #fff;margin-top: 20px;position: relative;.head {.xtx-more {position: absolute;top: 20px;right: 20px;}.tag {text-align: center;color: #999;font-size: 20px;position: relative;top: -20px;}}.body {display: flex;justify-content: space-around;padding: 0 40px 30px;}}.bread-container {padding: 25px 0;}
}
.home-banner {width: 1240px;height: 500px;margin: 0 auto;img {width: 100%;height: 500px;}
}
</style>
十二、一级分类 - 激活状态显示和分类列表渲染
1. 激活状态显示
①src/views/Layout/components/LayoutHeader.vue以及LayoutFixed.vue
<RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
2. 分类列表渲染
分类的数据已经在面包屑导航实现的时候获取到了,只需要通过v-for遍历出来即可
src/views/Category/index.vue
<script setup>
import { getTopCategoryAPI } from '@/apis/category.js'
import { getBannerAPI } from '@/apis/home.js'
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import GoodsItem from '../Home/components/GoodsItem.vue'const categoryData = ref({})
const route = useRoute()const getCategory = async () => {const res = await getTopCategoryAPI(route.params.id)categoryData.value = res.result
}
getCategory()// 监听路由的变化
watch(route, () => {// 路由变化时,重新发请求getCategory()
})// 获取banner
const bannerList = ref([])
const getBanner = async () => {const res = await getBannerAPI({distributionSite: '2'})// console.log(res)bannerList.value = res.result
}getBanner()
</script><template><div class="top-category"><div class="container m-top-20"><!-- 面包屑 --><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item></el-breadcrumb></div><!-- 轮播图 --><div class="home-banner"><el-carousel height="500px"><el-carousel-item v-for="item in bannerList" :key="item.id"><img :src="item.imgUrl" alt="" /></el-carousel-item></el-carousel></div><!-- 分类列表渲染 --><div class="sub-list"><h3>全部分类</h3><ul><li v-for="i in categoryData.children" :key="i.id"><RouterLink to="/"><img :src="i.picture" /><p>{{ i.name }}</p></RouterLink></li></ul></div><divclass="ref-goods"v-for="item in categoryData.children":key="item.id"><div class="head"><h3>- {{ item.name }}-</h3></div><div class="body"><!-- 这里注意查看GoodsItem里的prop是什么,要对应 --><GoodsItem v-for="good in item.goods" :good="good" :key="good.id" /></div></div></div></div>
</template>
十三、一级分类 - 解决路由缓存问题
1. 什么是路由缓存问题
使用带有参数的路由时需要注意的是,当用户从/users/johnny导航到/users/jolyne时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。
问题:一级分类的切换正好满足上面的条件,组件实例复用,导致分类数据无法更新
解决问题的思路:1. 让组件实例不复用,强制销毁重建;2. 监听路有变化,变化之后执行数据更新操作。
2. 解决方案
方案一:给router-view添加key
以当前路由完整路径为key的值,给router-view组件绑定,破坏缓存
src/views/Layout/index.vue
<script setup>
import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
import LayoutFixed from './components/LayoutFixed.vue'
import { useCategoryStore } from '@/stores/category.js'// 触发获取导航列表的action
const categoryStore = useCategoryStore()
// 一进页面就调用
categoryStore.getCategory()
</script><template><LayoutFixed /><LayoutNav /><LayoutHeader /><!-- 给二级路由出口添加key,破坏复用机制,强制销毁重建 --><RouterView :key="$route.fullPath" /><LayoutFooter />
</template>
方案二:使用onBeforeRouteUpdate钩子函数,做精确更新
API 文档 | Vue Router
onBeforeRouteUpdate钩子函数可以再每次路由更新之前执行,在回调中执行需要数据更新的业务逻辑即可,或者使用onBeforeRouteUpdate导航守卫,它也可以取消导航
src/views/Category/index.vue
<script setup>
import { getTopCategoryAPI } from '@/apis/category.js'
import { getBannerAPI } from '@/apis/home.js'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import GoodsItem from '../Home/components/GoodsItem.vue'
import { onBeforeRouteUpdate } from 'vue-router'const categoryData = ref({})
const route = useRoute()const getCategory = async (id = route.params.id) => {const res = await getTopCategoryAPI(id)categoryData.value = res.result
}
getCategory()// 目标:路由参数变化时,把分类数据接口重新发送
onBeforeRouteUpdate((to) => {// console.log('路由变化了')// 使用最新的路由参数请求最新的分类数据getCategory(to.params.id)
})// 获取banner
const bannerList = ref([])
const getBanner = async () => {const res = await getBannerAPI({distributionSite: '2'})// console.log(res)bannerList.value = res.result
}getBanner()
</script>
方案三:使用watch侦听器,监视路由的变化
<script setup>
import { getTopCategoryAPI } from '@/apis/category.js'
import { getBannerAPI } from '@/apis/home.js'
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import GoodsItem from '../Home/components/GoodsItem.vue'const categoryData = ref({})
const route = useRoute()const getCategory = async () => {const res = await getTopCategoryAPI(route.params.id)categoryData.value = res.result
}
getCategory()// 监听路由的变化
watch(route, () => {// 路由变化时,重新发请求getCategory()
})// 获取banner
const bannerList = ref([])
const getBanner = async () => {const res = await getBannerAPI({distributionSite: '2'})// console.log(res)bannerList.value = res.result
}getBanner()
</script>
3. 总结
①路由缓存问题产生的原因时什么?
答:路由只有参数变化时,会复用组件实例
②三种方案都可以解决路由缓存问题,如何选择呢?
答:如果在意性能问题,选择onBeforeUpdate,精细化控制;如果不在意性能问题,选择key,简单粗暴。
watch监听是即时触发的,可以在参数变化时立即获取最新数据,有利于实时更新页面展示;onBeforeUpdate在路由参数发生变化之后才会触发,因此可能存在一些延迟,不如watch及时。
十四、一级分类 - 使用逻辑函数拆分业务
概念理解:基于逻辑函数拆分业务是指把同一个组件中独立的业务代码通过函数做封装处理,提升代码的可维护性。
实现步骤:
①按照业务声明以`use`开头的逻辑函数
②把独立的业务逻辑封装到各个函数内部
③函数内部组件中需要用到的数据或者方法return出去
④在组件中调用函数把数据或者方法组合回来使用
src/views/Category/composables/useBanner.js
// 封装banner轮播图相关的业务代码
import { ref, onMounted } from 'vue'
import { getBannerAPI } from '@/apis/home'export function useBanner() {const bannerList = ref([])const getBanner = async () => {const res = await getBannerAPI({distributionSite: '2'})// console.log(res)bannerList.value = res.result}onMounted(() => getBanner())return {bannerList}
}
src/views/Category/composables/useCategory.js
// 封装分类数据业务相关代码
import { onMounted, ref } from 'vue'
import { getTopCategoryAPI } from '@/apis/category.js'
import { useRoute } from 'vue-router'
import { onBeforeRouteUpdate } from 'vue-router'export function useCategory() {// 获取分类数据const categoryData = ref({})const route = useRoute()const getCategory = async (id = route.params.id) => {const res = await getTopCategoryAPI(id)categoryData.value = res.result}onMounted(() => getCategory())// 目标:路由参数变化的时候 可以把分类数据接口重新发送onBeforeRouteUpdate((to) => {// 存在问题:使用最新的路由参数请求最新的分类数据getCategory(to.params.id)})return {categoryData}
}
src/views/Category/index.vue
<script setup>
import GoodsItem from '../Home/components/GoodsItem.vue'
import { useBanner } from './composables/useBanner'
import { useCategory } from './composables/useCategory'
const { bannerList } = useBanner()
const { categoryData } = useCategory()
</script>
相关文章:

前端Vue小兔鲜儿电商项目实战Day03
一、Home - 整体结构搭建和分类实现 1. 页面结构 ①按照结构新增5个组件,准备最简单的模板,分别在Home模块的入口组件中引入 src/views/Home/components/ HomeCategory.vue HomeBanner.vue HomeNew.vue HomeHot.vue HomeProduct.vue <script …...
ORACLE 查询SQL优化
1 使用EXPLAIN PLAN 使用EXPLAIN PLAN查看查询的执行计划,这可以帮助你理解查询是如何被Oracle执行的。基于执行计划,你可以确定是否存在索引缺失、不必要的全表扫描等问题。 以下是几种使用EXPLAIN PLAN的方法: 使用EXPLAIN PLAN FOR: 你可以…...

Ansible03-Ansible Playbook剧本详解
目录 写在前面5. Ansible Playbook 剧本5.1 YAML语法5.1.1 语法规定5.1.2 示例5.1.3 YAML数据类型 5.2 Playbook组件5.3 Playbook 案例5.3.1 Playbook语句5.3.2 Playbook1 分发hosts文件5.3.3 Playbook2 分发软件包,安装软件包,启动服务5.3.3.1 任务拆解…...

Qt-qrencode生成二维码
Qt-qrencode开发-生成二维码📀 文章目录 Qt-qrencode开发-生成二维码📀[toc]1、概述📸2、实现效果💽3、编译qrencode🔍4、在QT中引入编译为静态库的QRencode5、在Qt中直接使用QRencode源码6、在Qt中使用QRencode生成二…...
长安链使用Golang编写智能合约教程(三)
本篇主要介绍长安链Go SDK写智能合约的一些常见方法的使用方法或介绍 资料来源: 官方文档官方示例合约库 官方SDK接口文档 教程一:智能合约编写1 教程二:智能合约编写2 一、获取参数、获取状态、获取历史记录的方法解析 注意! …...

Vercel deploy- Nextjs project error-URL link-env variable
Vercel deploy- Nextjs project error-URL link-env variable Error Check Database URL Check next-auth URL NEXTAUTH_URLhttps://yourappname.vercel.app/ 依次排查可能性 Application error: a server-side exception has occurred (see the server logs for more in…...

Java | Leetcode Java题解之第123题买卖股票的最佳时机III
题目: 题解: class Solution {public int maxProfit(int[] prices) {int n prices.length;int buy1 -prices[0], sell1 0;int buy2 -prices[0], sell2 0;for (int i 1; i < n; i) {buy1 Math.max(buy1, -prices[i]);sell1 Math.max(sell1, b…...

Ubuntu22.04之扩展并挂载4T硬盘(二百三十三)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…...
Redis实现延迟队列
最近用到一个延迟消息的功能,第一时间想到使用MQ或者MQ的插件,因为数据量不大,所以尝试使用Redis来实现了,毕竟Redis也天生支持类似MQ的队列消费,所以,在这里总结了一下Redis实现延迟消息队列的方式。 一、…...

如何准确查找论文数据库?
在学术研究过程中,查找相关论文是获取最新研究成果、支持自己研究的重要途径。准确查找论文数据库不仅可以节省时间,还能确保找到高质量的学术资源。本文将介绍一些有效的方法和策略,帮助您准确查找论文数据库。 1. 选择合适的数据库 不同的…...

翻译《The Old New Thing》- What a drag: Dragging a virtual file (IStream edition)
What a drag: Dragging a virtual file (IStream edition) - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20080319-00/?p23073 Raymond Chen 2008年03月19日 拖拽虚拟文件(IStream 版本) 上一次,我们看…...

【FPGA】Verilog语言从零到精通
接触fpga一段时间,也能写点跑点吧……试试系统地康康呢~这个需要耐心但是回报巨大的工作。正原子&&小梅哥 15_语法篇:Verilog高级知识点_哔哩哔哩_bilibili 1Verilog基础 Verilog程序框架:模块的结构 类比:c语言的基础…...

unity打包的WebGL部署到IIS问题
部署之后会出错,我遇到的有以下几种; 进度条卡住不动 明明已经部署到了IIS上,为什么浏览网页的时候还是过不去或者直接报错。 进度条卡住不动的问题其实就是wasm和data的错误。 此时在浏览器上按F12进入开发者模式查看错误(下图…...
GPT-4o:人工智能的新里程碑
GPT-4o,作为OpenAI最新推出的人工智能技术,无疑在人工智能领域掀起了新一轮的浪潮。这款新型的语言模型不仅继承了GPT系列的核心优势,更在多个方面实现了突破性的进展。以下,我们将从版本间的对比分析、GPT-4o的技术能力以及个人整…...

发现一个ai工具网站
网址 https://17yongai.com/ 大概看了下,这个网站收集的数据还挺有用的,有很多实用的ai教程。 懂ai工具的可以在这上面找找灵感。...

第二十五章新增H5基础(以及视频~兼容)
1.HTML5中新增布局标签 HTML5新增了页眉,页脚,内容块等文档结构相关标签,可以使文档结构更加清晰明了。 1.新增的结构标签 1、<header>标签 定义文档或者文档中内容块的页眉。通常可以包含整个页面或一个内容区域的标题,…...
[英语单词] production quality
Our goal is to implement a production quality switch platform that supports standard management interfaces and opens the forwarding functions to programmatic extension and control. 说在openswitch的文档里有说这两词,含义是产品质量。是production修…...
windows安装nodeJs,以及常用操作
1. 官网(Node.js — Run JavaScript Everywhere (nodejs.org))下载想要安装的node版本 的安装包完成安装 2.环境变量设置: 系统变量: Path新增:D:\Program Files\nodejs (node安装目录) 3.设置淘宝源: npm config set registr…...

MySql part1 安装和介绍
MySql part1 安装和介绍 数据 介绍 什么是数据库,数据很好理解,一般来说数据通常是我们所认识的 描述事物的符号记录, 可以是数字、 文字、图形、图像、声音、语言等,数据有多种形式,它们都以经过数字化后存入计算机…...

SpringBoot打war包并配置外部Tomcat运行
简介 由于其他原因,我们需要使用SpringBoot打成war包放在外部的Tomcat中运行,本文就以一个案例来说明从SpringBoot打war包到Tomcat配置并运行的全流程经过 环境 SpringBoot 2.6.15 Tomcat 8.5.100 JDK 1.8.0_281 Windows 正文 一、SpringBoot配置打war包 第一步&a…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...