练手项目简介:
该商城是一个综合性的B2C电商平台,有登录功能,进入首页可查看商品列表,商品分类,以及在商详页进行商品的选购、结算、与支付。个人中心包含会员信息及会员订单等。共计20+页面,8+模块,上百个接口的封装调用。
使用到了如下的vue3全家桶技术:
create-vue+Pinia+ElementPlus+Vue3+Setup+VueRouter+VueUse
小兔鲜项目中的亮点提纲:
- 自定义vue指令v-lazy-img
- axios的二次封装
- 前后端如何进行接口调用
- pinia在项目中的使用
- 如何持久化本地pinia数据?
- 商品组件的封装, 并注册成全局组件
- 路由缓存问题,即路由跳转时,页面没有刷新
- 瀑布流布局,并实现商品列表的无限加载功能
- 支付业务流程
- 定制路由滚动行为
- 手动实现导航组件的吸顶展示功能
- 商详页,图片预览组件的小图切换大图与放大镜效果
- 在路由页面,使用导航守卫,配置路由跳转前后的行为
小兔鲜项目中的亮点:
1) 自定义vue指令v-lazy-img
背景: 电商平台项目有大量的商品图与详情图, 同时大量加载与渲染图片资源会挤占带宽, 首页白屏与加载时间过长,体验不好, 所以需要使用到图片懒加载。封装成vue指令是为了在项目各文件中更便捷使用。
原理:图片懒加载,当滚动时元素出现在视口中时,才需要src替换成接口返回的图片地址。
步骤:
1、在根目录directives创建index.js文件, 内容如下
// 使用了vueuse获取元素是否出现在视口中的函数方法
import { useIntersectionObserver } from '@vueuse/core'
// 创建 图片懒加载的自定义指令
export const lazyImgPlugin = {
install(app){
app.directive('lazy-img',{
// el 指令绑定的元素
// binding 指令等号后面的内容 binding.value这里是图片的src
mounted(el, binding) {
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
if (isIntersecting) {
// 替换img真正的图片地址
el.src = binding.value
stop()
}
},
)
},
})
}
}
2、main.js文件中引入并注册
// 引入并注册自定义的图片懒加载组件
import {lazyImgPlugin} from '@/directives/index'
app.use(lazyImgPlugin)
3、具体使用 <img v-lazy-img="goods.picture" :alt="goods.name" />
2) axios的二次封装
- 引入axios并创建实例, 添加请求拦截器与响应拦截器, 在请求头添加token参数, 在响应数据时判断是否登录过期, 与对接口返回的错误进行提示。
- 需要注意登录后 token的缓存与使用, 以及token过期后的处理。
import axios from 'axios'
// 引入toast
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'
// 引入pinia里的userstore
import { useLoginUserStore } from '@/stores/userStore'
const userStore = useLoginUserStore()
// 引入router,token失效跳转至登录页
import router from '@/router'
// 创建axios实例
const http = axios.create({
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
timeout: 5000
})
// axios请求拦截器
http.interceptors.request.use(
(config) => {
const token = userStore.userInfo.token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(e) => Promise.reject(e)
)
// axios响应式拦截器
http.interceptors.response.use(
(res) => res.data,
(e) => {
// 接口错误提示
ElMessage({type:'error', message: e.response.data.message || '服务器错误'})
// 接口401 token过期统一处理
if(e.response.status == '401'){
// 清除用户数据
userStore.clearUserInfo()
setTimeout(()=>{
// 跳转至登录页面
router.replace('/login')
},1200)
}
return Promise.reject(e)
}
)
export default http
为什么要在请求拦截器携带Token参数
- 为什么
Token作为用户标识,在很多个接口中都需要携带Token才可以正确获取数据,所以需要在接口调用时携带Token。另外,为了统一控制采取在请求拦截器携带的方案 - 如何配置
Axios请求拦截器可以在接口正式发起之前对请求参数做一些事情,通常Token数据会被注入到请求header中,格式按照后端要求的格式进行拼接处理
3) 前后端如何进行接口调用
- 在utils文件的http.js文件中引入axios并进行axios的二次封装
- 在apis引入http并定义接口
import http from '@/utils/http'
export const getCartApi = ()=>{
return http({
url: '/member/cart'
})
}
- 项目中的具体使用示例
// 引入接口api 与 vue3
import { getOrderDetailApi} from '@/api/order'
import { ref, onMounted } from 'vue'
// 定义返回的数据载体
const orderDetail = ref({})
// 定义调用接口的方法
const getOrderDetail = async()=>{
const res = await getOrderDetailApi()
orderDetail.value = res.result
}
// 放到合适的生命周期里
onMounted(()=>{
getOrderDetail()
})
4) pinia在项目中的使用
Pinia是Vue的专属的最新状态管理库,是Vuex状态管理工具的替代品
1.提供更加简单的API(去掉了mutation)
2.提供符合组合式风格的API(和Vue3新语法统一)
3.去掉了modules的概念,每一个store都是一个独立的模块
4.搭配TypeScript一起使用提供可靠的类型推断
添加Pinia到Vue项目
1.使用create-vue 创建空的新项目,npm init vue@latest
2.按照官方文档安装pinia到项目
项目中使用到了pinia的模块:
- 用户模块:获取用户信息并登录成功后储存用户信息、退出登录时清除用户信息
- 首页的nav模块:定义导航数据,接口获取导航。在首页导航与吸顶导航中调用,避免代码重复与冗余。
- 购物车模块:购物车的增删改查,清空购物车,以及一些总价、总数量等的计算属性
5)如何持久化本地pinia数据?
1、用户数据中有一个关键的数据叫做Token(用来标识当前用户是否登录),而Token持续一段时间才会过期
2、Pinia的存储是基于内存的,刷新就丢失,为了保持登录状态就要做到刷新不丢失,需要配合持久化进行存储
3、有两种方式,本地存储localStorage,以及pinia的插件pinia-plugin-persistedstate
4、如何在vue3项目中进行配置pinia-plugin-persistedstate?
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
- 安装:
npm i pinia-plugin-persistedstate
- 将插件添加到 pinia 实例上
- 在主入口文件main.js引入与使用
- 创建 Store 时,将 persist 选项设置为 true。
6)商品组件的封装, 并注册成全局组件
为了解决两个问题:组件复用、业务维护。把可复用的部分和结构只写一次,会变的部分放在插槽以及props里,以及规划好slot插槽的使用。
步骤一:在根目录components里添加goodItem.vue,写好静态结构与样式,使用defineProps,写好业务逻辑,确定要传进来的数据与要暴露出去的数据和方法。
步骤二:在components文件夹里的根文件注册成全局组件(app.component)
install(app){app.component('RitaImageView', ImageViewVue)}
步骤三:引入插件化的全局组件
import {componentsPlugin} from '@/components/index'
app.use(componentsPlugin)
步骤四:无需引入组件,使用<RitaImageView :goods='list'>
7)路由缓存问题,即路由跳转时,页面没有更新
什么是路由缓存问题:使用带有参数的路由时需要注意的是,当用户从/users/johnny 导航到/users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。
问题:一级分类的切换正好满足上面的条件,组件实例复用,导致分类数据无法更新
解决问题的思路:
- 让组件实例不复用,强制销毁重建 。
给router-view添加key属性 - 监听路由变化,变化之后执行数据更新操作。
当router更新时重新发送页面请求onBeforeRouteUpdate((to)=>{getNavList(to.params.id)})
8)瀑布流布局,实现商品列表的无限加载功能
需求背景:在商品筛选结果页与商品列表页,监听页面触底时重新触发列表数据的请求与加载,使得用户浏览行为不被打断。
核心实现逻辑:使用elementPlus提供的v-infinite-scroll指令监听是否满足触底条件,满足加载条件时让页数参数加一获取下一页数据,做新老数据拼接渲染。加载完毕结束监听。
<!-- element plus组件提供的触底监听 与停止监听 -->
<div class="body" v-infinite-scroll="load" :infinite-scroll-disabled="disabled">
<!-- 商品列表-->
<rita-goods-item v-for="good in subCategoryGoodsList" :key="good.id" :goods="good" />
</div>
9)支付业务流程
10)定制路由滚动行为
当切换路由时,页面自动滚动到顶部,在router文件中进行配置
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
// 路由行为的配置项 切换路由时自动滚回顶部
scrollBehavior(){
return {
top:0
}
}
})
11)手动实现导航组件的吸顶展示功能
- 需求背景:当首页页面过长时,使用吸顶效果,使得用户在滑动读取数据的时候把导航条一直固定在屏幕上方,以便用户快速操作和交互。
- 逻辑实现:vueuse中的获取滚动距离顶部的y轴参数,当滚动距离大于100(页面header部分的高度)时,添加show类名,实现吸顶效果
<script setup>
// 获取y轴距离顶部的滚动距离
import {useScroll} from '@vueuse/core'
const { y } = useScroll(window)
</script>
<template>
<div class="app-header-sticky" :class="y>=78?'show':''">
...
</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;
}
</style>
12)商详页,图片预览组件的小图切换大图与放大镜效果
小图切换大图
思路:维护一个数组图片列表,鼠标划入小图记录当前小图下标值,通过下标值在数组中取对应图片,显示到大图位置。
步骤:准备组件静态模版 => 为小图绑定事件,记录当前激活下标值 => 通过下标切换大图显示 => 通过下标实现激活状态
放大镜功能拆解
1.左侧滑块跟随鼠标移动
2.右侧大图放大效果实现
3.鼠标移入控制滑块和大图显示隐藏
13)在路由页面,使用导航守卫,配置路由跳转前后的行为
主要实现以下几点:
1、配置页面跳转的loading动效
2、切换路由时自动滚回页面顶部
3、没有登录时,在地址栏输入url,不能直接跳转到相对应的页面
4、页面标题随着meta进行变化
import { ElLoading } from 'element-plus'
import 'element-plus/theme-chalk/el-loading.css'
// 跳转前
router.beforeEach((to,from,next)=>{
ElLoading.service({
fullscreen: true,
text: 'Loading...'
})
window.scrollTo(0,0)
next()
})
// 跳转后
router.afterEach((to)=>{
setTimeout(()=>{
const loadingInstance = ElLoading.service()
loadingInstance.close()
},1000)
document.title = to.meta.title || 'my project'
})