Vue3 笔记

前言
Vue3 介绍

优势

%title插图%num

基于 vue2 的知识,可以很快理解 vue3 的特性。使用 TypeScript 支持;

Vue2 组合式 API 对比 Vu3 选项式 API

%title插图%num

早期 vu2 对于大项目的维护比较困难,原因是因为每个内容放在不同的区域中。若需要改动某项方法,则可能连带着其他区域的内容进行修改。
大量的代码都要上下滑动找到对应的内容。造成不必要的麻烦。

%title插图%num

vue3 从 vue2 的 选项式 的代码书写,变成了 集中式 的代码书写。从而集中式管理;

代码书写风格

实现 点击按钮 让数字 + 1 的业务

%title插图%num

vue3 的集中式管理使得 代码量更精简,从分散式维护转为集中式维护,更容易封装复用

Vue3 官方文档

Vue3 创建

认识 create-vue

对比 vue2 的 vue@cil 脚手架工具,官方提倡使用官方新的脚手架工具。因为其底层 从 wabpack 打包切换到了 vite (下一代构建工具),
为开发提供极速响应;

%title插图%num
vite 的构建和运行的速度极快

使用 create-vue 创建项目

确保已安装 Node.js 的 16.0 或者更高版本

nodejs:^16.0

创建一个 Vue应用

PowerShell
# 这将会安装并执行 create-vue
npm init vue@latest

配置项目 (初期学习全部勾选 否 即可)

PowerShell
√ 请输入项目名称: ... vue-demo
√ 是否使用 TypeScript 语法? .../
√ 是否启用 JSX 支持? .../
√ 是否引入 Vue Router 进行单页面应用开发? .../
√ 是否引入 Pinia 用于状态管理? .../
√ 是否引入 Vitest 用于单元测试? .../
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? .../

正在构建项目 D:\smm\code\vue\vue3\vue-demo...

安装依赖、启动项目

PowerShell
# 跳转到项目目录 vue-demo 为你创建的项目名
cd vue-demo
# 安装依赖
npm install
# 运行项目
npm run dev
%title插图%num

完成安装,Vue 启动后。项目默认部署在 http://127.0.0.1:5174 上

%title插图%num

启动成功后访问项目的界面如下图所示:

%title插图%num
Vue3 目录

目录介绍

Vue3 的项目目录和关键文件如下:

  1. vite.config.js – 项目的配置文件 基于 vite 的配置
  2. package.json – 项目包文件 核心依赖项变成了 Vue3.x 和 vite
  3. main.js – 入口文件 createApp 函数创建应用实例
  4. app.vue – 根组件 SFC 单文件组件 script-template-style
    • 变化1:脚本 script 和模板 template 顺序调整
    • 变化2:模板 template 不在要求唯一根元素
    • 变化3:脚本 script 添加 setup 表示支持组合式 API
  5. index.html – 单页面入口 提供 id 为 app 的挂载点
%title插图%num

vite.config.js

vite.config.js
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      // 允许我们以 @ 方式去访问对应目录 (Vue2 中默认底层已配置)
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

package.json

package.json
{
  "name": "vue-demo",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.3.11"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.5.2",
    "vite": "^5.0.10"
  }
}

mian.js

mian.js
import './assets/main.css'

// 从 new Vue() 改成了 createApp() 创建应用实例
// 将来 创建路由 和创建 store 变成了 
// createRoter()、createStore()
// 目的是为了保证每个实例的独立封闭性,不受其他模块的影响

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

App.vue

App.vue
<!-- 逻辑 -->
<!-- setup 是允许在 script 中直接编写组合式 API -->
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>
<!-- 结构 -->
<!-- 允许根目录存在多元素 -->
<template>
  <header>
    <img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />

    <div class="wrapper">
      <!-- 局部注册 引入即用 -->
      <HelloWorld msg="You did it!" />
    </div>
  </header>

  <main>
    <TheWelcome />
  </main>
</template>
<!-- 样式 -->
<style scoped>
header {
  line-height: 1.5;
}

.logo {
  display: block;
  margin: 0 auto 2rem;
}

@media (min-width: 1024px) {
  header {
    display: flex;
    place-items: center;
    padding-right: calc(var(--section-gap) / 2);
  }

  .logo {
    margin: 0 2rem 0 0;
  }

  header .wrapper {
    display: flex;
    place-items: flex-start;
    flex-wrap: wrap;
  }
}
</style>
vscode 插件

由于 Vue2 使用的 vuter 不适用于 Vue3 项目,需要禁用该插件并启用 Vue3 支持的插件 volar

%title插图%num
组合式 API
格式

组合式 API 的入口 就是 setup

setup 选项的写法和执行时机

setup 是一个钩子函数,在 vue 的生命周期中它的执行时机要先于 beforCreate ,因此 setup 中无法获取到 this

%title插图%num
Vue
<script>
export default {
  // setup 函数中是获取不到 this, this 是 undefined
  setup() {
    console.log('setup 函数', this);
  },
  beforeCreate() {
    console.log('beforCreate 函数')
  }
}
</script>

效果演示

%title插图%num

setup 的函数和使用

由于 setup 函数是在 vue 创建前执行,若想讲 setup 中的实际数据提供给其他区域,需要直接 return 出来

Vue
<script>
export default {
  setup() {
    const message = 'Hello word';
    const logMessage = () => {
      console.log('这是一个提示');
    }
    // 将需要的数据 return 出去
    return {
      message,
      logMessage
    }
  }
}
</script>
<template>
  <div>{{ message }}</div>
  <button @click="logMessage">按钮</button>
</template>

效果演示:

%title插图%num

<script setup> 语法糖

为了方便开发者能简化上方步骤,即每次使用都需要 return,官方提供了语法糖来代替 return 的操作;
即:在 script 的左侧加上 setup 修饰,Vue3 会自动将加了 setup 修饰的 script 标签进行处理,并 return 出去

Vue
<script setup>
const message = 'Hello word';
const logMessage = () => {
  console.log('这是一个提示');
}
</script>
<template>
  <div>{{ message }}</div>
  <button @click="logMessage">按钮</button>
</template>

<style>
body {
  padding-left: 700px;
}
</style>

效果演示:

%title插图%num
reactive

Vue3中,默认的数据并不是响应式的,使用 import 导入 reactive 模块,会将参数中的对象转成响应式并返回该对象

说明

接收对象类型数据的参数传入并返回一个响应式对象。

  1. 从 vue 包中导入 reactive 函数
  2. 在 <script setup> 中执行 reactive 函数并传入类型为对象的初始值,并使用遍历接收返回值
Vue
<script setup>
// 导入 reactive 模块
import { reactive } from 'vue';

// reactive 接收一个对象类型的数据,返回一个响应式的对象
const state = reactive({ count: 1 });
// 实现方法
const setCount = () => {
  state.count++;
  console.log(state.count);
}
</script>
<template>
  <!-- 响应式的数据当发生改变时 会动态渲染界面 -->
  <div>{{ state.count }}</div>
  <button @click="setCount">+1</button>
</template>

效果演示

%title插图%num
ref

Vue3中,默认的数据并不是响应式的,使用 import 导入 ref 模块,会将参数中的对象(复杂类型)或简单类型数据转成响应式并返回该对象

推荐使用 ref 方法声明数据,因为该方法比起 reactive 方法更灵活实用

说明

接收 简单类型 或 复杂类型 的数据,返回一个响应式的对象。

本质上是再原有传入数据的基础上,外层包了一层复杂类型的对象。因此打印 ref 导出的内容时候,可以看到 ref 返回的是一个对象。
其中传入 ref 的值放在了对象成员的 value 中。

它底层的逻辑就是将简单数据包装成复杂类型之后,在借助 reactive 实现响应式数据。

Vue
<script setup>
import { ref } from 'vue';


// reactive 接收一个对象类型的数据,返回一个响应式的对象
const count = ref(0);
console.log(count);
</script>
%title插图%num
  1. 从 vue 包中导入 ref 函数
  2. 在 <script setup> 中执行 ref 函数并传入类型为复杂或简单类型的数据,并使用遍历接收返回值
Vue
<script setup>
import { ref } from 'vue';

const count = ref(0);
// 实现方法
const setCount = () => {
  count.value++;
  console.log(count.value); // 通过 value 获取到数据
}
</script>
<template>
  <!-- 响应式的数据当发生改变时 会动态渲染界面 -->
  <!-- 在 template 中使用 ref 数据不需要添加 .value,因为vue 帮我们扒了一层 --> 
  <div>{{ count }}</div>
  <button @click="setCount">+1</button>
</template>

效果演示

%title插图%num
computed

computed 是 vue3 的计算属性,它的计算属性基本思想和 vue2 的完全一致,只是在组合式API下修改了写法。

操作

  1. 导入 computed 函数,并创建一个只读的计算属性 ref;
  2. 指向函数 在 回调参数 中 return 基于响应式数据做计算的值,用变量接收
Vue
<script setup>
import { computed, ref } from 'vue';

const state = ref([1, 2, 3, 4]);
// 计算属性会在回调参数中的 return 计算值
const computedState = computed(() => {
  // 基于响应式数据做计算后的值
  return state.value.map(item => item * 2);
})
</script>
<template>
  <!-- 响应式的数据当发生改变时 会动态渲染界面 -->
  <div>原数组:{{ state }}</div>
  <div>计算属性返回数组:{{ computedState }}</div>
</template>

效果演示

%title插图%num

get set 操作

创建一个可写的计算属性 ref;但是一般情况下 计算属性不应该有副作用,并尽量避免直接修改计算属性的值

  1. 不建议增加副作用:例如去发起一些请求等操作,操作dom是不合理的,计算属性应该只做数据的展示
  2. 布建议去修改计算属性的值:因为计算属性建议仅用于 只读
Vue
<script setup>
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0
</script>
watch

watch 是用于声明在数据更改时调用的侦听回调;

作用:侦听一个或者多个数据的变化,数据变化时立即执行回调函数;拥有俩个额外参数:

  1. immediate (立即执行)
  2. deep (深度监听)

侦听单个数据

使用 watch(需要监听的响应式数据,(新值,旧值)=>{ // 操作的方法 }) 语法实现:

Vue
<script setup>
import { ref, watch } from 'vue';
const state = ref(0);

const addNum = function () {
  state.value++
}

// 调用 watch 侦听变化
watch(state, (newValue, oldValue) => {
  console.log(newValue, oldValue);
})
</script>
<template>
  <div>{{ state }}</div>
  <button @click="addNum">+1</button>
</template>

效果演示

%title插图%num

侦听多个数据的变化

传入数组,同时监听多个响应式数据的变化,数组中任何一个数据的值发生变化时,均会触发 watch 侦听器的回调函数

Vue
<script setup>
import { ref, watch } from 'vue';
const num_1 = ref(0);
const num_2 = ref(0);

const addNum_1 = function () {
  num_1.value++
}
const addNum_2 = function () {
  num_2.value++
}

// 调用 watch 侦听变化
watch([num_1, num_2], (newValue, oldValue) => {
  console.log('监听数据1:' + newValue[0], oldValue[0]);
  console.log('监听数据2:' + newValue[1], oldValue[1]);
})
</script>
<template>
  <div>{{ num_1 }}</div>
  <div>{{ num_2 }}</div>
  <button @click="addNum_1">+1</button>
  <button @click="addNum_2">+1</button>
</template>

效果演示

%title插图%num

初始化触发、深度监听

immediate 和 deep 参数 可以实现侦听器初始化时触发和深度监听对象数据的操作

  1. immediate 参数声明成 true 后,会在侦听器创建时候立即触发回调,响应式数据发生变化之后继续执行回调
  2. deep 为 true 时会开启深度监听,watch 默认是浅层监听,即:只有对象指向的复杂数据的下标发生改变时才触发
Vue
<script setup>
import { ref, watch } from 'vue';

const obj = ref({
  name: '点赞数',
  total: 0
})

const upTotal = function () {
  obj.value.total++;
}

// 调用 watch 侦听变化 开启深度监听
watch(obj, (newValue, oldValue) => {
  console.log(newValue, oldValue);
}, { deep: true })
</script>
<template>
  <div>{{ obj.name }}</div>
  <div>{{ obj.total }}</div>
  <button @click="upTotal">+1</button>
</template>

效果演示

%title插图%num

精确侦听对象属性

注意侦听的对象需要使用下方的固定写法:watch(()=> 值 ,()=>{ // 回调方法 })

若只需要监听对象中某个值的变化,在不开启 deep 的前提下能触发 watch 的回调函数,可以使用该操作:

Vue
<script setup>
import { ref, watch } from 'vue';

const obj = ref({
  name: '点赞数',
  total: 0
})

const upTotal = function () {
  obj.value.total++;
}

// 调用 watch 侦听变化 精确侦听 obj.value.total 值
watch(
  () => obj.value.total,
  (newValue, oldValue) => {
    console.log(newValue, oldValue);
  })
</script>
<template>
  <div>{{ obj.name }}</div>
  <div>{{ obj.total }}</div>
  <button @click="upTotal">+1</button>
</template>

效果演示

%title插图%num
生命周期

Vue3 也像 Vue2 一样,提供了 生命周期的 API,对照表如下:

说明 选项式API 组合式API
初始化 beforeCreate/created setup
挂载前 beforeMount onBeforeMount
挂载完成 mounted onMounted
更新前 beforeUpdate onBeforeUpdate
更新完成 updated onUpdated
销毁前 beforeUnmount onBeforeUnmount
销毁完成 unmounted onUnmounted

写法

多次调用相同的生命周期的钩子函数,会按照顺序依次执行,互不冲突;

Vue
<script setup>
import { onMounted } from 'vue';

// 挂载阶段 钩子函数
onMounted(() => {
  console.log('mounted 生命周期函数');
})

// 进入页面就触发
console.log('setup 生命周期函数');

// 可以调用多次生命周期函数
onMounted(() => {
  console.log('mounted 第二个生命周期函数');
})
</script>

效果演示

%title插图%num
父子通信

子组件引用 – 局部引入

一般组件定义在项目的 src/components 文件夹下,页面需要引用可使用 import 方式引用。
引用无需声明注册,可直接在 <template> 中使用

页面
<script setup>
import item from './components/item.vue'
</script>
<template>
  <div class="content">
    <h1>父组件</h1>
    <item></item>
  </div>
</template>

<style scoped>
.content {
  width: 400px;
  height: 200px;
  text-align: center;
  border: 5px dashed green;
}
</style>

引用子级组件

Vue
<script setup>
</script>
<template>
    <div class="compCont">
        <h1>子组件</h1>
    </div>
</template>

<style scoped>
.compCont {
    display: inline-block;
    padding: 20px 50px;
    border: 4px dashed #66cc;
}
</style>

效果展示

%title插图%num

父传子

父组件中给子组件绑定属性,子组件内部通过 defineProps() 提供的 编译器宏 来接收内容;

defineProps 原理:在编译阶段做一个标识,编译解析过程时会在对应区域进行编译转换

界面
<script setup>
import { ref } from 'vue';
import item from './components/item.vue'

const sayHi = ref('hello word');
const num = ref(0);
const upNum = function () {
  num.value++;
}

</script>
<template>
  <div class="content">
    <h1>父组件</h1>
    <button @click="upNum">+1</button>
    <!-- 父向子传参 -->
    <item :sayHi="sayHi" :num="num"></item>
  </div>
</template>
子级组件
<script setup>
// 接收父级传递的参数
defineProps({
    sayHi: String,
    num: Number
})

</script>
<template>
    <!-- 展示内容 -->
    <div class="compCont">
        <h1>子组件</h1>
        <p>{{ sayHi }}</p>
        <p>{{ num }}</p>
    </div>
</template>

效果展示

%title插图%num

子传父

子传父是通过事件来传递信号,由父级来直接操作数据;父组件中给子组件通过@绑定事件,子组件通过 emit 方法触发事件。

Vue3 规定了在子组件中需要定义 defineEmits() 编译器宏生成 emit 方法,通过编译器宏生成的 emit 创建对应的事件

界面
<script setup>
import { ref } from 'vue';
import item from './components/item.vue'

const sayHi = ref('hello word');
const num = ref(0);
const upNum = function () {
  num.value++;
}

// 触发子级组件的事件 并 接收子级传递的参数 进行赋值
const subNum = function (subValue) {
  num.value = num.value + subValue;
}

</script>
<template>
  <div class="content">
    <h1>父组件</h1>
    <button @click="upNum">+1</button>
    <item :sayHi="sayHi" :num="num" @sub="subNum"></item>
  </div>
</template>
子级组件
<script setup>
const emit = defineEmits(['sub'])
defineProps({
    sayHi: String,
    num: Number
})

// 向父级页面触发事件 并传入参数 -1
const subCallFn = () => {
    emit('sub', -1);
}
</script>
<template>
    <div class="compCont">
        <h1>子组件</h1>
        <p>{{ sayHi }}</p>
        <p>{{ num }}</p>
        <button @click="subCallFn">-1</button>
    </div>
</template>

效果演示

%title插图%num
模板引用

概念

通过 ref 标识获取真实的 dom对象 或者 组件实例对象

  1. 调用 ref 函数生成一个 ref 对象
  2. 通过 ref 标识绑定 ref 对象到标签
Vue
<script setup>
import { onMounted, ref } from 'vue';

// 变量名需要与 template 的标签的 ref 属性同名
const h1Ref = ref(null);

// 挂载完成事件
onMounted(() => {
  // 获取到了对应 <h1> 的 Dom
  console.log(h1Ref.value);
})

</script>
<template>
  <div class="content">
    <h1 ref="h1Ref">我是dom</h1>
  </div>
</template>

效果演示

%title插图%num

获取网页 Dom 元素

ref 绑定标签效果:实现点击让输入框聚焦

Vue
<script setup>
import { ref } from 'vue';

const iptRef = ref(null);

const focusFn = function () {
  iptRef.value.focus();
  console.log('已聚焦');
}

</script>
<template>
  <div class="content">
    <input ref="iptRef" type="text">
    <button @click="focusFn">实现聚焦</button>
  </div>
</template>

效果演示

%title插图%num

获取 Vue 组件实例

通过 ref 去获取组件的实例

Vue
<script setup>
import { ref } from 'vue';
import testCom from './components/testCom.vue';

const testRef = ref(null);

const getCom = () => {
  console.log(testRef.value);
}
</script>
<template>
  <div class="content">
    <button @click="getCom">获取组件</button>
    <testCom ref="testRef"></testCom>
  </div>
</template>

获取到组件后,理论上就可以拿到子组件中 defineExpose 内向外暴露的属性和方法

效果演示

%title插图%num

Vue 组件实例向外暴露对应的属性和方法

默认情况下在 <script setup> 语法糖下的组件内部的属性和方法是不开放给父组件访问的,
可以通过 defineExpose 编译宏指定那些熟悉和方法允许访问。

子组件
<script setup>
const count = 1000;
const sayHi = function () {
    console.log('我是子组件的方法');
}

defineExpose({
    count,
    sayHi
})
</script>

完成后,在打印组件时就会显示对应内容

%title插图%num
自定义指令

作用和场景

除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)

一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:

局部注册

Vue
<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

效果展示

%title插图%num

全局注册

将一个自定义指令全局注册到应用层级

JavaScript
const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('focus', {
  /* ... */
  mounted(el, binding) {
        /* ... */
    }
})

指令钩子函数

一个指令的定义对象可以提供几种钩子函数 (都是可选的)

JavaScript
const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

指令传参(简化)

通过 v-指令名:”参数” 绑定指令接收的参数,全局注册指令可通过 app.directive(el,binding) 参数2 的 binding.value 接收

Vue
<div v-color="color"></div>
JavaScript
app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

更多内容可以参考 官方文档

provide 和 inject

作用和场景

provide 和 inject 的效果是实现顶层组件向任意的底层组件传递数据和方法,实现跨层级组件通信

%title插图%num

实现语法:

只要存在层级关系的组件,都可以跨层级通信

  1. 顶层组件通过 provide 函数提供数据
  2. 底层组件通过 inject 函数获取数据
顶层组件
provide('key',顶层组件中的数据)
底层组件
const message = inject('key')

顶层给底层传数据

该方法是为了顶层组件能与底层组件能直接传递数据,下方是一个传递普通数据的案例:

顶层组件
<script setup>
import cententCom from '@/components/centent-com.vue';
import { provide } from 'vue';

// 1. 跨层传递普通数据
provide('theme-color','red');
</script>

<template>
    <h1>我是顶层组件</h1>
    <cententCom></cententCom>
</template>
中间组件
<script setup>
import bottomCom from '@/components/bottom-com.vue'
</script>

<template>
<h2>我是中间组件</h2>
<bottomCom></bottomCom>
</template>
底层组件
<script setup>
import { inject } from 'vue';
const themeColor = inject('theme-color')
</script>

<template>
    <h3 :style="{ color: themeColor }">我是底层组件 {{ themeColor }}</h3>
</template>

效果演示

%title插图%num

可以看到,顶层组件的 provide() 传入的参数被底层组件的 inject(); 接收到,并直接改变了对应内容;

合理的操作和传递

数据通过 provide 传到 底层组件,如果底层需要改变顶层传入的数据的值。不建议直接进行操作。
而是依靠顶层传入的方法来进行操作;

相当于外卖送了一个炸鸡,并附带一个手套。意思是由数据供应方提供处理数据的操作

底层调用顶层方法改数据

顶层组件
<script setup>
import cententCom from '@/components/centent-com.vue';
import { provide, ref } from 'vue';

const count = ref(10)
// 跨层传递响应式数据
provide('count', count);
// 跨层传递修改方法
provide('changeCount', (newVal) => {
    count.value = newVal;
})
</script>
<template>
    <h1>我是顶层组件</h1>
    <span>顶层数据为 {{ count }}</span>
    <cententCom></cententCom>
</template>
中间组件
<script setup>
import bottomCom from '@/components/bottom-com.vue'
</script>

<template>
<h2>我是中间组件</h2>
<bottomCom></bottomCom>
</template>
底层组件
<script setup>
import { inject } from 'vue';
const count = inject('count');
const chuangeFn = inject('changeCount')

function addFn() {
    chuangeFn(count.value + 1);
}
</script>

<template>
    <h3>我是底层组件</h3>
    <span>接收数据为 {{ count }}</span>
    <button @click="addFn">数据自增</button>
</template>

效果演示

%title插图%num
defineOptions

新特性诞生背景

有 <script setup> 之前,如果定义 props,emits 可以轻而易举的添加一个与 setup 平级的熟悉;

Vue
<script>
export default {
    setup() {
        // ...
    },
    methods() {
        // ...
    },
    props() {
        // ...
    }
}
</script>

但是用了 <script setup> 后,就没法这么干了 setup 属性已经没有了,自然无法添加与其平级的属性。

为了解决这个问题,引入了 defineProps 与 defineEmits 这俩个宏。但是只是解决了 props 与 emit 这俩个属性引入。

如果我们要在 <script setup> 中定义组件的 name 或者其他自定义的属性,就用到了 defineOptions;

Vue3.3 中新引入了 defineOptions 宏,用来定义 Options API 的选项。可以用 defineOptions 定义任意的选项。
(除了 props,emits,expose,slots 除外,因为这些已经预先存在对应的编辑器宏的函数)

Vue
<script setup>

defineOptions({
  name: 'Demo',
  inheritAttrs: false
  // ...更多自定义属性
})
}
</script>
defineModel

该新特性目前为实验性

新特性诞生的背景

在 vue3 中,自定义组件上使用 v-model 相当于传递了一个 modelValue属性,同时触发 update:modelValue 事件。
如果自定义的子组件要实现 v-model 双向绑定,则对应就得写一些比较麻烦的操作;而 defineModel 就是为了简化这样的操作而诞生的。

Vue 的指令

Vue
<script setup>
import { ref } from 'vue';
import sonCom from '@/components/son-com.vue';

const text = ref('你好~');
</script>
<template>
    <sonCom v-model="text"></sonCom>
</template>

效果相当于

Vue
<script setup>
import { ref } from 'vue';
import sonCom from '@/components/son-com.vue';

const text = ref('你好~');
</script>
<template>
    <sonCom :text="text" @update:text="text = $event"></sonCom>
</template>

若是以前需要实现类似 Vue2 的双向绑定就要先定义 Props,再定义 emits。其中有许多重复的代码,
子组件需要动态修改此值,还需要手动调用 emit 函数

父级定义 v-model

父级界面
<script setup>
import { ref } from 'vue';
import myInput from '@/components/myInput.vue';

const text = ref('');
</script>
<template>
  <div class="content">
    <p>父级页面值:{{ text }}</p>
    <myInput v-model="text"></myInput>
  </div>
</template>

子级组件要实现 modelValue 的传值 和发起 update:modelValue 事件

子级组件
<script setup>
import { ref } from 'vue';
// 接收参数
defineProps({
    modelValue: String
})
// 定义 emit 事件
const emit = defineEmits(['update:modelValue'])
</script>

<template>
    <input
    type="text" 
    :value="modelValue" 
    @input="e => emit('update:modelValue', e.target.value)" />
</template>

父级还要实现接收 update:modelValue 并修改对应数据,只不过 v-mode 相当于已经操作

效果展示

%title插图%num

defineModel 的使用

1. 在 vite.config.js 文件中开启实验性的功能 defineModel

JavaScript
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue({
      script: {
        defineModel: true
      }
    }),
  ],
  resolve: {
    alias: {
      // 允许我们以 @ 方式去访问对应目录 (Vue2 中默认底层已配置)
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

2. 父级使用 v-model 并传入数据

Vue
<script setup>
import { ref } from 'vue';
import myInput from '@/components/myInput.vue';

const text = ref('');
</script>
<template>
  <div class="content">
    <p>父级页面值:{{ text }}</p>
    <myInput v-model="text"></myInput>
  </div>
</template>

3. 使用实验性的方法 defineModel 获取到的 v-model 的值是支持直接修改的

Vue
<script setup>
import { defineModel } from 'vue';
// 通过 defineModel() 传过来的 v-model 数据支持直接修改
const modelValue = defineModel();
</script>

<template>
    <!-- 直接修改父级传过来的值 -->
    <input type="text" :value="modelValue" @input="e => modelValue = e.target.value" />
</template>

效果演示

%title插图%num
路由

vue3 也支持路由方式去实现单页面切换的效果,关于 vue2 Router 的介绍可以 参考笔记

VueRouter官方文档

创建路由

定义 路由组

在部署 vue3 项目时,勾选 router 后,会额外配置 router 的演示内容

@/router/index.js
// createRouter 创建 router 对应实例
// createWebHistory 创建 history 模式的路由
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      // 默认指向路由
      path: '/',
      component: Layout,
      children: [
        // path 为空则为默认二级路由
        {
          path: '',
          component: Home
        },
        {
          // 接收 params 参数的路由
          path: 'category/:id',
          component: Category
        }
      ]
    },
    {
      path: '/login',
      component: Login
    }
  ],
  // 扩展:每次切换路由 自动滚动到顶部
  scrollBehavior() {
    return {
      top: 0
    }
  }
})

export default router

设置挂载点

通过 router-view 路由出口组件,将当期路由对应的内容挂载在目标位置,
若有二级或多级的路由,同样也要在对应的父级提供挂载点的位置;以便对应的内容提供

Vue HTML
<template>
  <!-- 路由出口组件 -->
  <router-view></router-view>
</template>
跳转路由

跳转指定路径

在 setup 的环境中,需要引用 vue-router 提供的 useRouter 去实现路由方法

JavaScript
import { useRouter } from 'vue-router';
// 路由对象方法 
const router = useRouter()

// 直接替换当前路由 不会添加新的历史记录
router.replace({ path: '/' })
// 使用路由器实例进行路由导航
router.push('/about');
路由传参

接收参数

在 setup 的环境中,需要引用 vue-router 提供的 useRoute 方法去获取路由传参
(注意与上面 useRouter 的区别)

路由传递参数
<template>
  <RouterLink to="/Category/22"></RouterLink>
  <RouterLink to="/Category?id=23"></RouterLink>
</template>
路由接收参数
import { useRoute } from 'vue-router';
const route = useRoute()
// ...
// 获取 params 参数并传值
getCategory(route.params.id)  // 22
// 获取 query 参数并传值
getCategory(route.query.id) // 23
切换回调

同一个路由当参数更新时,不会触发界面的更新;
这意味着即使路由参数发生变化,组件实例仍然是相同的,因此不会触发组件的更新

onBeforeRouteUpdate

vue3 的 router 提供了 onBeforeRouteUpdate 函数用于路由切换时触发的回调。

Vue
<script setup>
// ...
import { onMounted, ref } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
const goods = ref({})
// 获取 route 参数
const route = useRoute()
const getGoods = async (id = route.params.id) => {
  const res = await getDetail(id)
  goods.value = res.result
}
onMounted(() => getGoods())

// 路由切换更新回调
onBeforeRouteUpdate(async (to) => {
  // 重新获取数据 渲染界面
  await getGoods(to.params.id)
})
</script>
pinia
说明

Pinia 是 Vue 的最新的状态管理工具,是 Vuex 的替代品

%title插图%num

效果:

  1. 提供了更简单的API (去掉了 mutation)
  2. 提供符合组合式风格的 API (和 Vue3 新语法统一)
  3. 去掉了 Modules 的概念,每个 store 都是一个独立的模块
  4. 配合 TypeScript 更加友好,提供可靠的类型推断
手动安装

在实际开发项目的时候,关于 Pinia 的配置,可以在项目创建时候自动添加。但是为了让我们了解,建议先了解手动添加的操作;

官方提供了 Pinia 的安装文档:pinia 文档

创建项目目录

创建项目

PowerShell
npm create vue@latest

安装依赖

PowerShell
npm i

# 使用 yarn 下载 pinia
yarn add pinia
# 或者使用 npm
npm install pinia

配置 Pinia 到 Vue3 项目

mian.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

// 创建 pinia 实例
const pinia = createPinia()
const app = createApp(App)

// 安装 pinia插件 到 vue 实例
app.use(pinia)
app.mount('#app')
基本语法

Pinia 可以创建多个仓库,并为多个仓库创建唯一标识。在 Pinia 当中多个仓库互相之间是独立的。当然最终也会挂载在同一个状态树上。

JavaScript
import { defineStore } from 'pinia'

// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,
// 同时以 `use` 开头且以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)

// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {
  // 其他配置...
})

组合式风格

与 Vue 的选项式 API 类似,我们也可以传入一个带有 stateactions 与 getters 属性的 Option 对象

JavaScript
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

你可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。
为方便上手使用,Option Store 应尽可能直观简单

选项式风格

 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,
并且返回一个带有我们想暴露出去的属性和方法的对象。

JavaScript
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数

操作数据

使用 pinia 创建数据,并提供给 Vue 页面。

创建数据仓库

store/counter.js
import { defineStore } from "pinia";
import { ref } from "vue";

// 定义 store

export const userCounterStore = defineStore('counter', () => {

    // 声明数据 state - count
    const count = ref(0);
    // 声明数据 state - msg
    const msg = ref('hello pinia')

    // 声明操作数据的方法 action (普通函数)
    const upCount = () => count.value++
    const subCount = () => count.value--

    // 声明基于派生的计算属性 getters

    return {
        count,
        msg,
        upCount,
        subCount
    }
})

页面引用仓库

页面
<script setup>
// ...
import { userCounterStore } from '@/store/count'

// 不要尝试对 pinia 数据进行解构,会丢失响应式效果
const counterStore = userCounterStore();
</script>

<template>
  <div class="content">
    <h1>APP根组件</h1>
    <h4>{{ counterStore.msg }}</h4>
    <p>{{ counterStore.count }}</p>
    <!-- ... -->
  </div>
</template>

其他子组件同理,引用 pinia 提供的数据是同步共享的

组件
<script setup>
import { userCounterStore } from '@/store/count';
const counterStore = userCounterStore();
</script>

<template>
    <div class="comContent">
        <h3>Son1Com组件</h3>
        <h4>{{ counterStore.msg }}</h4>
        <p>{{ counterStore.count }}</p>
        <!-- 通过 pinia 提供的方法去修改 store数据 -->
        <button @click="counterStore.upCount">+1</button>
        <button @click="counterStore.subCount">-1</button>
    </div>
</template>

效果演示

%title插图%num

使用计算属性

声明基于数据派生的计算属性 getters 可以使用 compted() 函数来创建

JavaScript
import { defineStore } from "pinia";
import { computed, ref } from "vue";

// 定义 store

export const userCounterStore = defineStore('counter', () => {

    // 声明数据 state - count
    const count = ref(0);
    // 声明数据 state - msg
    const msg = ref('hello pinia')

    // 声明操作数据的方法 action (普通函数)
    const upCount = () => count.value++
    const subCount = () => count.value--

    // 声明基于派生的计算属性 getters
    const double = computed(() => count.value * 2)

    return {
        count,
        msg,
        double,
        upCount,
        subCount
    }
})

组件引用计算属性时,相当于引用了计算属性 computed 函数 return 的值

Vue
<script setup>
import { userCounterStore } from '@/store/count';
const counterStore = userCounterStore();
</script>

<template>
    <div class="comContent">
        <h3>Son2Com组件</h3>
        <h4>{{ counterStore.msg }}</h4>
        <p>{{ counterStore.double }}</p>
        <button @click="counterStore.upCount">+1</button>
        <button @click="counterStore.subCount">-1</button>
    </div>
</template>

效果演示

%title插图%num
异步操作

下载 axios 用于发起网络请求,关于 axios 的教程可以在这里查阅

PowerShell
npm i axios
# 或使用 yarn 下载
yarn add axios

在 pinia 创建的仓库中定义对应的异步请求数据的方法和接收的变量

JavaScript
import axios from 'axios';
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useChannelStore = defineStore('channel', () => {
    // 声明数据
    const channelList = ref([]);

    // 声明操作数据的方法
    const getList = async () => {
        const url = 'http://geek.itheima.net/v1_0/channels'
        // 异步获取到数据 赋值给对应本地 state
        const { data: { data } } = await axios.get(url);
        channelList.value = data.channels
    }

    return {
        channelList,
        getList
    }
})

页面调用 pinia 的数据和提供操作数据的方法实现页面的数据铺设

Vue
<script setup>
import { useChannelStore } from '@/store/channel'

const channelStore = useChannelStore();
</script>

<template>
  <div class="content">
    <h1>APP根组件</h1>
    <button 
    @click="channelStore.getList"
    >获取频道数据</button>
    <ul>
      <li 
      v-for="(item, index) in channelStore.channelList" 
      :key="index"
      >
        {{ item.id }} -- {{ item.name }}
      </li>
    </ul>
  </div>
</template>

效果演示

%title插图%num
storeToRefs

当我们直接解构 pinia 提供的数据时候,会丧失数据的响应式效果。所以我们不应该直接去解构数据;

使用 pinia 提供的 storeToRefs 方法去包裹 pinia 返回的仓库。就可以去解构数据仍保留响应式效果;

相关文档

使用 Store

虽然我们前面定义了一个 store,但在我们使用 <script setup> 调用 useStore()(或者使用 setup() 函数,
像所有的组件那样) 之前,store 实例是不会被创建的:

Vue
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>

你可以定义任意多的 store,但为了让使用 pinia 的益处最大化
(比如允许构建工具自动进行代码分割以及 TypeScript 推断),你应该在不同的文件中去定义 store

如果你还不会使用 setup 组件,你也可以通过映射辅助函数来使用 Pinia

一旦 store 被实例化,你可以直接访问在 store 的 stategetters 和 actions 中定义的任何属性。
我们将在后续章节继续了解这些细节,目前自动补全将帮助你使用相关属性。

请注意,store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value
就像 setup 中的 props 一样,如果你写了,我们也不能解构它

Vue
<script setup>
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {
  store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>

为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()。它将为每一个响应式属性创建引用。
当你只使用 store 的状态而不调用任何 action 时,它会非常有用。
请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:

Vue
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>

原理

使用 storeToRefs 方法,是为了保持响应式的效果。具体效果如下:

Vue
<script setup>
import { useChannelStore } from '@/store/channel'
import { storeToRefs } from 'pinia';

const store = useChannelStore();
const { getList } = store;
const { channelList } = storeToRefs(store);
</script>

<template>
  <div class="content">
    <h1>APP根组件</h1>
    <button @click="getList">获取频道数据</button>
    <ul>
      <li v-for="(item, index) in channelList" :key="index">
        {{ item.id }} -- {{ item.name }}
      </li>
    </ul>
  </div>
</template>

效果演示

%title插图%num
持久化

使用 pinia 持久化插件来实现 Pinia 数据的持久化存储;更多内容可查看 官方文档

%title插图%num

安装

使用 npm 安装插件

PowerShell
npm i pinia-plugin-persistedstate

在 main.js 上全局挂载使用

JavaScript
import persist from 'pinia-plugin-persistedstate'

// ...
app.use(crearePinia().use(persist))

store 仓库,使用 persist:true 开启后,当前 store 将会使用默认持久化配置

组合式
import { defineStore } from 'pinia'

export const useStore = defineStore(
  'main',
  () => {
    const someState = ref('你好 pinia')
    return { someState }
  },
  {
    persist: true,
  },
)
选项式
import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  state: () => {
    return {
      someState: '你好 pinia',
    }
  },
  persist: true,
})

使用案例

对使用 pinia 制作的 加法器 进行 持久化存储;只需要在 defineStore 第三个参数传入配置对象

JavaScript
import { defineStore } from "pinia";
import { computed, ref } from "vue";

// 定义 store

export const userCounterStore = defineStore('counter', () => {

    // 声明数据 state - count
    const count = ref(0);
    // 声明数据 state - msg
    const msg = ref('hello pinia')

    // 声明操作数据的方法 action (普通函数)
    const upCount = () => count.value++
    const subCount = () => count.value--

    // 声明基于派生的计算属性 getters
    const double = computed(() => count.value * 2)

    return {
        count,
        msg,
        double,
        upCount,
        subCount
    }
},
    { persist: true } // 开启当前模块的持久化
)

效果演示

%title插图%num

刷新界面,仍然保持数据的存活

原理 和 自定义

更多内容可以阅读 官方文档

pinia 是使用 localStorage 进行存储,并以 store.$id 作为 storage 默认的 key。
会自行对需要存储的数据进行 JSON.stringify/JSON.parse 进行序列化/反序列化;

如若需要更多配置,可以给 persist 传入一个对象。进行自定义配置。

JavaScript
import { defineStore } from "pinia";
import { computed, ref } from "vue";

// 定义 store

export const userCounterStore = defineStore('counter', () => {
    // 声明数据 state - count
    const count = ref(0);
    // 声明数据 state - msg
    const msg = ref('hello pinia')
    // 声明对象
    const obj = ref({
     a: 1
    })
 // ...
},
    {
     persist: {
      key:'smm-counter', // 自定义传入的本地存储的 key
      paths: ['count','obj.a'] // 指定哪些数据需要持久化
    }
 } // 开启当前模块的持久化
)
Element plus

Element plus 是专门适用于 vue3 系列的样式框架 >>>官方文档

%title插图%num
配置安装

安装

推荐使用自动按需导入

PowerShell
# NPM
$ npm install element-plus --save

# Yarn
$ yarn add element-plus

# pnpm
$ pnpm install element-plus

安装 unplugin-vue-components 和 unplugin-auto-import 插件

PowerShell
npm install -D unplugin-vue-components unplugin-auto-import

配置 unplugin-vue-components 和 unplugin-auto-import 插件

vite.config.js
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

尝试使用官方文档的 el-button 按钮组件 进行测试

Vue
<template>
  <el-button>Default</el-button>
  <el-button type="primary">Primary</el-button>
  <el-button type="success">Success</el-button>
  <el-button type="info">Info</el-button>
  <el-button type="warning">Warning</el-button>
  <el-button type="danger">Danger</el-button>
</template>

效果演示

%title插图%num
主题定制

通过安装 scss 修改覆盖 elementplus 默认的样式,并通知 Element 采用 scss 样式语言

官方文档

安装

基于vite的项目默认不支持css预处理器,需要开发者单独安装

PowerShell
npm i sass -D

创建一个替换 elementplus 样式的 scss 样式表

src/styles/element/index.scss
/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      // 主色
      'base': #27ba9b,
    ),
    'success': (
      // 成功色
      'base': #1dc779,
    ),
    'warning': (
      // 警告色
      'base': #ffb302,
    ),
    'danger': (
      // 危险色
      'base': #e26237,
    ),
    'error': (
      // 错误色
      'base': #cf4444,
    ),
  )
)

通知 elementplus 使用 scss 方式,并导入到 elementplus 配置

vite.config.js
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver({
        importStyle: 'sass' // 配置 elementPlus 采用 sass 样式
      })]
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css: {
    preprocessorOptions: {
      scss: {
        // 自动导入定制化样式文件进行样式覆盖
        additionalData: `
      @use "@/styles/element/index.scss" as *;
      ` // 上方指定的目录文件
      }
    }
  }
})

效果展示

%title插图%num
根据 element/index.scss 样式修改
VueUse
%title插图%num

Vueuse 是一个功能强大的 Vue.js 生态系统工具库,它提供了一系列的可重用的 Vue 组件和函数,帮助开发者更轻松地构建复杂的应用程序。例如 获取DOM滚动高度,切换暗黑模式、获取鼠标位置等

官方地址:https://vueuse.org/
相关文档:https://blog.csdn.net/m0_69824302/article/details/136353352

定义懒加载

依赖 Vueuse 监听元素是否到达视野区域,当到达该区域时,给 src 属性赋值

创建模块

@/directives/index.js
// 定义懒加载插件
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,
          (entries) => {
            // 如果进度到视野区域 回调函数的下方参数返回 true
            if (entries[0].isIntersecting) {
              // 修改图片的 src 的值 实现延迟加载图片
              el.src = binding.value
              // 为避免重复触发 stop() 停止监听元素的交叉情况
              stop()
            }
          }
        )
      }
    })
  }
}

在main.js 中全局注册该模块

mian.js
import {lazyPlugin} from '@/directives'
// ...
app.use(lazyPlugin)

使用 v-img-lazy 使用自定义指令

JavaScript
<img v-img-lazy="xxx.jpg" alt="">

效果展示

%title插图%num
获取鼠标相对位置

依赖 VueUse 的 useMouseInElement(dom) 方法,获取鼠标的在元素中的具体相对位置

相关文档

useMouseInElement 传入 dom 目标时,会提供 elementX, elementY, isOutside 这三个响应式数据

  • elementX 相对于元素的X轴位置
  • elementY 相对于元素Y轴的位置
  • isOutside 鼠标光标是否不在元素中

案例演示

导入 vueuse 方法 并绑定对应用于的放大镜样式的元素

Vue
<script setup>
import { ref, watch } from 'vue'
import { useMouseInElement } from '@vueuse/core'

// 获取鼠标相对位置
const target = ref(null)
const { elementX, elementY, isOutside } = useMouseInElement(target)
</script>

实现业务逻辑

JavaScript
// ...
// 控制滑块跟随鼠标移动(监听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

})

绑定元素的背景图定位属性的参数

Vue
<template>
      <!-- 左侧大图-->
 <div class="middle" ref="target">
       <img :src="prop.imageList[activeIndex]" alt="" />
            <!-- 蒙层小滑块 -->
      <div class="layer" 
      v-show="!isOutside" 
      :style="{ left: `${left}px`, top: `${top}px` }"
      ></div>
    </div>
    <div class="goods-image">
        <!-- ... -->
        <!-- 放大镜大图 -->
        <div class="large" :style="[
            {
                backgroundImage: `url(xxx.jpg)`,
                backgroundPositionX: `${positionX}px`,
                backgroundPositionY: `${positionY}px`,
            },
        ]" v-show="!isOutside"></div>
    </div>
</template>

效果展示

%title插图%num