axios
1.axios的基础封装
随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍
这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用
如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了
这时候我们就需要对axios
进行二次封装,让使用更为便利
import axios from 'axios'
// 创建axios实例
const http = axios.create({
baseURL: 'http://localhose'(请求前缀),
timeout: 5000(超时事件)
})
// axios请求拦截器
instance.interceptors.request.use(config => {
return config
}, e => Promise.reject(e))
// axios响应式拦截器
instance.interceptors.response.use(res => res.data, e => {
return Promise.reject(e)
})
//导出
export default http;
案例应用
import http from '@/utils/http'
export function getCategoryAPI () {
return http({
url: '/xxx/xxxx'(资源路径)
})
}
Pinia
1.状态管理优化请求
解决问题:在一些页面组件上发送了相同的资源请求,使用pinia的状态管理来优化为一次请求
- 创建useCategoryStore
import { getCategoryAPI } from "@/api/layout";
import { ClassData } from "@/api/model/layoutModel";
import { defineStore } from "pinia";
import { ref } from 'vue'
export const useCategoryStore = defineStore('category', () =>{
//state导航列表数据
const categoryList = ref([] as ClassData[])
//aciton获取导航数据的方法
const getCategory = async() => {
const res = await getCategoryAPI();
categoryList.value = res.result
}
//返回数据
return {
categoryList,
getCategory
}
})
- 在页面加载之前使用onMounted通过aciton进行状态计算
import { useCategoryStore } from '@/stores/categoryStore'
import { onMounted } from 'vue'
const categoryStore = useCategoryStore();
//在页面初始化得到category 使用pinia进行状态管理 将两次请求变成一次请求
onMounted(() => {
categoryStore.getCategory()
})
- 在组件中使用通过categoryStore中return的state数据
<script setup lang="ts">
import { useCategoryStore } from '@/stores/categoryStore';
const categoryStore = useCategoryStore();
</script>
<template>
<li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</template>
2.Pinia管理用户数据
基本思想:Pinia负责用户数据相关的state和action,组件中只负责触发action函数并传递参数
// 管理用户数据相关
import { UserInfo } from "@/api/model/loginModel";
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginAPI } from '@/apis/user'
export const useUserStore = defineStore('user', () => {
// 1. 定义管理用户数据的state
const userInfo = ref({} as UserInfo)
// 2. 定义获取接口数据的action函数
const getUserInfo = async ({ account, password }) => {
const res = await loginAPI({ account, password })
userInfo.value = res.result
}
// 3. 以对象的格式把state和action return
return {
getUserInfo
}
}, {
//持久化,方便后续使用
persist: true,
})
3.pinia本地购物车
在用户未登录的状态下,如果对某个商品进行了加入购物车的操作,将会加入到本地购物车
在用户登录的状态下,如果对某个商品进行了加入购物车的操作,将会走后台接口将数据写入数据库,并且将pinia本地购物车中的物品也更新到数据库中
import { addCartAPI} from "@/api/cart";
export const useCartStore = defineStore('cart',() => {
//state => cartList;
const cartList = ref([] as CartItem[]);
const userStore = useUserStore();
//获取token查看是否登录
const isLogin = computed(() => userStore.userInfo.token)
//aciton => addCart
const addCart = async ( good : CartItem) => {
if(isLogin){
//已经登录
//调用接口
const {skuId,count} = good;
//接口api
await addCartAPI({skuId,count});
const res = await findNewCartListAPI();
cartList.value = res.result;
}else{
//没有登录
//添加购物车
//已经添加过 => count + 1
//没有添加过 => 直接push
const item = cartList.value.find((item) => good.skuId === item.skuId)
if(item) {
item.count++;
}else{
cartList.value.push(good)
}
}
}
// 获取最新购物车列表
const updateNewList = async () => {
const res = await findNewCartListAPI();
cartList.value = res.result;
};
}
合并购物车,实现于用户登录的时候
import { mergeCartAPI } from '@/api/cart'
export const useUserStore = defineStore('user' ,() => {
const cartStore = useCartStore();
//定义管理数据的state
const userInfo = ref({} as UserInfo)
//定义获取接口函数的action函数
const getUserInfo = async({ account, password}) =>{
const res = await loginAPI({ account, password})
userInfo.value = res.result
// 合并购物车
await mergeCartAPI(
cartStore.cartList.map((item) => {
return {
skuId: item.skuId,
selected: item.selected,
count: item.count
};
})
);
//更新购物车的列表
await cartStore.updateNewList();
}
}
vue
1.组件封装复用
纯展示类组件通用封装思路总结:
- 搭建纯静态的部分,不管可变的部分
- 抽象可变的部分为组件参数:非复杂的模板抽象为props,复杂的结构模板抽象为插槽
HomePanel.vue
<script setup lang="ts">
defineProps<{
title: string;
subTitle: string;
}>()
</script>
<template>
<div class="home-panel">
<div class="container">
<div class="head">
<!-- 主标题和副标题 -->
<h3>
{{ title }}<small>{{ subTitle }}</small>
</h3>
</div>
<!-- 主体内容区域 -->
<slot />
</div>
</div>
</template>
index.vue
//导入组件
import HomePanel from './components/HomePanel.vue'
<template>
<HomePanel title="新鲜好物" sub-title="新鲜好物 好多商品">
<div>我是新鲜好物的插槽内容</div>
</HomePanel>
<HomePanel title="人气推荐" sub-title="人气推荐 好多商品">
<div>我是人气推荐的插槽内容</div>
</HomePanel>
</template>
2.无限加载实现
类似于tb,pdd在你搜索一个物品的时候,出现的商品列表滑不到底,无限加载商品(加载到数据库最后一条数据)
基础思路:
- 触底条件满足之后 page++,拉取下一页数据
- 新老数据做数组拼接
- 判断是否已经全部加载完毕,停止监听
<script setup lang="ts">
//展示核心代码,getList为初始页面请求返回的结果 已经省略
const disabled = ref(false as boolean);
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
}
}
</script>
3.基于业务逻辑的函数拆分
基本思想:把组件内独立的业务逻辑通过 useXXX
函数做封装处理,在组件中做组合使用
useBanner
import { ref, onMounted} from 'vue'
import { BannerResult } from '@/api/model/homeModel';
import { getBannerAPI } from '@/api/home';
export function useBanner(){
//获取轮播图数据
const bannerList = ref([] as BannerResult[])
const getBannerList = async() =>{
const res = await getBannerAPI( {distributionSite : '2'});
bannerList.value = res.result;
}
onMounted(() => {
getBannerList();
})
return {
bannerList
}
}
useCategory
import { ref, onMounted } from 'vue'
import { getCategoryAPI } from '@/api/category';
import { Category } from '@/api/model/categoryModel';
import { useRoute } from 'vue-router';
import { onBeforeRouteUpdate } from 'vue-router';
export function useCategory(){
//获取分类数据
const route = useRoute();
const categoryData = ref({} as Category)
const getCategoryDate = async(id = route.params.id as string) => {
const res = await getCategoryAPI(id)
categoryData.value = res.result;
}
//目标:路由参数变化的时候,可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
getCategoryDate(to.params.id as string)
})
return {
categoryData
}
}
index.vue
import { useBanner } from './composables/useBanner'
import { useCategory } from './composables/useCategory'
const { bannerList } = useBanner()
const { categoryData } = useCategory()
VueUse
1.图片懒加载
场景:电商网站的首页通常会很长,用户不一定能访问到页面靠下面的图片,这类图片通过懒加载优化手段可以做到只有进入视口区域才会加载图片
核心原理:图片进入视口才会加载图片
基于useIntersectionObserver | VueUse
(1).封装全局指令
// 定义懒加载插件
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)
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
console.log(isIntersecting)
if (isIntersecting) {
// 进入视口区域
el.src = binding.value
stop()
}
},
)
}
})
}
}
(2).注册全局指令
// 全局指令注册
import { directivePlugin } from '@/directives'
app.use(directivePlugin)
(3).替换指令
<template>
<!-- 指令替换:将img的src 替换为 新定义的全局组件 v-img-lazy -->
<img v-img-lazy="goods.picture" alt="" />
</template>
2.图片预览组件封装
1).小图切换大图
<script>
defineProps<{
imageList:string[];
}>();
//1.小图切换大图显示
const activeIndex = ref(0)
const enterhandler = (i:number) => {
activeIndex.value = i;
}
</script>
<template>
<div class="goods-image">
<!-- 左侧大图-->
<div class="middle" ref="target">
<img :src="imageList[activeIndex]" alt="" />
</div>
<!-- 小图列表 -->
<ul class="small">
<li v-for="(img, i) in imageList" :key="i" @mouseenter="enterhandler(i)" :class="{ acitve : i === activeIndex}">
<img :src="img" alt="" />
</li>
</ul>
</div>
</template>
2).放大镜效果显示
<script setup lang="ts">
import { ref,watch } from 'vue';
import { useMouseInElement } from '@vueuse/core'
defineProps<{
imageList:string[];
}>();
//1.小图切换大图显示
const activeIndex = ref(0)
const enterhandler = (i:number) => {
activeIndex.value = i;
}
//2.获取鼠标相对位置
const target = ref(null)
const { elementX, elementY, isOutside } = useMouseInElement(target)
// 3. 控制滑块跟随鼠标移动(监听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
// console.log('后续逻辑执行了')
// 有效范围内控制滑块距离
// 横向
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[activeIndex]" 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="enterhandler(i)" :class="{ acitve : i === activeIndex}">
<img :src="img" alt="" />
</li>
</ul>
<!-- 放大镜大图 -->
<div class="large" :style="[
{
backgroundImage: `url(${imageList[activeIndex]})`,
backgroundPositionX: `${positionX}px`,
backgroundPositionY: `${positionY}px`,
},
]" v-show="!isOutside"></div>
</div>
</template>
vueRoute
1.路由缓存问题
相应路由参数的变化
使用带有参数的路由时需要注意的时,当用户从/users/johnny
导航到/users/jolyne
时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用显得更加高效,不过,这也意味这组件的声明周期钩子不会被调用
问题:一级分类的切换正好满足上面的条件,组件实例复用,导致分类数据无法更新
解决思路:
一、让组件实例不复用,强制销毁重建
<RouterView :key="$router.fullPath" />
二、监听变化,变化之后执行数据更新操作
<script setup lang="ts">
import { onBeforeRouteUpdate } from 'vue-router';
const route = useRoute();
const categoryData = ref({} as Category)
const getCategoryDate = async(id = route.params.id as string) => {
const res = await getCategoryAPI(id)
categoryData.value = res.result;
}
//目标:路由参数变化的时候,可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
getCategoryDate(to.params.id as string)
})
</script>
2.路由行为定制
解决问题:在用户切换一级路由时,页面不会返回到顶部的情况
//路由滚动行为定制
scrollBehavior() {
return {
top:0
}
}