vue项目
组件分类
通用型组件(ElementPlus)-Dialog模态框
业务定制化组件=商品热榜组件
主题色定制-sass引入
安装sass
axios实例化配置
使用实例化的 Axios 对象后,在发起请求时不需要再写完整的 URL。当使用实例化对象发起请求时,实例的 baseURL 会作为请求的基础 URL,而只需要提供相对路径即可。
比如,如果创建了一个实例 instance,设置了 baseURL 为 'https://api.example.com',那么在使用这个实例发起请求时,只需要提供相对路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| javascriptCopy code
const instance = axios.create({ baseURL: 'https://api.example.com', });
instance.get('/users') .then(response => { console.log(response.data); }) .catch(error => { console.error(error); });
|
这种方式使得代码更加简洁,同时提高了代码的可维护性。全局的 axios 对象需要在每次请求时提供完整的 URL,而实例化的对象可以通过设置 baseURL 来减少在每个请求中写重复的 URL 部分。
吸顶交互
安装vueUse插件
引用useScroll函数
1 2 3 4 5
| <script setup>
import { useScroll } from "@vueuse/core"; const { y } = useScroll(window); </script>
|
样式嵌套:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| .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; } ... }
|
1 2
| <div class="app-header-sticky" :class="{ show: y > 78 }"> </div>
|
因为两个组件都需要请求接口数据,如何优化?
Pinia优化重复请求
在store文件夹里面新建category.js文件用来管理和共享分类接口的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { defineStore } from 'pinia' import { getCategoryAPI } from '@/apis/layout'
export const useCategoryStore = defineStore({ id: 'category', state: () => ({ categoryList: [], }), actions: { async getCategory() { try { const res = await getCategoryAPI() console.log(res); this.categoryList = res.result } catch (error) { console.log('获取分类出错:', error); } } } })
|
组件如何调用?
方法一:父组件调用异步请求action方法,子组件分别调用state数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { useCategoryStore } from "@/stores/category"; const categoryStore = useCategoryStore();
*** ... <ul class="app-header-nav"> <li class="home" v-for="item in categoryStore.categoryList" :key="item.id" > <RouterLink to="/">{{ item.name }}</RouterLink> </li> </ul> ...
|
组件封装(数据传输问题)
把可复用的结构只写一次,把可能发送变化的部分抽象成组件参数(props/插槽)
逻辑拆分与组件封装的模块化优化,使用动态参数传递机制实现数据和结构的分离,提高了代码的可维护性和可拓展性;
如goodsitem组件封装
调用复用组件的组件:
1 2 3 4 5 6
| import GoodsItem from "./GoodsItrm.vue"; <ul class="goods-list"> <li v-for="good in cate.goods" :key="good.id"> <GoodsItem :goods="good" /> </li> </ul>
|
组装的复用组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <script setup> defineProps({ goods: { type: Object, default: () => {}, }, }); </script> <template> <RouterLink to="/" class="goods-item"> <img v-img-lazy="goods.picture" alt="" /> <p class="name ellipsis">{{ goods.name }}</p> <p class="desc ellipsis">{{ goods.desc }}</p> <p class="price">¥{{ goods.price }}</p> </RouterLink> </template>
|
实现图片懒加载
1、全局指令定义
2、调用vueUse的useIntersectionObserver函数:判断图片是否进入视图区域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { useIntersectionObserver } from '@vueuse/core'
app.directive('img-lazy', { mounted(el, binding) { console.log(el, binding); useIntersectionObserver( el, ([{ isIntersecting }]) => { console.log(isIntersecting); if (isIntersecting) { el.src = binding.value } }, ) } })
<img v-img-lazy="item.picture" alt="" />
|
懒加载优化
问题一:逻辑书写位置不合理,复杂度转移
前面是将代码写到了入口文件main.js,这样写会增加main.js的复杂度,不妨将懒加载指令封装成插件,入口文件只需要负责注册插件即可
创建插件文件夹,index.js文件中写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = { install(app) { app.directive('img-lazy', { mounted(el, binding) { console.log(el, binding); useIntersectionObserver( el, ([{ isIntersecting }]) => { console.log(isIntersecting); if (isIntersecting) { el.src = binding.value } }, ) } }) } }
|
入口文件注册:
1 2 3
| import {lazyPlugin} from '@/directives' app.use(lazyPlugin)
|
问题二:useIntersectionObserver重复监听
增加stop函数,第一次图片加载完毕后调用stop函数停止监听,防止内存浪费
1 2 3 4 5 6 7 8 9 10 11
| const { stop } = useIntersectionObserver( el, ([{ isIntersecting }]) => { if (isIntersecting) { el.src = binding.value stop() } }, )
|
主页一级分类
思路:准备组件模板->封装接口函数->调用接口获取数据(使用路由参数)->渲染模板
路由缓存问题
使用带有参数的路由需要注意的是,当用户从/users/id1导航到/users/id2时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。
如何解决问题?
思路1:让组件实例不再复用,强制销毁重建;
思路2:监听路由变化,变化之后执行数据更新操作;
方案一:给router-view添加key
以当前路由完整路径为key的值,给router-view组件绑定
为什么用key?我们最常见的用例是与v-for结合,但是key也可以用于强制替换一个元素/组件而不是复用它。
1
| <RouterView :key="$route.fullPath" />
|
但是这种方法也存在问题,过于粗暴,如果每次导航都销毁组件重建,说明每次导航都要重新发送一次数据请求,如果有相同数据,则存在浪费。那怎么做到只发送需要的数据请求呢?
方案二:使用beforeRouterUpdate导航钩子
该钩子函数可以在每次路由更新之前执行,在回调中执行需要数据更新的业务逻辑即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { onBeforeRouteUpdate, useRoute } from "vue-router"; const route = useRoute(); const getCategory = async (id = route.params.id) => { const res = await getTopCategoryAPI(id); categoryData.value = res.result; }; onMounted(() => getCategory());
onBeforeRouteUpdate((to) => { console.log(to); getCategory(to.params.id); });
|
useRoute 用于在 Vue 3 中访问当前路由对象。它返回一个包含当前路由信息的对象,类似于 this.$route 在 Vue 2 中的用法。它返回一个响应式的路由对象,包含了当前路由的各种信息,例如 path、params、query ,这里是使用当前路由对象的params的id。
在 Vue Router 中,onBeforeRouteUpdate 是一个导航守卫,它会在当前路由更新但是同一组件被复用时被调用。这个守卫的回调函数接受一个 to 参数,它表示即将导航到的目标路由对象。
其中 to.params.id 表示即将导航到的路由的 id 参数。
这部分代码涉及到 Vue Router 的路由导航守卫,具体是 onBeforeRouteUpdate 钩子函数的使用。这个函数在路由参数发生变化时被调用,允许你在组件复用时对路由参数的变化做出响应。在这里,你使用了该函数来重新获取分类数据接口,以确保在路由参数变化时,相应的分类数据会被重新加载。
这点的亮点在于实现了在路由参数变化时动态更新相关数据,为用户提供了更流畅的页面切换体验,同时也是在合理利用 Vue Router 提供的路由导航守卫进行数据预加载。
逻辑拆分
将独立的业务逻辑拆分出去再组合的过程
比如将请求分类和请求轮播图数据的业务拆分封装到独立的js文件中,函数use打头,内部封装逻辑便于后期维护,return组件需要用到的数据和方法给组件消费。
组件:.vue文件
1 2 3 4
| import { useCategory } from "./composables/useCategory"; import { useBanner } from "./composables/useBanner"; const { categoryData } = useCategory(); const { bannerList } = useBanner();
|
内部封装函数:.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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 } }
|
useCategory.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { onMounted, ref } from 'vue' import { getTopCategoryAPI } from "@/apis/category"; 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) => { console.log(to); getCategory(to.params.id); }); return { categoryData } }
|
列表无限加载功能实现
获取二级分类商品列表的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
}
export const getSubCategoryAPI = (data) => { return request({ url: '/category/goods/temporary', method: 'POST', data }) }
|
组件调用接口,获取基本数据列表:
1 2 3 4 5 6 7 8
| const goodList = ref([]); const repData = ref({ categoryId: route.params.id, page: 1, pageSize: 20, sortField: "publishTime", });
|
获取并加载商品列表:
1 2 3 4 5 6 7 8 9
| const getGoodList = async () => { const res = await getSubCategoryAPI(repData.value); goodList.value = res.result.items; }; onMounted(() => { getGoodList(); });
|
使用 Element-plus 提供的 v-infinite-scroll 指令监听是否满足触底条件,满足加载条件时让页数参数加一获取下一页数据,做新老数据拼接渲染。
1 2 3 4 5 6 7 8 9
| <div class="body"
v-infinite-scroll="load"
:infinite-scroll-disabled="disabled" > <GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id" /> </div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| const disabled = ref(false); const load = async () => { repData.value.page++; const res = await getSubCategoryAPI(repData.value); goodList.value = [...goodList.value, ...res.result.items]; if (res.result.items.length === 0) { disabled.value = true; } };
|
定制路由行为
在不同路由切换的时候,可以自动滚动到页面的顶部,而不是停留在原先的位置。
vue-router支持scrollBehavior配置项,可以指定路由切换时的滚动位置。在router实例文件中配置:
1 2 3 4 5 6 7
| router:[...] , scrollBehavior() { return { top: 0 } }
|
空对象多层属性访问报错
goods一开始是空对象,.categories[1]就是undefined
1
| <el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories[1].id}` }">{{goods.categories[1].name}} </el-breadcrumb-item>
|
如何解决?
1、可选链语法:
1
| <el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories?.[1].id}` }">{{goods.categories[1].name}} </el-breadcrumb-item>
|
2、v-if手动控制渲染时机,保证只有数据存在才渲染
1 2 3
| <div class="container" v-if="goods.detail"> ... (在外层盒子加一个if判断语句,如果goods数据存在,则为真,才会进行后面的渲染)
|
定义表单和规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const form = ref({ account: "12056258282", password: "hm#qd@23!", agree: true, });
const rules = { account: [{ required: true, message: "用户名不能为空", trigger: "blur" }], password: [ { required: true, message: "密码不能为空" }, { min: 6, max: 24, message: "密码长度要求6-14个字符" }, ], agree: [ { validator: (rule, val, callback) => { console.log(val); return val ? callback() : new Error("请先同意协议"); }, }, ], };
|
表单统一校验:
1 2 3 4 5 6 7 8 9 10 11
| const formRef = ref(null); const doLogin = () => { formRef.value.validate( (vaild) => { console.log(vaild); if (vaild) { } }); };
|
按需导入样式
如果使用 unplugin-element-plus 并且只使用组件 API,你需要手动导入样式。
Example:
1 2
| import "element-plus/theme-chalk/el-message.css"; import { ElMessage } from 'element-plus'
|
持久化用户数据
插件安装
1
| npm i pinia-plugin-persistedstate
|
将插件添加到 pinia 实例上
1 2 3 4 5
| import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia() pinia.use(piniaPluginPersistedstate)
|
创建 Store 时,将 persist 选项设置为 true。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| export const useUserStore = defineStore({ id: 'user', state: () => ({ userInfo: {} }), actions: { async getUserInfo({ account, password }) { try { const res = await loginAPI({ account, password }) console.log(res); this.userInfo = res.result } catch (e) { console.log('获取用户数据出错', e); } } }, persist: true })
|
(一开始本地存储无效,因为persist位置写错了)
运行机制:在设置state的时候会自动把数据同步给localstorage,在获取state数据的时候会优先从localstorage中获取。
如何存储登录用户数据?(刷新后还在)
思路:判断验证用户身份的token是否存在,如果存在则调用有用户信息的组件;否则调用请登录的组件
1
| <template v-if="userStore.userInfo.token">
|
请求拦截器携带Token
请求拦截器可以在接口正式发起之前对请求参数做一些事情,通常token数据会被注入到请求header中,格式按照后端要求的格式进行拼接处理。
1 2 3 4 5 6 7 8 9 10 11
| httpInstance.interceptors.request.use(config => { const userStore = useUserStore(); const token = userStore.userInfo.token if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, e => Promise.reject(e))
|
路由跳转问题
useRouter只能在组件内使用,要在独立的js文件或ts文件不能使用useRouter获取?
地址切换交互-tab切换交互
1、打开弹框交互:点击切换地址按钮,打开弹框,回显用户可选地址列表;
2、切换地址交互:点击切换地址,点击确定按钮,激活地址替换默认收货地址。
2:点击时记录当前激活地址对象activeAddress,点击哪个地址就把哪个地址对象记录下来;
通过动态类名:class控制激活样式类型active是否存在,判断条件:激活地址对象的id===当前项id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <!-- 切换地址 --> <el-dialog v-model="showDialog" title="切换收货地址" width="30%" center> <div class="addressWrapper"> <div @click="switchAddress(item)" class="text item" :class="{ active: activeAddress.id === item.id }" v-for="item in checkInfo.userAddresses" :key="item.id" > <ul> <li> <span>收<i />货<i />人:</span>{{ item.receiver }} </li> <li><span>联系方式:</span>{{ item.contact }}</li> <li><span>收货地址:</span>{{ item.fullLocation + item.address }}</li> </ul> </div> </div> <template #footer> <span class="dialog-footer"> <el-button @click="showDialog = false">取消</el-button> <el-button type="primary" @click="confirm">确定</el-button> </span> </template> </el-dialog>
|
点击确定后将记录的地址传给显示到页面的参数重新渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const checkInfo = ref({}); const defaultAddress = ref({});
const getcheckInfo = async () => { const res = await getCheckoutInfoAPI(); checkInfo.value = res.result; const item = checkInfo.value.userAddresses.find( (item) => item.isDefault === 0 ); defaultAddress.value = item; };
onMounted(() => { getcheckInfo(); });
const showDialog = ref(false);
const activeAddress = ref({}); const switchAddress = (item) => { activeAddress.value = item; };
const confirm = () => { defaultAddress.value = activeAddress.value; showDialog.value = false; };
|
总结:记录激活项(整个对象/id…)+动态类型控制。
支付业务流程
跳转支付地址(get请求)(订单id+回跳地址url)->跳转到回跳地址url(订单id+支付状态)
1 2 3 4 5 6 7
|
const baseURL = "http://pcapi-xiaotuxian-front-devtest.itheima.net/"; const backURL = "http://localhost:5173/paycallback"; const redirectUrl = encodeURIComponent(backURL); const payUrl = `${baseURL}pay/aliPay?orderId=${route.query.id}&redirect=${redirectUrl}`;
|
1 2 3 4 5
| <div class="item"> <p>支付平台</p> <a class="btn wx" href="javascript:;"></a> <a class="btn alipay" :href="payUrl"></a> </div>
|
跳转到支付页面后输入账号密码支付密码即可,后面是后端的工作。
页面最后会自动跳转回backURL地址,所以要配置对应的路由以及页面组件。页面效果展示支付成功还是失败,以及一些订单信息。
获取订单信息调用接口:
1 2 3 4 5
| export const getOrderAPI = (id) => { return request({ url: `/member/order/${id}` }) }
|
1 2 3 4 5 6 7 8
| const getOrderInfo = async () => { const res = await getOrderAPI(route.query.orderId); orderInfo.value = res.result; }; onMounted(() => { getOrderInfo(); });
|
支付倒计时实现
因为接口给的时间参数是秒数,要写一个函数可以把秒数格式化为倒计时的显示状态。
安装一个格式化的插件:
函数封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { computed, onUnmounted, ref } from 'vue' import dayjs from 'dayjs'
export const useCountDown = () => { let timer = null const time = ref(0) const formatTime = computed(() => { return dayjs.unix(time.value).format('mm分ss秒') }) const start = (currentTime) => { time.value = currentTime timer = setInterval(() => { time.value-- }, 1000) } onUnmounted(() => { timer && clearInterval(timer) }) return { formatTime, start } }
|
组件调用函数:
1 2 3 4 5 6 7 8 9 10 11 12
| import { useCountDown } from "@/composables/useCountDown";
const { formatTime, start } = useCountDown();
const getPayInfo = async () => { const res = await getOrderAPI(route.query.id); console.log("支付数据", res); payInfo.value = res.result; start(res.result.countdown); };
|