vue3小兔鲜商城项目学习笔记+资料分享02

最近正在学习vue3小兔鲜
下面是学习笔记
建议大家先去看我第一篇小兔鲜的文章,强烈建议,非常建议,十分建议,从头开始看更完整。

布局模块

路由设计

**目标:**能够理解小兔鲜项目中的路由设计

内容:

一级路由有登录 Login 和布局容器 Layout

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v3gF56aX-1668072603824)(media/image-20211229174027074.png)]

路径 组件(功能) 嵌套级别
/ 首页布局容器 Layout 1级
/login 登录 1级
/category/:id​ 分类 2级
/product/:id 商品详情 2级
/cart 购物车 2级
/checkout 填写订单 2级
/pay 支付 2级
/pay/result 支付结果 2级

配置 eslint 规则

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
  root: true,
+  // 自定义规则
+  rules: {
+    // vue组件必须用组合词: 关闭
+    'vue/multi-word-component-names': 'off',
+  },
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript/recommended',
    '@vue/eslint-config-prettier',
  ],
  env: {
    'vue/setup-compiler-macros': true,
  },
};

配置路由

**目标:**能够配置小兔鲜儿项目中的路由

核心代码:

  • 创建组件views/Layout/index.vue






  • 创建组件views/Login/index.vue






  • 创建文件router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: "/",
      component: () => import("@/views/Layout/index.vue"),
    },
    {
      path: "/login",
      component: () => import("@/views/Login/index.vue"),
    },
  ],
});

export default router;

  • main.ts中导入
import { createApp } from 'vue'
import App from './App.vue'
import 'normalize.css'
import '@/assets/styles/common.less'
+ import router from './router'

const app = createApp(App)

+ app.use(router)
app.mount('#app')

  • 修改App.vue,预留路由出口

  • 浏览器查看效果

注意事项:

  • script 标签的 setup 和 lang=“ts” 再 TS 项目中都是需要的,不能省略
  • vue3 项目 index.vue 文件后缀名需要补全

Layout布局-顶部通栏

**目标:**能够完成Layout组件的顶部通栏布局

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sFBuqXxT-1668072603840)(media/image-20211229175807388.png)]

核心步骤:

看不到字体图标?

  • index.html 引入字体图标文件

+ 
小兔鲜儿

静态结构

  • 新建头部导航组件

Layout/components/app-topnav.vue







3)在 src/views/Layout.vue 中导入使用

<script setup lang="ts">
import AppTopnav from "./components/app-topnav.vue";
script>

<template>
  <AppTopnav />
template>

<style lang="less" scoped>style>


Layout布局-头部布局

**目标:**能够完成Layout组件的头部布局

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yQkMqWoj-1668072603841)(media/image-20211229180517464.png)]

核心代码

  • Layout/components/ 下新建 app-header.vue 组件,基础布局如下:







  • src/views/Layout.vue 中导入使用。







看不到图片?

拷贝素材到项目中

  • assets/images/中提供图片,在素材中已经提供

nav 组件拆分

因为在后面的吸顶交互里,我们需要复用导航部分,所以这里我们先直接把他拆分出来,拆分成一个单独的组件

  • Layout/compoennts/ 下,新建app-header-nav.vue组件







  • Layout/components/app-header.vue





  • 查看效果

Layout布局-底部

**目标:**能够完成Layout布局的底部布局效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Culk46GH-1668072603842)(media/image-20211229182504728.png)]

静态结构

  • Layout/components下,新建/app-footer.vue 组件,基础布局如下:







  • src/views/Layout.vue 中导入使用。







  • 查看效果

使用 pinia 管理数据

目标: 通过 pinia 管理项目中的数据。

核心步骤:

  • main.ts中注册 pinia
import { createApp } from 'vue'
import App from './App.vue'

import 'normalize.css'
import '@/assets/styles/common.less'

import router from './router'
+ import { createPinia } from 'pinia'
+ const pinia = createPinia()

const app = createApp(App)
app.use(router)
+ app.use(pinia)
app.mount('#app')

  • 创建文件store/modules/home.ts,用于管理home模块的数据
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useHomeStore = defineStore('home', () => {
  // 准备响应式数据
  const money = ref(14000);
  // 记得 return 返回
  return { money };
});


  • 创建store/index.ts统一管理所有的模块
export * from './modules/home';

  • Layout/index.vue中测试
import useHomeStore from '@/store'
const home = useHomeStore()
console.log(home.money)

  • 控制台查看效果

使用 Pinia 获取头部分类导航

**目标:**能够发送请求完成分类导航的渲染

核心代码:

  • store/modules/home.ts中提供 state 和 actions
const useHomeStore = defineStore('home', {
  state: () => ({
    categoryList: []
  }),
  actions: {
    async getAllCategory() {
      const res = await request.get('/home/category/head')
      console.log(res)
    }
  }
})

  • Layout/index.vue中发送请求


TS 类型声明文件规划

定义类型声明

  • src\types\modules\home.d.ts中定义数据类型
// 分类数据单项类型
export interface Goods {
  desc: string;
  id: string;
  name: string;
  picture: string;
  price: string;
  title: string;
  alt: string;
};

export interface Children {
  id: string;
  name: string;
  picture: string;
  goods: Goods[];
};

export interface Category {
  id: string;
  name: string;
  picture: string;
  children: Children[];
  goods: Goods[];
};

// 分类数据列表类型
export type CategoryList = Category[];


类型出口统一

  • 新建 src\types\index.d.ts
// 统一导出所有类型文件
export * from "./api/home";


应用

  • 修改store/modules/home.ts,给 axios 请求增加泛型
import { defineStore } from "pinia";
import request from "@/utils/request";
import type { CategoryList } from "@/types";

const useHomeStore = defineStore("home", {
  state: () => ({
    categoryList: [] as CategoryList,
  }),
  actions: {
    async getAllCategory() {
      const res = await request.get("/home/category/head");
      this.categoryList = res.data.result;
    },
  },
});

export default useHomeStore;


  • 渲染分类导航 在Layout/components/app-header-nav.vue




Axios 二次封装

目标:改写 Axios 返回值的 TS 类型

Axios 二次封装,让 AxiosTS 类型组合使用时更方便。

Axios 内置类型声明解读

// 1. Axios 实例类型
export class Axios {
  // ...省略
  request<T = any, R = AxiosResponse<T>>(config): Promise<R>;
  get<T = any, R = AxiosResponse<T>>(url: string, config): Promise<R>;
}

// 2. AxiosResponse 返回值类型
export interface AxiosResponse<T = any, D = any>  {
  data: T;
  // ...省略
}


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kj996dNM-1668072603843)(media/image-20220218165657044.png)]

TS 类型升级支持

import { defineStore } from "pinia";
import request from "@/utils/request";
import type { CategoryList } from "@/types";

+ interface ApiRes {
+   msg: string;
+   result: T;
+ }

const useHomeStore = defineStore({
  id: "home",
  state: () => ({
    categoryList: [] as CategoryList,
  }),
  actions: {
    async getAllCategory() {
-      // 能用, res.data 的返回值类型为 any
-      const res = await request.get("/home/category/head");
+     // 恭喜已经有 TS 类型提醒了,res.data 能提示 result 和正确的类型
+      const res = await request.get>("/home/category/head");
      this.categoryList = res.data.result;
    },
  },
});

export default useHomeStore;

TS 类型进阶封装(先使用)

  • 课堂中先直接使用,提高开发效率。
  • ⏰ 课后大家自行解读,提升自己 TS 类型处理能力。

参考代码

src\utils\request.ts

- import axios from "axios";
+ import axios, { type Method } from "axios";

const instance = axios.create({
  baseURL: "http://pcapi-xiaotuxian-front-devtest.itheima.net/",
  timeout: 5000,
});

// 添加请求拦截器
instance.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    return config;
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);

// 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    return response;
  },
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  }
);

+ // 后端返回的接口数据格式
+ interface ApiRes {
+    msg: string;
+    result: T;
+ }

+/**
+ * axios 二次封装,整合 TS 类型
+ * @param url  请求地址
+ * @param method  请求类型
+ * @param submitData  对象类型,提交数据
+ */
+export const http = (method: Method, url: string, submitData?: object) => {
+  return instance.request>({
+    url,
+    method,
+    //  自动设置合适的 params/data 键名称,如果 method 为 get 用 params 传请求参数,否则用 data
+    [method.toUpperCase() === "GET" ? "params" : "data"]: submitData,
+  });
+};

export default instance;


使用

import { defineStore } from "pinia";
-import request from "@/utils/request";
+import { http } from "@/utils/request";

const useHomeStore = defineStore({
  id: "home",
  state: () => ({
    categoryList: [] as CategoryList,
  }),
  actions: {
    async getAllCategory() {
-      const res = await request.get>("/home/category/head");
+      // 使用起来简洁很多
+      const res = await http("GET", "/home/category/head");
      this.categoryList = res.data.result;
    },
  },
});

export default useHomeStore;


分类导航吸顶功能

电商网站的首页内容会比较多,页面比较长,为了能让用户在滚动浏览内容的过程中都能够快速的切换到其它分类。需要分类导航一直可见,所以需要一个吸顶导航的效果。

核心步骤:

目标: 完成头部组件吸顶效果的实现

交互要求

  1. 滚动距离大于等于 78 的时候,组件会在顶部固定定位
  2. 滚动距离小于 78 的时候,组件消失隐藏

实现思路

  1. 准备一个吸顶组件,准备一个类名,控制显示隐藏
  2. 监听页面滚动,判断滚动距离,距离大于 78 添加类名

静态结构

核心代码:

  • Layout/components/下,新建 app-header-sticky.vue 组件







  • Layout首页引入吸顶导航组件







吸顶实现

  • 在滚动到 78px 完成显示效果(添加类名)

    通过滚动事件的触发,判断当前是否已经滚动了 78px,如果大于则添加类名,否则移除类名

    1. document.documentElement.scrollTop 获取滚动距离
    2. :class 动态控制类名显示

组件src/views/Layout/components/app-header-sticky.vue






分类导航吸顶功能-重构

目标: 使用 vueuse/core 重构吸顶功能

vueuse/core : 组合式 API 常用复用逻辑的集合

https://vueuse.org/core/useWindowScroll/

核心步骤

1)安装 @vueuse/core 包,它封装了常见的一些交互逻辑

yarn add @vueuse/core

2)在吸顶导航中使用

src/components/app-header-sticky.vue





常见疑问:

  • vue2 项目中能使用 @vueuse/core 吗?
    • 可以使用,需配合 @vue/composition-apiVue2 老项目支持 组合式API
    • @vueuse/core 只能以 组合式API 形式使用。

首页

配置二级路由

**目标:**配置首页的路由,首页 Home 组件属于二级路由

核心步骤:

  • 创建组件 src/views/Home/index.vue







  • 配置路由
{
  path: '/',
  component: () => import('@/views/Layout/index.vue'),
  children: [
    {
      path: '/',
      component: () => import('@/views/Home/index.vue'),
    },
  ],
},

  • 配置路由出口

整体组件拆分

任务目标: 从整体角度按照模块功能进行组件拆分

1)拆分左侧分类组件

Home/components/home-category.vue

<script setup lang="ts">script>

<template>
  <div class="home-category">分类组件div>
template>

<style lang="less" scoped>style>


2)拆分banner组件

Home/components/home-banner.vue

<script setup lang="ts">script>

<template>
  <div class="home-banner">bannerdiv>
template>

<style scoped lang="less">
.home-banner {
  width: 1240px;
  height: 500px;
  position: absolute;
  left: 0;
  top: 0;
  z-index: 98;
}
style>


3)home组件中引入使用

<script setup lang="ts">
import HomeBanner from "./components/home-banner.vue";
import HomeCategory from "./components/home-category.vue";
script>

<template>
  <div class="page-home">
    <div class="home-entry">
      <div class="container">
        
        <HomeCategory />
        
        <HomeBanner />
      div>
    div>
  div>
template>

<style lang="less" scoped>style>


左侧分类实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gjbmqFn-1668072603845)(media/left.png)]

1. 导航menu数据渲染

Home/components/home-category.vue

静态结构







数据渲染一级分类
<script setup lang="ts">
import useStore from "@/store";
import { RouterLink } from "vue-router";
// 获取 Pinia 中的 home 模块,分类数据为 home.categoryList 
const { home } = useStore();
script>

<template>
  <div class="home-category">
    <ul class="menu">
      <li v-for="item in home.categoryList" :key="item.id">
        <RouterLink to="/">{{ item.name }}RouterLink>
        <RouterLink to="/">{{ "茶咖酒具" }}RouterLink>
        <RouterLink to="/">{{ "水具杯壶" }}RouterLink>
        
      li>
    ul>
  div>
template>


数据渲染二级分类




2. 鼠标移入layer展示

实现步骤

  • 布局交互
    • 每个Li标签对应一个自己的layer弹层,默认全部隐藏
    • Li 标签hover状态的时候让自己下面的layer弹层展示出来
  • 数据获取
    • 把goods字段也导入进来

代码落地

1)准备布局


<div class="layer">
  <h4>分类推荐 <small>根据您的购买或浏览记录推荐small>h4>
  <ul>
    <li v-for="i in 9" :key="i">
      <RouterLink to="/">
        <img src="https://yanxuan-item.nosdn.127.net/5a115da8f2f6489d8c71925de69fe7b8.png" alt="">
        <div class="info">
          <p class="name ellipsis-2">【定金购】严选零食大礼包(12件)p>
          <p class="desc ellipsis">超值组合装,满足馋嘴欲p>
          <p class="price"><i>¥i>100.00p>
        div>
      RouterLink>
    li>
  ul>
div>

2)导入新增goods字段

const list = computed(() => {
  return home.categoryList.map((item) => {
    return {
      id: item.id,
      name: item.name,
      children: item.children.slice(0, 2),
      // 添加 goods 字段
      goods: item.goods, 
    }
  })
})

3)渲染模板视图


<div class="layer">
  <h4>分类推荐 <small>根据您的购买或浏览记录推荐small>h4>
  <ul>
    <li v-for="goods in item.goods" :key="goods.id">
      <RouterLink to="/">
        <img :src="goods.picture" alt="" />
        <div class="info">
          <p class="name ellipsis-2">
            {{ goods.name }}
          p>
          <p class="desc ellipsis">{{ goods.desc }}p>
          <p class="price"><i>¥i>{{ goods.price }}p>
        div>
      RouterLink>
    li>
  ul>
div>

XtxUI 组件库

基本使用

任务目标: 把组件库从素材文件夹,复制到项目中使用。

核心步骤:

  1. 复制素材中的 components文件夹下所有 XtxUI 组件,放到 src/components 中。
  2. yarn lintnpm run lint 格式化文件。
  3. 使用组件库提供的组件。

新建测试页面:src\views\Test\index.vue






局部注册组件

任务目标: 组件库中封装了统一出口,修改导入组件库组件的方式。





全局注册组件

**任务目标:**以插件的形式注册全局组件

核心步骤

  1. 新建文件components/index.ts
import type { App, Plugin } from 'vue'
import Skeleton from './Skeleton/Skeleton.vue'

const XtxUI: Plugin = {
  install(app: App) {
    app.component(`XtxSkeleton`, Skeleton);
  },
};

export default XtxUI;


  1. main.ts中全局注册
import XtxUI from "./components/XtxUI";

const app = createApp(App)
app.use(XtxUI)

  1. 在页面中使用。




  • 问题:全局组件注册成功,但是调用时没有 TS 类型提示。
  • 解决方案:为组件库创建对应的类型声明文件。

全局组件 TS 类型声明文件

任务目标: 为全局组件书写对应的 TS 类型声明文件。

  • 用法参考:element-plus 源码 Element-Plus 源码链接
  • Volar 插件说明:Volar 插件说明

新建类型声明文件: src\components\XtxUI\global.d.ts ,准备基本结构

// 全局组件类型声明文件 for Volar
declare module 'vue' {
  // 全局组件需要定义 interface GlobalComponents
  export interface GlobalComponents { 
     全局组件名: 组件类型;
  }
}

export { }

添加全局组件类型声明:

+// 导入 .vue 源文件
+import Button from "./Button/index.vue";
+import Skeleton from "./Skeleton/Skeleton.vue";

// 全局组件类型声明文件 for Volar
declare module "vue" {
  // 全局组件需要定义 interface GlobalComponents
  export interface GlobalComponents {
+    // typeof 获取 TS 类型
+    XtxButton: typeof Button;
+    XtxSkeleton: typeof Skeleton;
  }
}

export {};

骨架组件

基本使用

任务目标: 在分类模块中使用我们定义好的骨架组件增强用户体验

核心步骤:

  1. 测试 XtxSkeleton 的使用


分类优化

**任务目标:**能够使用骨架组件优化首页的分类展示

核心步骤

  1. Home/components/home-category.vue中优化左侧分类的展示
  1. Layout/components/app-header-nav.vue中优化头部导航的展示
  • 首页

注意:

  • v-if 和 v-for 指令在 Vue2Vue3 的情况不同,
  • Vue3v-if 的优先级比 v-for 更高
  • 推荐在同一元素上使用 v-ifv-for,建议配合template 标签进行处理。参考资料

轮播图功能

获取数据

任务目标: 基于 pinia 获取轮播图数据

核心代码:

  1. store/modules/home.ts文件中封装接口,获取轮播图数据
const useHomeStore = defineStore('home', {
  actions: {
    // ...
    // 获取轮播图数据
    async getBannerList() {
      const res = await http('GET', '/home/banner');
      console.log('/home/banner', res);
    },
  }
})

export default useHomeStore


  1. 在组件中发送请求Home/components/home-banner.vue


  1. 根据请求返回的数据,在src\types\api\home.d.ts 文件中定义对应的 TS 类型声明。
// 轮播图类型
export interface Banner {
  id: string;
  imgUrl: string;
  hrefUrl: string;
  type: string;
}

export type BannerList = Banner[];


  1. store/modules/home.ts文件中,完善 TS 类型声明。
-import type { CategoryList } from '@/types'
+import type { CategoryList, BannerList } from '@/types'
const useHomeStore = defineStore('home', {
  state: () => ({
    // ...
    // 轮播图数据
+    bannerList: [] as BannerList
  }),
  actions: {
    // ...
    // 获取轮播图数据
    async getBannerList() {
-      const res = await http("GET", "/home/banner");
+      const res = await http("GET", "/home/banner");
+      this.bannerList = res.data.result;
    },
  }
})

export default useHomeStore


渲染轮播图(快速实现)

**任务目标:**基于封装好的轮播图组件快速实现 Banner 模块

  1. 在Banner组件中使用Home/components/home-banner.vue




首页主体-面板组件封装

新鲜好物、人气推荐俩个模块的布局结构上非常类似,我们可以抽离出一个通用的面板组件来进行复用

任务目标: 封装一个通用的面板组件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NUzI4WPP-1668072603846)(media/panel.png)]

思路分析

  1. 图中标出的四个部分都是可能会发生变化的,需要我们定义为可配置
  2. 主标题和副标题由于是纯文本,我们定义成 props 即可
  3. 右侧内容和主体内容由于可能会传入较为复杂的自定义模板,我们定义成 slot 利用插槽渲染

核心代码

  • 组件编写

Home/components/home-panel.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>
        
        <slot name="right">slot>
      div>
      
      <slot>slot>
    div>
  div>
template>

<style scoped lang="less">
.home-panel {
  background-color: #fff;
  .head {
    padding: 40px 0;
    display: flex;
    align-items: flex-end;
    h3 {
      flex: 1;
      font-size: 32px;
      font-weight: normal;
      margin-left: 6px;
      height: 35px;
      line-height: 35px;
      small {
        font-size: 16px;
        color: #999;
        margin-left: 20px;
      }
    }
  }
}
style>


  • 使用home-pannel组件Home/index.vue
<script setup lang="ts">
import HomeBanner from './components/home-banner.vue';
import HomeCategory from './components/home-category.vue';
import HomePanel from './components/home-panel.vue';
script>

<template>
  <div class="page-home">
    
    <div class="home-entry">
      <div class="container">
        
        <HomeCategory />
        
        <HomeBanner />
      div>
    div>
    
    <HomePanel title="大标题" sub-title="副标题">
      <template #right>
        <XtxMore />
      template>
      <h2>我是主体内容-默认插槽h2>
    HomePanel>
  div>
template>

<style lang="less" scoped>style>


首页主体-新鲜好物

目标:封装一个新鲜好物的组件,用于处理新鲜好物模块。

  1. 创建组件,准备静态结构 Home/components/home-new.vue







  1. 修改 src\views\Home\index.vue ,引入并使用组件。




  1. store/modules/home.ts文件中封装请求
// ...
const useHomeStore = defineStore('home', {
  // ...
  actions: {
    // ...
    async getNewGoodsList() {
      const res = await http('GET', '/home/new');
      console.log('/home/new', res);
    }
  }
})


  1. Home\components\home-new.vue 组件中,调用 actions 获取数据


  1. 完善类型声明文件(商品类型可复用)
// 电商网站商品的类型基本一致,可以复用
export interface Goods {
  id: string;
  name: string;
  desc: string;
  price: string;
  picture: string;
  orderNum: number;
}

// 商品列表类型(可以复用)
export type GoodsList = Goods[];

  1. 完善 store/modules/home.ts 文件中的类型
import { defineStore } from 'pinia'
import { http } from '@/utils/request'
-import type { CategoryList, BannerList } from '@/types'
+import type { CategoryList, BannerList, GoodsList } from '@/types'
const useHomeStore = defineStore('home', {
  state: () => ({
     // ...
+    newGoodsList: [] as GoodsList
  }),
  actions: {
    // ...
    async getNewGoodsList() {
-      const res = await http("GET", "/home/new");
+      const res = await http("GET", "/home/new");
+      this.newGoodsList = res.data.result
    }
  }
})

export default useHomeStore


  1. Home\components\home-new.vue 组件中,完成列表渲染


首页主体-人气推荐(课后作业)

温馨提示:人气推荐的逻辑和新鲜好物的逻辑基本一致

(1)发送请求,获取数据 src/store/modules/home.ts

const useHomeStore = defineStore('home', {
  state: () => ({
+    hotGoodsList: [] as GoodsList
  }),
  actions: {
+    async getHotGoodsList() {
+      const res = await http("GET", "/home/hot");
+      this.hotGoodsList = res.data.result
+    }
  }
})


(2)创建组件Home/components/home-hot.vue








(3)首页中渲染src/views/home/index.vue








按需请求(请求懒加载)

电商项目核心优化技术手段:组件数据懒加载 (首屏渲染优化)

说明:电商类网站的首页内容会有好几屏,如果直接加载并渲染所有屏的数据,会比较浪费性能。

优化:应该 当模块进入到 可视区 ,再发请求获取数据

检测目标元素的可见性

任务目标: 了解如何检测目标元素的可见性

技术方案:

我们可以使用 @vueuse/core 中的 useIntersectionObserver 来实现监听组件进入可视区域行为,

需要配合 vue3 的组合 API 的方式才能实现

https://vueuse.org/core/useIntersectionObserver/

先分析下这个useIntersectionObserver 函数:






我们以新鲜好物模块为例演示一下这个函数的使用方式

1)通过 ref 属性获得组件实例并测试

2)使用useIntersectionObserver监听函数

<script setup lang="ts">
import HomePanel from "./home-panel.vue";
import { ref } from "vue";
import { useIntersectionObserver } from "@vueuse/core";
import useStore from "@/store";
const { home } = useStore();
// 通过 ref 获得组件实例
const target = ref(null);
const { stop } = useIntersectionObserver(
  // target 被检测的目标元素
  target,
  // isIntersecting 是否进入可视区域
  ([{ isIntersecting }]) => {
    // 在此处可根据isIntersecting来判断,然后做业务
    console.log('是否进入可视区域', isIntersecting);
    if (isIntersecting) {
      home.getHotGoodsList();
      stop();
    }
  }
);
script>

<template>
  <div class="home-hot">
    
    <HomePanel ref="target" title="人气推荐" sub-title="人气爆款 不容错过">
      ...
    HomePanel>
  div>
template>

3)测试效果

打开浏览器,人气推荐模块还未进入到可视区,打印值为false,

然后我们滑动页面,当人气模块组件进入可视区中时,再次发生打印,此时为true,

到此我们就可以判断组件进入和离开可视区了

**特别注意:**每次被监听的dom进入离开可视区时都会触发一次,而不是只触发一次, 可以stop关闭监听

具体业务实现

任务目标: 利用我们捋清楚的发送请求的位置实现业务数据拉取完成实际业务功能

实现步骤

  1. 发送的ajax请求在isIntersecting 为true时触发
  2. 一旦触发一次之后停止监听,防止接口重复调用

代码落地

<script setup lang="ts">
 // ...省略
script>




<HomePanel ref="target" title="人气推荐" sub-title="人气爆款 不容错过">...HomePanel>

组件数据懒加载逻辑复用

本节目标: 抽离组件数据懒加载可复用的逻辑

现存问题

首页中,很多地方都应该使用组件数据懒加载这个功能,不管是哪个模块使用,下面代码都会重复书写

事实上,唯一可能会随着业务使用发生变化的是 ajax接口的调用

其余的部分我们进行重复使用,抽离为可复用逻辑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNzOgAbJ-1668072603847)(media/cma1.png)]

抽离通用逻辑

1)抽离逻辑

src/hooks/index.ts

import { useIntersectionObserver } from "@vueuse/core";
import { ref } from "vue";

/**
 * 请求按需加载
 * @param apiFn 发送请求函数
 * @returns   target 用于模板绑定
 */
export const useObserver = (apiFn: () => void) => {
  // 准备个 ref 用于绑定模板中的某个目标元素(DOM节点或组件)
  const target = ref(null);
  const { stop } = useIntersectionObserver(target, ([{ isIntersecting }]) => {
    console.log("是否进入可视区域", isIntersecting);
    if (isIntersecting) {
      // 当目标元素进入可视区域时,才发送请求
      apiFn();
      // 请求已发送,主动停止检查
      stop();
    }
  });
  // 返回 ref 用于模板绑定,建议返回对象格式支持解构获取
  return { target };
};


2)业务改写



骨架组件 - 优化默认显示结构

Pinia 持久化存储 - 首页数据缓存

目标: 通过 Pinia 插件快速实现持久化存储。

插件文档:点击查看

用法

安装

yarn add pinia-plugin-persistedstate
# 或
npm i pinia-plugin-persistedstate

使用插件

+ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia();
+ pinia.use(piniaPluginPersistedstate);
app.use(pinia);

模块开启持久化

const useHomeStore = defineStore("home",{
+  persist: true
   state:()=>({})
  // ...省略
});

常见疑问

  • Vue2 能不能用 Pinia 和 持久化存储插件。
    • 可以使用,需配合 @vue/composition-api 先让 Vue2 老项目支持 组合式API
    • Pinia 能在 组合式API 中使用。
  • 模块做了持久化后,以后数据会不会变,怎么办?
    • 先读取本地的数据,如果新的请求获取到新数据,会自动把新数据覆盖掉旧的数据。
    • 无需额外处理,插件会自己更新到最新数据。

进阶用法

需求:不想所有数据都持久化处理,能不能按需持久化所需数据,怎么办?

  • 可以用配置式写法,按需缓存某些模块的数据。
import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  state: () => ({
      someState: 'hello pinia',
      nested: {
        data: 'nested pinia',
      },
  }),
  // 所有数据持久化
  // persist: true,
  // 持久化存储插件其他配置
  persist: {
    // 修改存储中使用的键名称,默认为当前 Store的 id
    key: 'storekey',
    // 修改为 sessionStorage,默认为 localStorage
    storage: window.sessionStorage,
    // 按需持久化,默认不写会存储全部
    paths: ['nested.data'],
  },
})

你可能感兴趣的:(前端,商城,VUE,学习,vue.js,javascript)