1.Pinia优化重复请求
在项目中,吸顶组件和首页的头部内容是一样的,所以不用发送两次请求。
通过Pinia集中管理数据,再把数据给组件使用。
只需要把请求封装在一个store里,调用即可使用。
2.面板组件的封装
由于项目中的新鲜好物和人气推荐结构一样,内容不同,所以可以通过组件封装来实现两个组件。
核心:把不变的只写一次,把变化的抽象为组件参数(props/插槽)
纯展示类组件通用封装思路总结:1. 搭建纯静态的部分,不管可变的部分2. 抽象可变的部分为组件参数 :非复杂的模版抽象成props,复杂的结构模版抽象为插槽。
3. 图片懒加载
当用户进入首页的时候,底部的图片不加载出来,当进入视口区域了,才加载图片。
步骤:
1)使用自定义指令,自定义懒加载指令(v-img-lazy)
//定义全局指令
app.directive('img-lazy',{
mounted(el,binding){
//el:指令绑定的那个元素
//binding:binding.value 指令等于号后面绑定的表达式的值 图片url
//console.log(el,binding.value)
}
})
2)使用VueUse中的useIntersectionObserver函数来判断是否进入视口区
//引入vueuse中的useIntersectionObserver函数
import {useIntersectionObserver} from '@vueuse/core'
app.directive('img-lazy',{
mounted(el,binding){
console.log(el,binding.value)
useIntersectionObserver(
el,
([{isIntersecting}])=>{
// isIntersecting是一个布尔值
console.log(isIntersecting)
if(isIntersecting){
//进入了视口区域,需要发送请求获取图片
el.src=binding.value
}
}
)
}
})
3)在组件中使用懒加载指令
4)手动停止监听。
由于 useIntersectionObserver 这个函数会一直监听是否进入视口区域,需要我们手动停止。
使用 useIntersectionObserver 函数中的 stop方法 ,当图片第一次加载完毕后,停止监听。
4.编写一个插件
在本项目中,把懒加载指令封装为了一个插件。
步骤:
1)在一个文件中定义插件
//定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin={
install(app){
//懒加载指令逻辑代码
}
}
2)在main.js中注册懒加载指令
//引入懒加载指令插件
import {lazyPlugin} from '文件'
//注册懒加载插件
const app = createApp(App)
app.use(lazyPlugin)
5.一级分类的轮播图
一级分类轮播图和首页的轮播图一样,只是接口的参数不同。
所以,可以共用一个接口,只需要把封装的接口适配参数即可。
步骤:
1)封装轮播图的接口(一级分类和首页使用)
import httpInstance from "@/utils/http";
export function getBannerAPI(params={}){
// 首页为1 一级分类为2
// 默认是首页
const {distributionSite='1'}=params
return httpInstance({
url:'/home/banner',
params:{distributionSite}
})
}
2)获取数据(首页轮播图是默认的,所以参数可以不写;一级分类参数{ distributionSite: "2" })
//首页轮播图
<script setup>
import {getBannerAPI} from '@/apis/home'
import {onMounted, ref} from 'vue'
const bannerList=ref([])
const getBanner=async()=>{
const res=await getBannerAPI()
console.log(res);
bannerList.value=res.result
}
onMounted(()=>getBanner())
</script>
//一级分类轮播图
<script setup>
import {getBannerAPI} from '@/apis/home'
import {onMounted, ref} from 'vue'
const bannerList=ref([])
const getBanner=async()=>{
const res=await getBannerAPI({ distributionSite: "2" })
console.log(res);
bannerList.value=res.result
}
onMounted(()=>getBanner())
</script>
3)渲染数据即可
6.路由缓存问题
当路由只有参数变化时,会复用组件,导致生命周期钩子无法执行,所以数据无法更新。
解决方法:
7.使用逻辑函数拆分业务
实现步骤:1. 按照业务声明以 `use` 打头 的逻辑函数2. 把 独立的业务逻辑 封装到各个函数内部3. 函数内部把组件中需要用到的数据或者方法 return出去4. 在 组件中调用函数 把数据或者方法组合回来使用
8.列表无限加载功能实现
使用elementPlus提供的 v-infinite-scroll 指令 监听是否满足触底条件。
满足加载条件时让页数参数加一,获取下一页数据,做新老数据拼接渲染。
//列表的无线加载功能
//结束监听
const disabled= ref (flase)
const load=async()=>{
//获取下一页的数据
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
}
}
//列表渲染
<div class="body" :v-infinte-scroll="load" :infinte-scroll-disabled="disabled">
<!-- 商品列表-->
</div>
9. 对象多层属性访问
本项目中,详情页在渲染数据时,由于一开始的goods是空对象,对空对象进行读取,返回的结果是undefined,会报错。
解决方法:
方法一:可选链
方法二: 使用 v-if 手动来控制渲染的时机,保证只有数据存在时才渲染。
10.放大镜效果
步骤:
获取鼠标在盒子中的相对位置--->控制滑块跟随鼠标移动--->实现大图效果--->鼠标移入移出控制显示隐藏
使用useMouseInElement来获取鼠标位置。
1)滑块跟随鼠标移动
获取鼠标相对位置------>控制滑块跟随移动
import {useMouseInElement} from '@vueuse/core'
// 1.获取鼠标相对位置
const target=ref(null)
// elementX:相对于盒子左侧的距离
// elementY:相对于盒子顶部的距离
// isOutside:判断盒子是否在盒子内
const {elementX,elementY,isOutside}=useMouseInElement(target)
//2.控制滑块(阴影框)跟随鼠标移动
//top---阴影框距离顶部的距离
//left---阴影框距离左侧的距离
const left= ref(0)
const top= ref(0)
//监听elementX/Y变化,一旦变化,重新设置left/top
watch([elementX,elementY,isOutside],()=>{
//如果鼠标没有移入到盒子中,就不执行后续的操作
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 }
})
2)大图效果实现
大图的宽高是小图的2倍,移动方向和滑块的相反,并且数值是2倍。
3)使用v-show和isOutside来控制大图的隐藏和显示
当鼠标移入,大图才显示。
11.使用三方组件
熟悉一个三方组件,首先重点看什么?
答:props和emit, props决定了当前组件接收什么数据,emit决定了会产出什么数据。
验证组件是否成功使用:
答:传入必要数据,是否交互功能正常 ------->点击选择规格,是否正常产出数据。
12.全局注册组件
在项目中的components目录下有可能还会有很多其他通用型组件,有可能在多个业务模块中共享,所有统一进行全局组件注册比较好。
步骤:
1)把components目录下的所有组件进行全局注册。
2)在main.js 中注册插件。
首先在 components 的 index.js 中进行全局注册
// 把components中的所有组件都进行全局化注册
// 通过插件的方式
import ImageView from './ImageView/index.vue'
import Sku from './XtxSku/index.vue'
export const componentPlugin={
install(app){
// app.component('组件的名字',使用的组件)
app.component('XtxImageView',ImageView)
app.component('XtxSku',Sku)
}
}
然后在 main.js 中注册插件
// 引入全局组件插件
import {componentPlugin} from '@/components'
// 注册全局组件
app.use(componentPlugin)
13.表单校验
当功能很复杂时,通过多个组件各自负责某个小功能,再组合成一个大功能是组件设计中的常用方法。
表单校验步骤:
1. 按照接口字段准备表单对象并绑定
2. 按照产品要求准备规则对象并绑定
3. 指定表单域的校验字段名
4. 把表单对象进行双向绑定
//表单校验(账号名+密码)
import {ref} from 'vue'
//1.准备表单对象
const from = ref({
account:'',
password:''
})
//2.准备规则对象
const rules={
account: [{ required: true, message: "用户名不能为空", trigeer: "blur" }],
password: [
{ required: true, message: "密码不能为空", trigeer: "blur" },
{ min: 6, max: 14, message: "密码长度为6-14个字符", trigeer: "blur" },
]
}
//3.渲染数据
14. 自定义校验规则
15. 整个表单的内容验证
在点击登录时,需要对所有需要校验的表单进行统一校验,防止用户还没有输入账号和密码,就直接点击登录。
步骤:
1. 获取form组件实例(通过ref获取)
2. 调用实例方法
import { loginAPI } from "@/apis/user";
import { ElMessage } from "element-plus";
import "element-plus/theme-chalk/el-message.css";
import { useRouter } from "vue-router";
// 3. 获取form实例做统一校验
const formRef = ref(null);
const router = useRouter();
const doLogin = () => {
const { account, password } = form.value;
// 调用实例方法
formRef.value.validate(async (valid) => {
// valid:所有表单都通过校验 才为true
console.log(valid);
// 以valid作为判断条件,如果通过校验才执行登录
if (valid) {
const res = await loginAPI({ account, password });
console.log(res);
// 1.提示用户
ElMessage({ type: "success", message: "登陆成功" });
// 2.跳转首页
router.replace({ path: "/" });
}
});
};
当前登录成功后,提示用户并发生跳转。
使用Element组件的ElMessage来提示用户,但是在使用时,样式需要单独导入。
跳转首页:使用useRouter跳转。
16.Pinia用户数据持久化
由于Pinia的存储是基于内存的,刷新就丢失,而token在pinia中存储,所以token会丢失。
在本项目中,为了保持登录状态,就要做到刷新不丢失,需要配合持久化进行存储。
这里我们使用pinia持久化插件:piniaPluginPersistedstate
最终效果:操作state时会自动把用户数据在本地的localStorage也存一份,刷新的时候会从localStorage中先取。
步骤:
1)安装插件包
npm i pinia-plugin-persistedstate
2)pinia注册插件
//main.js
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia=createPinia()
// 注册持久化插件
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
3)进行持久化的配置
添加 persist : true
17. 多模版适配
在本项目中,登录页使用了多模板适配。
当登录时,显示登录的模块;未登录时,显示未登录的模块。
18.请求拦截器携带Token
Token作为用户标识,在很多个接口中都需要携带Token 才可以正确获取数据,所以需要在接口调用时携带Token。
另外,为了统一控制,采取请求拦截器携带的方案。
如何配置:
Axios请求拦截器可以在接口正式发起之前对请求参数做一些事情,通常Token数据会被注入到 请求header 中,格式按照后端要求的格式进行拼接处理。
//src/utils/http.js
import { useUserStore } from '@/stores/user'
// axios请求拦截器
httpInstance.interceptors.request.use(config => {
// 1. 从pinia获取token数据
const userStore = useUserStore()
// 2. 按照后端的要求拼接token数据
const token = userStore.userInfo.token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, e => Promise.reject(e))
19.封装倒计时函数
本项目中存储的倒计时是总共的秒数,我们需要进行格式化。
这里就需要使用 dayjs插件 。
编写一个函数 useCountDown 把秒数格式化为倒计时的显示状态。
函数样例:
1. formatTime为显示的倒计时时间
2. start是倒计时启动函数,调用时可以设置初始值并且开始倒计时
步骤:
1)安装dayjs插件:
npm i dayjs
2)封装倒计时函数
//src/composables/useCountDown.js
// 封装倒计时逻辑函数
import {ref,computed, onUnmounted} from 'vue'
import dayjs from 'dayjs'
export const useCountDown=()=>{
// 1.响应式数据
let timer=null
const time=ref(0)
// 格式化时间为 xx分xx秒
const formatTime=computed(()=>dayjs.unix(time.value).format('mm分ss秒'))
// 2.开启倒计时函数
const start=(currentTime)=>{
// 开始倒计时的逻辑
// 核心逻辑的编写:每隔1s就减一
time.value=currentTime
timer=setInterval(()=>{
time.value--
},1000)
}
// 组件销毁时清除定时器
onUnmounted(()=>{
timer&&clearInterval(timer)
})
return {
formatTime,
start
}
}
3)在组件中使用
//src/views/Pay/index.vue
<script setup>
....
const {formatTime,start}=useCountDown()
....
</script>
<template>
.....
<p>支付还剩 <span>{{ formatTime }}</span>, 超时后将取消订单</p>
.....
</template>
20.分页器
//src/views/Member/components/UserOrder.vue
<script setup>
// 补充总条数
const total = ref(0)
const getOrderList = async () => {
const res = await getUserOrder(params.value)
// 存入总条数
total.value = res.result.counts
}
// 页数切换
const pageChange = (page) => {
params.value.page = page
getOrderList()
}
</script>
<template>
<el-pagination
:total="total"
@current-change="pageChange"
:page-size="params.pageSize"
background
layout="prev, pager, next" />
</template>