目录
Layout-吸顶导航交互实现
吸顶交互
vueUse
Layout-Pinia优化重复请求
为什么要优化
如何优化
Home-懒加载指令优化
场景和指令用法
实现思路和步骤
回顾核心步骤代码
Home-懒加载指令优化
问题1:逻辑书写位置不合理
问题2:重复监听问题
一级分类-解决路由缓存问题
什么是路由缓存问题
方案一:给router-view添加key
方案二:使用beforeRouteUpdate导航钩子
一级分类-使用逻辑函数拆分业务
具体怎么做
核心思想总结
二级分类-商品列表实现
列表无限加载功能实现
定制路由行为解决什么问题
详情页-图片预览组件封装
通过小图切换大图实现
放大镜效果实现
放大镜效果实现 - 滑块跟随鼠标移动
放大镜效果实现 - 大图效果实现
放大镜效果实现 - 鼠标移入控制显隐
总结
详情页-通用组件统一注册全局
为什么要优化
Layout-吸顶导航交互实现
吸顶交互
要求:浏览器在上下滚动的过程中,如果距离顶部的滚动距离大于78px,吸顶导航显示,小于78px隐藏
代码实现
获取滚动距离 动态添加类名show 控制组件的显示和隐藏
<template>
<div class="app-header-sticky" :class="{ show: y > 78 }">
<div class="container">
<RouterLink class="logo" to="/" />
<!-- 导航区域 -->
<ul class="app-header-nav">
<li class="home" v-for="item in getCategoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</ul>
<div class="right">
<RouterLink to="/">品牌</RouterLink>
<RouterLink to="/">专题</RouterLink>
</div>
</div>
</div>
</template>
<style scoped lang='scss'>
.app-header-sticky {
width: 100%;
height: 80px;
position: fixed;
left: 0;
top: 0;
z-index: 999;
background-color: #fff;
border-bottom: 1px solid #e4e4e4;
// 此处为关键样式!!!
// 状态一:往上平移自身高度 + 完全透明
transform: translateY(-100%);
opacity: 0;
// 状态二:移除平移 + 完全不透明
&.show {
transition: all 0.3s linear;
transform: none;
opacity: 1;
}
.container {
display: flex;
align-items: center;
}
.logo {
width: 200px;
height: 80px;
background: url("@/assets/images/logo.png") no-repeat right 2px;
background-size: 160px auto;
}
.right {
width: 220px;
display: flex;
text-align: center;
padding-left: 40px;
border-left: 2px solid $xtxColor;
a {
width: 38px;
margin-right: 40px;
font-size: 16px;
line-height: 1;
&:hover {
color: $xtxColor;
}
}
}
}
.app-header-nav {
width: 820px;
display: flex;
padding-left: 40px;
position: relative;
z-index: 998;
li {
margin-right: 40px;
width: 38px;
text-align: center;
a {
font-size: 16px;
line-height: 32px;
height: 32px;
display: inline-block;
&:hover {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}
.active {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}
}
</style>
vueUse
1. 安装Vueuse
VueUse 的官方(https://vueuse.org/)的介绍说这是一个 Composition API 的工具集合,适用于 Vue 2.x 或者 Vue 3.x,用起来和 React Hooks 还挺像的。
VueUse 插件的安装
npm install @vueuse/core
useScroll
响应式获取滚动位置和状态。
<script setup lang="ts">
import { useScroll } from '@vueuse/core'
const el = ref<HTMLElement | null>(null)
const { x, y, isScrolling, arrivedState, directions } = useScroll(el)
</script>
<template>
<div ref="el"></div>
</template>
Layout-Pinia优化重复请求
为什么要优化
结论:俩个导航中的列表是完全一致的,但是要发送俩次网络请求,存在浪费。通过Pinia集中管理数据,再把数据给 组件使用
如何优化
Home-懒加载指令优化
场景和指令用法
场景:电商网站的首页通常会很长,用户不一定能访问到页面靠下面的图片,这类图片通过懒加载优化手段可以做到 只有进入视口区域才发送图片请求
指令用法:
在图片img身上绑定指令,该图片只有在正式进入到视口区域时才会发送图片网络请求
实现思路和步骤
核心原理:图片进入视口才发送资源请求
回顾核心步骤代码
app.directive注册全局指令(指令名称,对象【指令生命周期】)
传递给指令的值。例如在
v-my-directive="1 + 1"
中,值是2
。vueUes (useIntersectionObserver)监听是否进入视口区域
会重复监听 赋值完成后 调用返回值 stop停止监听
进入到视口区域在给图片的src赋值 src有值后才会发送情求实现懒加载
Home-懒加载指令优化
问题1:逻辑书写位置不合理
问:懒加载指令的逻辑直接写到入口文件中,合理吗?
答:不合理,入口文件通常只做一些初始化的事情,不应该包含太多的逻辑代码,可以通过插件的方法把懒加载指令 封装为插件,main.js入口文件只需要负责注册插件即可
问题2:重复监听问题
useIntersectionObserver对于元素的监听是一直存在的,除非手动停止监听,存在内存浪费 解决思路:在监听的图片第一次完成加载之后就停止监听
一个插件可以是一个拥有
install()
方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给app.use()
的额外选项作为参数:app.use执行 install 会被调用 传入app实例对象
// 定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
install(app) {
app.directive('img-lazy', {
mounted(el, binding) {
// el: 指令绑定的那个元素 img
// binding: binding.value 指令等于号后面绑定的表达式的值 图片url
console.log(el, binding.value);
// 会重复监听 复制完成后调用stop 停止监听
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
console.log(isIntersecting, '是否进入到视口区域');
if (isIntersecting) {
// 进入视口区域
el.src = binding.value
stop()
}
}
)
},
})
}
}
import { lazyPlugin } from '@/directives'
app.use(lazyPlugin)
一级分类-解决路由缓存问题
什么是路由缓存问题
题:一级分类的切换正好满足上面的条件,组件实例复用,导致分类数据无法更新 解决问题的思路:1. 让组件实例不复用,强制销毁重建 2. 监听路由变化,变化之后执行数据更新操作
方案一:给router-view添加key
以当前路由完整路径为key的值,给router-view组件绑定
方案二:使用beforeRouteUpdate导航钩子
beforeRouteUpdate钩子函数可以在每次路由更新之前执行,在回调中执行需要数据更新的业务逻辑即可
import { onBeforeRouteUpdate } from "vue-router";
// 路由参数变化的时候,可以将分类接口重新发送
onBeforeRouteUpdate((to) => {
console.log("路由变化了");
// 存在问题:使用最新的路由参数 请求最新的分类数据
getTopCategory(to.params.id);
});
<!-- 添加key 破坏副用机制 强制销毁重建 -->
<!-- <router-view :key="$route.fullPath"></router-view> -->
1. 路由缓存问题产生的原因是什么?
路由只有参数变化时,会复用组件实例
2. 俩种方案都可以解决路由缓存问题,如何选择呢?
在意性能问题,选择onBeforeUpdate, 精细化控制 不在意性能问题,选择key,简单粗暴
一级分类-使用逻辑函数拆分业务
概念理解
基于逻辑函数拆分业务是指把同一个组件中独立的业务代码通过函数做封装处理,提升代码的可维护性
具体怎么做
实现步骤:
1. 按照业务声明以 `use` 打头的逻辑函数
2. 把独立的业务逻辑封装到各个函数内部
3. 函数内部把组件中需要用到的数据或者方法return出去
4. 在组件中调用函数把数据或者方法组合回来使用
核心思想总结
1. 逻辑拆分的过程是一个拆分再组合的过程
2. 函数use打头,内部封装逻辑,return组件需要用到的数据和方法给组件消费
// 封装banner相关业务代码
import { getBannerAPI } from "@/apis/layout";
import { onMounted, ref } from "vue";
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
}
}
<script setup>
import { useBanner } from "@/composables/useBanner";
const { bannerList } = useBanner();
</script>
二级分类-商品列表实现
列表无限加载功能实现
核心实现逻辑:使用elementPlus提供的 v-infinite-scroll 指令监听是否满足触底条件,满足加载条件时让页数参数加 一获取下一页数据,做新老数据拼接渲染
基础思路
- 触底条件满足之后 page++,拉取下一页数据
- 新老数据做数组拼接
- 判断是否已经全部加载完毕,停止监听
在要实现滚动加载的列表上上添加
v-infinite-scroll
,并赋值相应的加载方法,可实现滚动到底部时自动执行加载方法。infinite-scroll-disabled 是否禁用
<div
class="body"
v-infinite-scroll="load"
:infinite-scroll-disabled="disabled"
>
<!-- 商品列表-->
<GoodsItem v-for="good in goodList" :goods="good" :key="good.id" />
</div>
// 加载更多
const disabled = ref(false)
const load = async () => {
console.log('加载更多数据咯')
// 获取下一页的数据
reqData.value.page++
const res = await getSubCategoryAPI(reqData.value)
goodList.value = [...goodList.value, ...res.result.items]
// 加载完毕 停止监听
if (res.result.items.length === 0) {
disabled.value = true
}
}
定制路由行为解决什么问题
在不同路由切换的时候,可以自动滚动到页面的顶部,而不是停留在原先的位置
如何配置:vue-router支持 scrollBehavior 配置项,可以指定路由切换时的滚动位置
详情页-图片预览组件封装
通过小图切换大图实现
思路:维护一个数组图片列表,鼠标划入小图记录当前小图下标值,通过下标值 在数组中取对应图片,显示到大图位置
放大镜效果实现
功能拆解
1. 左侧滑块跟随鼠标移动
2. 右侧大图放大效果实现
3. 鼠标移入控制滑块和大图显示隐藏
放大镜效果实现 - 滑块跟随鼠标移动
思路: 获取到当前的鼠标在盒子内的相对位置(useMouseInElement),控
制滑块跟随鼠标移动(left/top)
1. 有效移动范围内的计算逻辑
横向:100 < elementX < 300,left = elementX - 小滑块宽度一半
纵向: 100 < elementY < 300,top = elementY - 小滑块高度一半
2. 边界距离控制
横向:elementX > 300 left = 200 ,elementX < 100 left = 0
纵向:elementY > 300 top = 200, elementY < 100 top = 0
放大镜效果实现 - 大图效果实现
效果:为实现放大效果,大图的宽高是小图的俩倍
思路:大图的移动方向和滑块移动方向相反,且数值为2倍
放大镜效果实现 - 鼠标移入控制显隐
总结
<script setup>
import { ref, watch } from "vue";
import { useMouseInElement } from "@vueuse/core";
defineProps({
imageList: {
type: Array,
default: () => {},
},
});
// 图片列表
// const imageList = [
// "https://yanxuan-item.nosdn.127.net/d917c92e663c5ed0bb577c7ded73e4ec.png",
// "https://yanxuan-item.nosdn.127.net/e801b9572f0b0c02a52952b01adab967.jpg",
// "https://yanxuan-item.nosdn.127.net/b52c447ad472d51adbdde1a83f550ac2.jpg",
// "https://yanxuan-item.nosdn.127.net/f93243224dc37674dfca5874fe089c60.jpg",
// "https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg",
// ];
// 小图切换 大图显示
const curIndex = ref(0);
// 实现鼠标移入交互
const mouseEnterFn = (i) => (curIndex.value = i);
// 获取鼠标相对位置
const target = ref(null);
const { elementX, elementY, isOutside } = useMouseInElement(target);
// 控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top)
const left = ref(0);
const top = ref(0);
const positionX = ref(0);
const positionY = ref(0);
watch([elementX, elementY, isOutside], () => {
console.log("xy变化了");
// 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑
if (isOutside.value) return;
// 有效范围内控制滑块距离
// 横向
if (elementX.value > 100 && elementX.value < 300) {
left.value = elementX.value - 100;
}
// 纵向
if (elementY.value > 100 && elementY.value < 300) {
top.value = elementY.value - 100;
}
// 处理边界
if (elementX.value > 300) {
left.value = 200;
}
if (elementX.value < 100) {
left.value = 0;
}
if (elementY.value > 300) {
top.value = 200;
}
if (elementY.value < 100) {
top.value = 0;
}
// 控制大图的显示
positionX.value = -left.value * 2;
positionY.value = -top.value * 2;
});
</script>
<template>
<div class="goods-image">
<!-- 左侧大图-->
<div class="middle" ref="target">
<img :src="imageList[curIndex]" alt="" />
<!-- 蒙层小滑块 -->
<div
class="layer"
v-show="!isOutside"
:style="{ left: `${left}px`, top: `${top}px` }"
></div>
</div>
<!-- 小图列表 -->
<ul class="small">
<li
v-for="(img, i) in imageList"
:key="i"
@mouseenter="mouseEnterFn(i)"
:class="{ active: i === curIndex }"
>
<img :src="img" alt="" />
</li>
</ul>
<!-- 放大镜大图 -->
<div
class="large"
:style="[
{
backgroundImage: `url(${imageList[curIndex]})`,
backgroundPositionX: `${positionX}px`,
backgroundPositionY: `${positionY}px`,
},
]"
v-show="!isOutside"
></div>
</div>
</template>
<style scoped lang="scss">
.goods-image {
width: 480px;
height: 400px;
position: relative;
display: flex;
.middle {
width: 400px;
height: 400px;
background: #f5f5f5;
}
.large {
position: absolute;
top: 0;
left: 412px;
width: 400px;
height: 400px;
z-index: 500;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-repeat: no-repeat;
// 背景图:盒子的大小 = 2:1 将来控制背景图的移动来实现放大的效果查看 background-position
background-size: 800px 800px;
background-color: #f8f8f8;
}
.layer {
width: 200px;
height: 200px;
background: rgba(0, 0, 0, 0.2);
// 绝对定位 然后跟随咱们鼠标控制left和top属性就可以让滑块移动起来
left: 0;
top: 0;
position: absolute;
}
.small {
width: 80px;
li {
width: 68px;
height: 68px;
margin-left: 12px;
margin-bottom: 15px;
cursor: pointer;
&:hover,
&.active {
border: 2px solid $xtxColor;
}
}
}
}
</style>
详情页-通用组件统一注册全局
为什么要优化
背景:components目录下有可能还会有很多其他通用型组件,有可能在多个业务模块中共享,所有统一进行全局组件 注册比较好
// 把commponents中的组件进行全局注册
// 通过插件的方式
import imageView from "@/components/imageView/index.vue";
import sku from "@/components/XtxSku/index.vue";
export const commponentsPlugin = {
install(Vue) {
// Vue.component(组件名字, 组件对象)
Vue.component('imageView', imageView)
Vue.component('sku', sku)
}
}
import { commponentsPlugin } from '@/components/index'
app.use(commponentsPlugin)