vue3踩坑笔记:电商平台小兔鲜项目回顾与总结

练手项目简介:

该商城是一个综合性的B2C电商平台,有登录功能,进入首页可查看商品列表,商品分类,以及在商详页进行商品的选购、结算、与支付。个人中心包含会员信息及会员订单等。共计20+页面,8+模块,上百个接口的封装调用。

使用到了如下的vue3全家桶技术:

create-vue+Pinia+ElementPlus+Vue3+Setup+VueRouter+VueUse

小兔鲜项目中的亮点提纲:

  1. 自定义vue指令v-lazy-img
  2. axios的二次封装
  3. 前后端如何进行接口调用
  4. pinia在项目中的使用
  5. 如何持久化本地pinia数据?
  6. 商品组件的封装, 并注册成全局组件
  7. 路由缓存问题,即路由跳转时,页面没有刷新
  8. 瀑布流布局,并实现商品列表的无限加载功能
  9. 支付业务流程
  10. 定制路由滚动行为
  11. 手动实现导航组件的吸顶展示功能
  12. 商详页,图片预览组件的小图切换大图与放大镜效果
  13. 在路由页面,使用导航守卫,配置路由跳转前后的行为

小兔鲜项目中的亮点:

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) 前后端如何进行接口调用
  1. 在utils文件的http.js文件中引入axios并进行axios的二次封装
  2. 在apis引入http并定义接口
import http from '@/utils/http'
export const getCartApi = ()=>{
    return http({
        url: '/member/cart'
    })
}
  1. 项目中的具体使用示例
// 引入接口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)
  1. 安装:npm i pinia-plugin-persistedstate
  2. 将插件添加到 pinia 实例上
  3. 在主入口文件main.js引入与使用
  4. 创建 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 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。

问题:一级分类的切换正好满足上面的条件,组件实例复用,导致分类数据无法更新

解决问题的思路:

  1. 让组件实例不复用,强制销毁重建 。
    给router-view添加key属性
  2. 监听路由变化,变化之后执行数据更新操作。
    当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'
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容