Vue3快速笔记

vue3快速笔记

  • 参考地址:https://blog.csdn.net/sq91fra/article/details/135963246

  • vue3中,一个vue组件必不可少的标签只有<template>,其余两个都可以省略

  • 基础示例: 插值语法演示

// App.vue

<script setup>
......
const message = "Hello World.vue"
</script>

<template>

  <h1>这是App根组件</h1>
  <hr />
  <p>生成一个随机数字:{{(Math.random()*10).toFixed(2)}}</p>
  <div>{{message}}</div>

</template>

<style scoped>
......
</style>

  • 响应式数据演示:
    • 注意:以下示例中,随机数也会一起更新
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'

import {ref} from 'vue'

const count = ref(0)
function increment () {
  count.value++
  console.log(count)
}
</script>

<template>

  <h1>这是App根组件</h1>
  <hr />
  <p>生成一个随机数字:{{(Math.random()*10).toFixed(2)}}</p>
  <button @click="increment">Count值为: {{count}}</button>
</template>

<style scoped>
......
</style>

这是因为 Vue 的响应式更新机制 导致的。

原因分析

  1. 响应式依赖追踪
    当你在模板中使用 {{ count }} 时,Vue 会建立依赖关系:模板依赖于 count 这个响应式变量。

  2. 组件重新渲染
    当你点击按钮调用 increment() 时:

    • count.value++ 修改了响应式数据
    • Vue 检测到响应式数据变化
    • 触发整个组件的重新渲染(Template会重新更新,故表达式会被重新计算)
  3. 模板重新执行
    在重新渲染过程中,模板中的所有表达式都会重新计算,包括:

    <p>生成一个随机数字:{{(Math.random()*10).toFixed(2)}}</p>
    

    所以每次点击按钮,随机数都会重新生成。

解决办法,使用computed缓存

<script setup>
......

import {ref,computed} from 'vue'

const count = ref(0)
const randomNumber = computed(()=>(Math.random()*10).toFixed(2))

function increment () {
  count.value++
  console.log(count)
}


</script>

<template>



  <h1>这是App根组件</h1>
  <hr />
  <p>生成一个随机数字:{{randomNumber}}</p>

  <div>
    <button @click="increment">Count值为: {{count}}</button>
  </div>




</template>

<style scoped>
......
</style>

两种API

  • Vue3 引入了许多新特性和优化,其中包括对 Composition API (组合式 API)的添加,它作为新的核心特性与原有的 Options API(选项式 API ) 一起工作
- 选项式 API 是 Vue.js 传统的组件写法,适合简单的组件和初学者。选项式 API 的优点是结构清晰,易于理解和上手。但是,当组件变得复杂时,相关的逻辑会分散在不同的选项中,使得代码组织和重用变得困难。

- 组合式 API 是 Vue 3 引入的新特性,提供了更灵活的代码组织和重用方式,可以更容易地将相关的逻辑组合在一起,并且可以跨组件重用这些逻辑。这对于编写大型和复杂的组件尤其有用。

- 开发者可以根据自己的需求选择使用 Options API 或 Composition API,两者可以共存于同一个组件中。这使得开发者可以渐进式地升级现有项目到 Vue 3,同时享受 Composition API 带来的灵活性。

setup函数介绍(和vue2中的data写法类似)

  • 使用了Vue 3的Composition API,但是以选项式API的写法在``标签中使用了setup函数举例
<script>

import {reactive} from "vue";

export default {
  setup(){ // Composition API的入口,在组件创建前执行
    let msg = "Hello"; // 普通变量 - 非响应式
    let stu = reactive({ // 声明响应式对象
      name:'cxk',
      age:18,
      gender:'女',
      hobby:{
        hobby1:'唱',
        hobby2:'跳',
        hobby3:'Rap',
        hobby4:'篮球',
      }
    });
    return { // 最后返回模板需要的数据
      msg,
      stu
    }
  }
}


</script>

<template>

  <h1>{{msg}}</h1>
  <hr>
  <h2>大家好,我是{{stu.name}},喜欢{{stu.hobby.hobby1}},{{stu.hobby.hobby2}},{{stu.hobby.hobby3}},{{stu.hobby.hobby4}}</h2>



</template>

<style scoped>
......
</style>

  • setup函数这种写法,和vue2中的data写法很类似
// Vue 2 写法
export default {
  data() {
   ......
    return {
      msg: "Hello",
      stu: {
        name: 'cxk',
        // ...
      }
    }
  }
}

// Vue 3 写法
export default {
  setup() {
    let msg = "Hello";
    let stu = reactive({
      name: 'cxk',
      // ...
    });
    return { msg, stu };
  }
}

响应式代理

  • reactive(): *定义一个*响应式对象(基本类型不要用它,要用ref,否则报错)
    • 注意: reactive定义的响应式数据是“深层次”的
// Demo1.vue

<script setup>

import {reactive} from "vue";

let car = reactive({
  brand:'Benz',
  price:1000
})

let games = reactive([
  {id:1,name:'QQSpeed'},
  {id:2,name:'Asktao'},
  {id:3,name:'FenGod'},
])

let obj = reactive({
  a:{
    b:{
      c:{
        d:666
      }
    }
  }
})

function changeCarPrice(){
  car.price += 10
}

function changeFirstGame(){
  games[0].name = 'QQGarden'
}

function test(){
  obj.a.b.c.d = 999
}

</script>

<template>
  <div>
    <h2>汽车信息:一台{{car.brand}}汽车,价值{{car.price}}万</h2>
    <h2>Game列表</h2>
    <ul>
      <li v-for="g in games" :key="g.id">{{g.name}}</li>
    </ul>
    <h2>测试:{{obj.a.b.c.d}}</h2>
    <button @click="changeCarPrice">修改汽车价格</button>
    <button @click="changeFirstGame">修改第一个Game</button>
    <button @click="test">测试</button>
  </div>
</template>

<style scoped>

</style>

初始值

修改后

  • ref(): 定义响应式变量,接收的数据可以是基本类型对象类型
    • 语法: let xxx = ref(初始值)
    • 返回值: 一个RefImpl的实例对象,简称ref对象refref对象的value属性是响应式的
      • 对于let name = ref(‘张三’)来说,name不是响应式的,name.value是响应式的//
    • 注意: JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可
// Demo2.vue

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

let name = ref('张三');
let age = ref(18);
let tel = '123454645644' // 普通变量(非响应式)

function changeName() {
  name.value = '李四';
  console.log(name.value)
  // 如下代码不会引起页面的更新(name.value才会)
  // name = ref('zhang-san')
}

function changeAge() {
  age.value += 1
  console.log(age.value)
}

function showTel(){
  alert(tel)
}

</script>

<template>
  <div>
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年龄+1</button>
    <button @click="showTel">点我查看联系方式</button>
  </div>
</template>

<style scoped>

</style>

小结:

  1. 若需要一个基本类型的响应式数据,必须使用ref
  2. 若需要一个响应式对象,层级不深,refreactive都可以。
  3. 若需要一个响应式对象,且层级较深,推荐使用reactive

toRef 与 toRefs

  • 作用: 将一个响应式对象中的每一个属性,转换为ref对象
    • 即把对象中的属性单独拆分出来以后,该属性依然保持响应式的能力
    • toRef 与 toRefs即单个批量的区别,其他一样!
<script setup>

  import {ref,reactive,toRefs,toRef} from "vue";
  // 数据
  let person = reactive({name:'张三', age:18, gender:'男'})
  // 通过toRef将person对象中的age属性取出,且依然保持响应式的能力
  let age = toRef(person,'age')
  // 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
  let {name,gender} = toRefs(person)

  function changeName() {
    name.value += '~'
  }

  function changeAge() {
    age.value += 1
  }

  function changeGender() {
    gender.value += '女'
  }


</script>

<template>
  <div>
    <h2>姓名:{{person.name}}</h2>
    <h2>年龄:{{person.age}}</h2>
    <h2>性别:{{person.gender}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changeGender">修改性别</button>
  </div>

</template>

<style scoped>

</style>

初始值

修改后

计算属性(与vue2一样)

  • 简单示例如下
<script setup>
  import {ref,computed} from "vue";

  let firstName = ref('zhang')
  let lastName = ref('san')

  // 只读不修改
  // let fullName = computed(()=>{
  //   return firstName.value + '-' + lastName.value
  // })

  let fullName = computed({
    get(){
      console.log('触发get操作')
      return firstName.value + '-' + lastName.value
    },
    set(val){
      console.log('有人修改了fullName',val)
      // 以下写法会触发无限递归
      // fullName.value = val.split('-')[0]
      // lastName.value = val.split('-')[1]
      const [first,last]=val.split('-')
      firstName.value = first
      lastName.value = last

    }
  })

  function changeFullName() {
    fullName.value = 'li-si'
  }

</script>

<template>
  <div>
    姓:<input type="text" v-model="firstName"> <br>
    名:<input type="text" v-model="lastName"> <br>
    全名:<span>{{fullName}}</span> <br>
    <button @click="changeFullName">全名改为:li-si</button>
  </div>

</template>

<style scoped>

</style>

监听器(watch,与vue2一样)

  • 特点: 只能监听四种数据
    • ref定义的数据。
    • reactive定义的数据。
    • 函数返回一个值(getter函数)。
    • 一个包含上述内容的数组。
    • 监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。
<script setup>
  import {ref,watch} from "vue";

  let sum = ref(0)
  function changeSum(){
    sum.value += 1
  }
  const stopWatch = watch(sum,(newVal,oldVal)=>{
    console.log('sum变化了',newVal,oldVal)
    if(newVal >= 10){
      stopWatch() // 通过调用 watch 返回的函数来停止监听
    }
  })
</script>

<template>
  <div>
    <h1>情况一:监视【ref】定义的【基本类型】数据</h1>
    <h2>当前求和为:{{sum}}</h2>
    <button @click="changeSum">点我Sum+1</button>
  </div>
</template>


  • 若想监视对象内部的数据,需要加上配置项{deep:true}
<script setup>
  import {ref,watch} from "vue";

  let person = ref({
    name:'张三',
    age:18
  })

  function changeName(){
    person.value.name += '~'
  }
  function changeAge(){
    person.value.age += 1
  }
  function changePerson(){
    person.value = {name:'李四',age:20}
  }

  watch(person,(newVal,oldVal)=>{
    console.log('person变化了',newVal,oldVal)
  },{deep:true}) // 这里若不配置deep,则只有执行changePerson时,才会触发watch

</script>

<template>
  <div class="person">
    <h1>情况二:监视【ref】定义的【对象类型】数据</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changePerson">修改整个人</button>
  </div>
</template>


  • 监视reactive定义的【对象类型】数据,默认开启了深度监视
<script setup>
  import {reactive,watch} from "vue";

  let person = reactive({
    name:'张三',
    age:18
  })

  let obj = reactive({
    a:{
      b:{
        c:666
      }
    }
  })

  // 方法
  function changeName(){
    person.name += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changePerson(){
    Object.assign(person,{name:'李四',age:20})
  }
  function test(){
    obj.a.b.c = 888
  }
  // 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的
  watch(person,(newValue,oldValue)=>{
    console.log('person变化了',newValue,oldValue)
  })
  watch(obj,(newValue,oldValue)=>{
    console.log('Obj变化了',newValue,oldValue)
  })



</script>

<template>
  <div>
    <h1>情况三:监视【reactive】定义的【对象类型】数据</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changePerson">修改整个人</button>
    <hr>
    <h2>测试:{{obj.a.b.c}}</h2>
    <button @click="test">修改obj.a.b.c</button>
  </div>
</template>

<style scoped>

</style>
  • 监视refreactive定义的【对象类型】数据中的某个属性,注意点如下:
    • 若该属性值不是【对象类型】,需要写成函数形式。
    • 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。
    • 结论:监视的要是对象里的属性,那么最好写函数式
      • 注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视
<script setup>
  import {reactive,watch} from 'vue'
  // 数据
  let person = reactive({
    name:'张三',
    age:18,
    car:{
      c1:'奔驰',
      c2:'宝马'
    }
  })

  // 方法
  function changeName(){
    person.name += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changeC1(){
    person.car.c1 = '奥迪'
  }
  function changeC2(){
    person.car.c2 = '大众'
  }
  function changeCar(){
    person.car = {c1:'雅迪',c2:'爱玛'}
  }

  // 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
  watch(()=>person.name,(newVal,oldVal)=>{
    console.log('person.name的值变化了',newVal,oldVal)
  })

  // 对象式写法无法触发监听
  // watch(person.car,(newVal,oldVal)=>{
  //   console.log('person.car变化了',newVal,oldVal)
  // },{deep:true})

  // 函数式写法可以触发监听
  watch(()=>person.car,(newVal,oldVal)=>{
    console.log('person.car变化了',newVal,oldVal)
  },{deep:true})




</script>

<template>
  <div class="person">
    <h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changeC1">修改第一台车</button>
    <button @click="changeC2">修改第二台车</button>
    <button @click="changeCar">修改整个车</button>
  </div>

</template>

<style scoped>

</style>
  • watchEffect: 不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
    • watch要明确指出监视的数据
<script setup>
  import {ref,watch,watchEffect} from 'vue'
  // 数据
  let temp = ref(0)
  let height = ref(0)

  // 方法
  function changePrice(){
    temp.value += 10
  }
  function changeSum(){
    height.value += 1
  }

  watch([temp,height],(value)=>{
    const [newTemp,newHeight]=value
    if(newTemp >= 50 || newHeight >= 20){
      console.log('逻辑来自于watch,请联系服务器')
    }
  })

  const stopWatch = watchEffect(()=>{
    if(temp.value >= 50 || height.value >= 20){
      // console.log(document.getElementById('demo')?.innerText)
      console.log('逻辑来自于watchEffect,请联系服务器')
    }
    if(temp.value === 100 || height.value === 50){
      console.log('逻辑来自于watchEffect,清理了')
      stopWatch()
    }
  })

</script>

<template>
  <div>
    <h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1>
    <h2 id="demo">水温:{{temp}}</h2>
    <h2>水位:{{height}}</h2>
    <button @click="changePrice">水温+1</button>
    <button @click="changeSum">水位+10</button>
  </div>

</template>

<style scoped>

</style>

- 效果: 当触发其中一个条件时,会同时触发watch和watchEffect

样式

  • 如果想在style scoped中设置全局样式:
    • 可以使用:deep(过滤器)设置,例如:#app :deep(h2)就是设置app下的所有h2
    • 你还可以使用:global(过滤器)设置全局样式
    • 你也可以在根组件中既使用<style>又使用<style scoped>
style module:允许你将 CSS 类名模块化,以避免全局样式的冲突
  • 当你在一个 style标签上使用 module 属性时,所有的 CSS 类名都只在当前组件内部有效,并且每个类名都会被编译成一个唯一的字符串,以避免在不同的组件或页面中重复
    • 在模板中,你需要使用 $style 对象来引用模块化的类名($style 对象是由 Vue 自动创建)
<template>
  <div :class="$style.red">This text is red.</div>
</template>
<style module>
.red {
  color: red;
}
</style>

  • 如果你想要给模块化的类名添加前缀,以避免潜在的冲突,你可以在 module 属性中指定一个前缀:
<style module="myPrefix">
.red {
  color: red;
}
</style>

模板语法

  • 声明全局变量(各个组件都可以访问)
// main.js
......
app.config.globalProperties.变量="值" // 可以在任一组件中访问
  • 为标签元素批量绑定属性示例
<script setup>
  import { ref } from 'vue'


  const url = 'https://vuejs.org/'
  const imgSrc = '/images/pretty.png'
  const attrs = ref({
    src: '/images/pretty.png',
    alt: 'pretty',
    style: 'width:400px;'
  })
</script>

<template>
  <h2>超链接: <a :href="url">Visit Vue.js</a>></h2>
  <img :src="imgSrc" alt="pretty" style="width: 200px">
  <hr />
  <!--批量绑定实例-->
<!--  <img :attrs=attrs />--> <!--错误写法-->
<!--  <img :=attrs />--> <!--编辑器会报错,但实际可以用-->
  <img :="attrs" /> <!--推荐这么写-->

</template>

<style scoped>

</style>
  • 练习:切换图片示例
<script setup>
import {ref, computed} from "vue";

// 定义两张图片路径
const images = [
  '/images/pretty.png',
  '/images/pretty1.png'
]

// 第一张图片的切换逻辑
const currentIndex1 = ref(0)
const imgSrc = computed(() => images[currentIndex1.value])

const changeImg = () => {
  // 使用模运算:确保索引在 0 和 1(或更多)之间循环
  currentIndex1.value = (currentIndex1.value + 1) % images.length
}

// 第二张图片的切换逻辑
const currentIndex2 = ref(0)
const attrs = ref({
  alt: 'pretty',
  style: 'width:400px;'
})

// 使用计算属性动态更新src
const attrsSrc = computed(() => images[currentIndex2.value])

const changeImg2 = () => {
  // 使用模运算:确保索引在 0 和 1(或更多)之间循环
  currentIndex2.value = (currentIndex2.value + 1) % images.length
}
</script>

<template>
  <button type="button" @click="changeImg">切换图片1</button>
  <img :src="imgSrc" alt="kun kun" style="width: 200px;"/>
  <br/>
  <button type="button" @click="changeImg2">切换图片2</button>
  <img :src="attrsSrc" v-bind="attrs"/>
</template>

<style scoped>

</style>

方法事件处理器

  • 引入场景: 无论点击哪个按钮,均可以触发handleClick
<script setup>
	function handleClick() {
		alert("按钮被点击...");
	}
</script>
<template>
	<button @click="handleClick">方法事件</button>
	<button @click="handleClick()">内联事件</button>
</template>

  • 区别
    • 内联事件处理器的参数由我们自己传递,你想传递什么参数都行。$event就是事件对象,我们可以自己传递
    • 方法事件处理器的参数由vue自动传递,传递的参数为DOM事件对象,该对象包含事件触发时的相关信息
      • 本质就是系统自动传参和开发者手动自定义传参的区别
<script setup>
	function handleClick(...args) {
		console.log(args);
	}
</script>
<template>
	<button @click="handleClick">方法事件</button>
	<button @click="handleClick(1,2,'Hello', $event)">内联事件</button>
</template>

- 点击"方法事件"按钮的运行结果: 返回"PointerEvent"对象,该对象包含事件触发时的相关信息
- 点击"内联事件"按钮的运行结果: 返回[1,2,'Hello',PointerEvent],该对象包含事件触发时的相关信息
  • 事件冒泡案例演示
<script setup>
function boxHandle(box) {
  console.log(box);
}
</script>
<template>
  <!-- 修改为@click.stop可以解决事件冒泡 -->
  <div class="box1" @click="boxHandle('box1')">
    box1
    <div class="box2" @click="boxHandle('box2')">
      box2
      <div class="box3" @click="boxHandle('box3')">
        box3
      </div>
    </div>
  </div>
</template>
<style scoped>
.box1 {
  width: 400px;
  height: 400px;
  background-color: red;
}
.box2 {
  width: 300px;
  height: 300px;
  background-color: yellow;
}
.box3 {
  width: 200px;
  height: 200px;
  background-color: greenyellow;
}
</style>


- 事件演示
	- 当点击"box3"的绿块时,控制台会输出 box3,box2,box1
	- 当点击"box2"的黄块时,控制台会输出 box2,box1
	
- 阻止事件冒泡,加上"stop"装饰器

<script setup>
function boxHandle(box) {
  console.log(box);
}
</script>
<template>
  <!-- 修改为@click.stop可以解决事件冒泡 -->
  <div class="box1" @click="boxHandle('box1')">
    box1
    <div class="box2" @click="boxHandle('box2')">
      box2
      <!--变更之处-->
      <div class="box3" @click.stop="boxHandle('box3')">
        box3
      </div>
    </div>
  </div>
</template>
<style scoped>
......
</style>

- 效果
	- 当点击"box3"的绿块时,控制台只输出 box3,不再向外冒泡
	- 而当点击"box2"的黄块时,控制台会输出 box2,box1,由于没有添加stop装饰器,依然向外冒泡
  • .prevent: 阻止事件默认行为(函数的逻辑会被执行,但是控件自身的默认行为会被阻止)
<script setup>
  function onSubmit(){
    console.log('onSubmit');
  }

  function handleLinkClick(){
    console.log('handleLinkClick');
  }
</script>

<template>
  <!-- 阻止表单提交刷新页面 -->
  <form @submit.prevent="onSubmit">
    <input type="text" />
    <button type="submit">提交</button>
  </form>

  <!-- 阻止链接跳转, -->
  <a href="https://baidu.com" @click.prevent="<script setup>
  function onSubmit(){
    console.log('onSubmit');
  }

  function handleLinkClick(){
    console.log('handleLinkClick');
  }
</script>

<template>
  <!-- 阻止表单提交刷新页面 ,onSubmit的逻辑会正常执行-->
  <form @submit.prevent="onSubmit">
    <input type="text" />
    <button type="submit">提交</button>
  </form>

  <!-- 阻止链接跳转,handleLinkClic的逻辑会正常执行 -->
  <a href="https://baidu.com" @click.prevent="handleLinkClick">跳转链接</a>
</template>

<style scoped>

</style>">跳转链接</a>
</template>

<style scoped>

</style>

数据绑定

  • 单向数据绑定:假设a数据单向绑定了b数据,那么a数据的改变会影响b数据,但是b数据的改变不会影响a数据
<script setup>
	import {
		ref
	} from 'vue';
	const text = ref("");
	function submitHandler() {
		console.log(text.value);
	}
</script>
<template>
	<form @submit.prevent="submitHandler">
		<div><input type="text" @input="(event) => (text = event.target.value)" /></div>
		<div><input type="text" @input="(event) => (text = event.target.value)" /></div>
		<div><button>提交</button></div>
	</form>
</template>

- 单向绑定路径: 用户输入 → 触发input事件 → 更新text变量 → 响应式系统检测到变化
- 反向绑定(这个例子没有实现): text变量变化 → 自动更新输入框的文本显示
- 两个输入框没有保持同步
	- 第一个输入框改变 → 更新 text → 但第二个输入框不会自动更新
	- 第二个输入框改变 → 更新 text → 但第一个输入框不会自动更新

  • 上述实例若想实现双向绑定,使用v-model即可
<script setup>
import {
  ref
} from 'vue';
const text = ref("");
function submitHandler() {
  console.log(text.value);
}
</script>
<template>
  <form @submit.prevent="submitHandler">
<!--    <div><input type="text" @input="(event) => (text = event.target.value)" /></div>-->
<!--    <div><input type="text" @input="(event) => (text = event.target.value)" /></div>-->
    <div><input type="text" v-model="text" /></div>
    <div><input type="text" v-model="text" /></div>
    <div><button>提交</button></div>
  </form>
</template>

- 效果: 不管修改哪个输入框的值,两个输入框的文本均保持同步
  • 修改上面的例子,变成一个手动双向绑定自动双向绑定
<script setup>
import {
  ref
} from 'vue';
const text = ref("");
function submitHandler() {
  console.log(text.value);
}
</script>
<template>
  <form @submit.prevent="submitHandler">
	<!--手动双向绑定-->
    <div><input type="text" @input="(event) => (text = event.target.value)" :value="text" /></div>
    <!--自动双向绑定-->
    <div><input type="text" v-model="text" /></div>
    <div><button>提交</button></div>
  </form>
</template>

### v-model的修饰符

  • .lazy使用change来处理数据,只有当绑定的数据失去焦点时才改变
  • .trim去除前后的空格
  • .number将数据转换为数值,如果不是数值就不会转换。
<script setup>
	import {
		ref
	} from 'vue';
	const text = ref("");
</script>
<template>
	<form>
		<!--文本内容立即改变-->
		<div>输入信息1:<input type="text" v-model="text" /><button>提交</button></div>
		<!--文本内容要等到失去鼠标焦点的时候,才发生改变-->
		<div>输入信息2:<input type="text" v-model.lazy="text" /><button>提交</button></div>
		<div>输入信息3:<input type="text" v-model.trim="text" /><button>提交</button></div>
		<div>输入信息4:<input type="text" v-model.number="text" /><button>提交</button></div>
		<div>输入信息5:<input type="text" v-model.lazy.trim="text" /><button>提交</button></div>
	</form>
</template>

Vue3组件

  • 概念: 可以理解为一个自定义的Html控件
  • 把组件注册为全局组件
import { createApp } from 'vue';
import ExampleComponent from './ExampleComponent.vue';//注意路径
const app = createApp({});
app.component('example-component', ExampleComponent);
app.mount('#app');

  • 把组件注册为局部组件(组合式API写法)
<script setup>
import ExampleComponent from './ExampleComponent.vue';//注意路径
    //其他逻辑
</script>

  • 把组件注册为局部组件(选项式API写法)
<script>
import ExampleComponent from './ExampleComponent.vue';//注意路径
export default {
  components: {
    ExampleComponent
  },
    //其他逻辑
}
</script>

父子组件之间的通信

  • props: 父组件通过 props 向子组件传递数据
    • 在子组件中,使用defineProps()接收父组件传递的值;
    • 父组件给子组件传递的值是只读的,不可修改。这个特性被称为单向数据流,这样设计是为了确保数据的安全性,方便快速找到数据错误的源头.
    • 如果父组件给子组件传递的值是一个对象,那么可以在子组件中修改这个对象的属性。但是尽量不要通过props在父组件修改子组件的数据,要使用自定义事件修改
// Demo17Child.vue

<script setup>
	// 接收父组件传过来的值
  const props = defineProps(["count",'stu'])
</script>

<template>
  <h2>count = {{props.count}}</h2>
  <button @click="props.count++">在子组件中修改值</button> <!--无法修改-->
  <hr />
  <h2>我是{{props.stu.name}},今年{{props.stu.age}}岁</h2>
  <button @click="props.stu.age++">在子组件修改值的属性</button><!-- 可以修改,但是不推荐 -->
</template>
......

// Demo17.vue
<script setup>
import {
  ref,reactive
} from 'vue';

import Demo17Child  from "@/components/Demo17Child.vue";

const count = ref(0);
const stu = reactive({
  name: "cxk",
  age: 18,
  gender: "女",
  hobby: {
    hobby1: "唱",
    hobby2: "跳",
    hobby3: "rap",
    hobby4: "篮球",
  }
})
</script>
<template>
	......

  <Demo17Child :count="count" :stu="stu"></Demo17Child>
</template>

  • 自定义事件: 子组件通过自定义事件向父组件发送消息
    • 在子组件中,使用defineEmits()声明事件
    • 在子组件的模板中使用$emit()发送事件请求,在"script"中则使用emits()发送请求
// 子组件

<script setup>
  
  ......
  const emits = defineEmits(["response"]) // 声明自定义事件
  const sendMessage = () => {
    emits('response','Hello from child!') // 发送事件并传参
  }

</script>

<template>
  ......
  <button @click="sendMessage">sendMessage</button> <!--触发事件-->
</template>

// 父组件
<script setup>
import {
  ref,reactive
} from 'vue';

import Demo17Child  from "@/components/Demo17Child.vue";
......
const handleResponse = (response) => {
  console.log('Received response from child:',response);
}


</script>
<template>
  ......
  <!--@response标识并使用handleResponse接收子组件的消息-->
  <Demo17Child :count="count" :stu="stu" @response="handleResponse"></Demo17Child>
</template>

  • 父子组件之间相互通讯完整示例
// 子组件

<script setup>
  const props = defineProps(["count",'stu'])

  const emits = defineEmits(["response"])
  const sendMessage = () => {
    emits('response','Hello from child!')
  }

</script>

<template>
  <h2>count = {{props.count}}</h2>
  <button @click="props.count++">在子组件中修改值</button> <!--无法修改-->
  <hr />
  <h2>我是{{props.stu.name}},今年{{props.stu.age}}岁</h2>
  <button @click="props.stu.age++">在子组件修改值的属性</button><!-- 可以修改,但是不推荐 -->
  <hr />
  <button @click="sendMessage">sendMessage</button>
</template>

// 父组件
<script setup>
import {
  ref,reactive
} from 'vue';

import Demo17Child  from "@/components/Demo17Child.vue";

const text = ref("");

const count = ref(0);
const stu = reactive({
  name: "cxk",
  age: 18,
  gender: "女",
  hobby: {
    hobby1: "唱",
    hobby2: "跳",
    hobby3: "rap",
    hobby4: "篮球",
  }
})

const handleResponse = (response) => {
  console.log('Received response from child:',response);
}


</script>
<template>
  <form>
    <div>输入信息1:<input type="text" v-model="text" /><button>提交</button></div>
    <div>输入信息2:<input type="text" v-model.lazy="text" /><button>提交</button></div>
    <div>输入信息3:<input type="text" v-model.trim="text" /><button>提交</button></div>
    <div>输入信息4:<input type="text" v-model.number="text" /><button>提交</button></div>
    <div>输入信息5:<input type="text" v-model.lazy.trim="text" /><button>提交</button></div>
  </form>
  <hr />

  <Demo17Child :count="count" :stu="stu" @response="handleResponse"></Demo17Child>
</template>


  • 父组件这边可以有一个变异版: 同时接收子组件参数和传递父组件参数
<script setup>
import {
  ref,reactive
} from 'vue';

......

// const handleResponse = (response) => {
//   console.log('Received response from child:',response);
// }

const handleResponse = (childMsg,parentMsg) => {
  console.log('Received response from child:',childMsg);
  console.log('Parent params:',parentMsg);
}


</script>
<template>
  ......
  <!--接收msg参数,并传递父组件自己的参数-->
  <Demo17Child :count="count" :stu="stu" @response="(msg)=>{handleResponse(msg,'ddd')}"></Demo17Child>
</template>

Vue 依赖注入(Provide/Inject

  • 允许一个祖先组件向其所有的后代组件传递数据,无论组件层次有多深(特别适合深层嵌套组件间的数据传递)

1. provide 函数

  • 在祖先组件中使用,提供数据给所有后代组件
  • 第一个参数是提供的键名(key)
  • 第二个参数是提供的值(value)
  • 可以提供响应式数据(ref)、函数、常量等

2. inject 函数

  • 在后代组件中使用,注入祖先组件提供的数据
  • 第一个参数是要注入的键名(key)
  • 第二个参数是默认值(可选),当未找到对应提供时使用

3. readonly 函数

  • 用于包装响应式数据,防止子组件直接修改
  • 保持响应式,但只能读取不能写入

实例演示:

  • 我们将创建四个组件:爷爷组件、父亲组件和儿子组件,孙子组件,演示从爷爷组件提供数据,儿子组件直接注入数据,而不需要通过父亲组件传递。
  • 爷爷组件提供数据,父亲组件不接收任何props,儿子组件通过inject获取爷爷提供的数据。
// Grandparent.vue

<script setup>
  import { ref, provide, readonly } from 'vue'
  import Parent from "@/components/Parent.vue";

  // 准备数据源(变量,对象和方法)
  const appName = ref('Vue 依赖注入演示')
  const version = ref('1.0.0')
  const updateAppName = (newName)=>{
    appName.value = newName
  }
  const userInfo = ref({
    name: '张三',
    age: 25,
    role: '管理员'
  })

  // 提供出去
  provide('appName',appName)
  provide('updateAppName',updateAppName)
  provide('userInfo',readonly(userInfo))
  provide('version','1.0.0')


</script>

<template>
  <div class="grandparent">
    <h2>爷爷组件 (提供者)</h2>
    <p>应用名称: {{ appName }}</p>
    <p>用户: {{ userInfo.name }} ({{ userInfo.age }}岁) - {{ userInfo.role }}</p>
    <p>版本: {{ version }}</p>
    <hr />
    <Parent />
  </div>
</template>

<style scoped>
.grandparent {
  padding: 20px;
  border: 3px solid #3498db;
  border-radius: 8px;
  margin: 10px;
}
</style>

// Parent.vue

<script setup>
  import {provide} from "vue";
  import Child from "@/components/Child.vue";

  provide('parentMessage','这是来自父组件的消息')
</script>

<template>
  <div class="parent">
    <h3>父组件 (中间层)</h3>
    <p>这个组件不需要接收 props,可以直接使用注入的数据</p>
    <hr />
    <Child />
  </div>
</template>

<style scoped>
.parent {
  padding: 15px;
  border: 2px solid #2ecc71;
  border-radius: 8px;
  margin: 10px;
}
</style>
// Child.vue
<script setup>
  import { inject, ref } from 'vue'
  import Grandchild from "@/components/Grandchild.vue";
	
	
  // 接收
  const appName = inject('appName')
  const updateAppName = inject('updateAppName')
  const userInfo = inject('userInfo')
  const version = inject('version')
  const parentMessage = inject('parentMessage')
  const optionalData = inject('optionalData','默认值')

  const newName = ref('')
  // 可以修改AppName
  const handleUpdateAppName = ()=>{
    if(newName.value.trim()){
      updateAppName(newName.value)
      newName.value = ''
    }
  }
</script>

<template>
  <div class="child">
    <h4>子组件 (消费者)</h4>
    <p>从爷爷组件注入的应用名称: {{ appName }}</p>
    <p>从爷爷组件注入的用户信息: {{ userInfo.name }}, {{ userInfo.role }}</p>
    <p>从爷爷组件注入的版本: {{ version }}</p>
    <p>从父组件注入的消息: {{ parentMessage }}</p>
    <p>可选数据(带默认值): {{ optionalData }}</p>

    <div class="actions">
      <input v-model="newName" placeholder="输入新应用名称" />
      <button @click="handleUpdateAppName">更新应用名称</button>
    </div>

    <hr />
    <Grandchild />
  </div>
</template>

<style scoped>
.child {
  padding: 10px;
  border: 1px solid #e74c3c;
  border-radius: 8px;
  margin: 10px;
}

.actions {
  margin-top: 10px;
}

input {
  margin-right: 10px;
  padding: 5px;
}

button {
  padding: 5px 10px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #2980b9;
}
</style>
// Grandchild.vue
<script setup>
  import { inject } from 'vue'

  const appName = inject('appName')
  const userInfo = inject('userInfo')
  const version = inject('version')
  const parentMessage = inject('parentMessage')
</script>

<template>
  <div class="grandchild">
    <h5>孙子组件 (深层消费者)</h5>
    <p>从爷爷组件注入的应用名称: {{ appName }}</p>
    <p>从爷爷组件注入的用户信息: {{ userInfo.name }}, {{ userInfo.age }}岁</p>
    <p>从爷爷组件注入的版本: {{ version }}</p>
    <p>从父组件注入的消息: {{ parentMessage }}</p>
    <p><small>即使隔了两层,也能直接访问爷爷组件的数据</small></p>
  </div>
</template>

<style scoped>
.grandchild {
  padding: 8px;
  border: 1px dashed #f39c12;
  border-radius: 8px;
  margin: 10px;
  background-color: #fef9e7;
}

h5 {
  margin: 0 0 10px 0;
  color: #e67e22;
}
</style>

AppName

执行流程

  1. 爷爷组件(Grandparent)通过 provide 提供数据
  2. 父组件(Parent)位于中间层,不需要传递 props
  3. 子组件(Child)和孙子组件(Grandchild)通过 inject 直接获取数据
  4. 数据更新时,所有注入该数据的组件都会自动响应更新

优点

  • 减少 Props 传递:避免层层传递 props(props drilling)
  • 代码更清晰:组件间关系更明确
  • 维护性更好:数据来源集中管理
  • 灵活性强:任何后代组件都可以按需注入所需数据

适用场景

  • 主题/样式配置
  • 用户认证信息
  • 全局状态(简单场景,复杂场景建议使用 Pinia)
  • 国际化/本地化设置
  • 表单上下文管理

动态组件 component 元素

  • 可以理解为一个自定义的html标签,传入想展示的控件,component就渲染成为那个控件
  • 比如,可以将一个页面的多个元素拆分为多个组件,然后展示出来
    • 其最终以什么标签呈现由is属性决定
<script setup>
  import Grandparent from "@/components/Grandparent.vue";

</script>

<template>
  <component is="h2">展示的是h2元素</component>
  <component is="h1">展示的是h1元素</component>
  <component is="div">展示的是div元素</component>
  <component is="a">展示的是a元素</component>
  <component is="Grandparent"></component>
</template>

<style scoped>

</style>

component

总结

  1. Provide/Inject 像是"局部版的 Vuex" - 适合组件树内部的共享
  2. Vuex/Pinia 是"专业的状态管理库" - 适合整个应用的状态管理
  3. 两者可以互补使用 - 全局用 Pinia,局部用 Provide/Inject
  4. 选择依据 - 根据项目规模、团队习惯和具体需求选择

建议:小型项目或组件内部通信用 Provide/Inject;中大型项目用 Pinia;两者结合使用能达到最佳效果。

插槽知识点

  • 插槽的作用: 在组件的模板中预留一个位置,并可以决定该位置的具体内容

    • 例如,先在子组件中定义插槽(即预留位置)
    • 然后在父组件中引用该子组件,然后填充子组件预留的内容
  • 默认插槽: 使用"slot"来定义一个插槽
    • 默认插槽里面可以有默认内容,如果父组件定义了该组件没有传递东西,就会显示slot中的默认内容
// 子组件
......
<template>
  <div>
    <slot>我是子组件默认的内容!</slot>
  </div>
</template>
......

// 父组件
<script setup>
  import DefaultSlotSon1Demo from "@/components/slot/defaultSlotSon1Demo.vue";
</script>

<template>
  <div>我是父组件</div>
  <DefaultSlotSon1Demo /> <!--展示子组件的默认内容-->
  <DefaultSlotSon1Demo>我更改了子组件的内容</DefaultSlotSon1Demo><!--修改了子组件的默认内容-->
</template>
......
  • 具名插槽: 如果组件中有多个"slot",可以通过添加"name"属性来区分
    • 在父组件中,可以通过"v-slot"(vue3中可以简写为"#"号)来对应插槽
// 子组件

......
<template>
  <div>
    <slot name="header"></slot>
    <slot>我是子组件默认的内容!</slot>
    <slot name="footer"></slot>
  </div>
</template>
......

// 父组件

<script setup>
  import nameSlotSon2Demo from "@/components/slot/nameSlotSon2Demo.vue";
</script>

<template>
  <div>我是父组件</div>
  <div>
    <nameSlotSon2Demo>
      <template v-slot:header> <!--v-slot写法-->
        <h1>填充子组件预留的header插槽内容</h1>
      </template>

      <template #default> <!--#写法-->
        <h1>修改子组件默认的插槽内容</h1>
      </template>

      <template #footer>
        <h2>填充子组件预留的footer插槽内容</h2>
      </template>
    </nameSlotSon2Demo>
  </div>
</template>



  • 作用域插槽: 允许子组件向父组件传递数据,使得父组件可以定义一个模板来接收子组件传递的数据
    • 注意事项: 不支持"#"号的简写
// 子组件

<script setup>
  import {ref} from "vue";

  const user = ref({
    name:'张三',
    age:18
  })
</script>

<template>
  <div>
    <slot :user="user"></slot>
  </div>
</template>

// 父组件

<script setup>

  import DomainSlotSon3Demo from "./domainSlotSon3Demo.vue"
</script>

<template>
  <div>我是父组件</div>


  <DomainSlotSon3Demo>
<!--    <template #slotProps>--> <!--这里不支持简写-->
    <template v-slot="slotProps">
      <p>姓名: {{slotProps.user.name}}</p>
      <p>年龄: {{slotProps.user.age}}</p>
    </template>
  </DomainSlotSon3Demo>
</template>

<style scoped>

</style>

作用域插槽的工作原理:子组件负责管理数据,父组件负责决定如何渲染这些数据

  1. 数据流向:子组件 → 父组件
    • 子组件通过"slot :data=data"提供数据(专注于数据逻辑)
    • 父组件通过 'v-slot="{ data }"'接收数据(自定义UI呈现)
  2. 渲染位置:数据来自子组件,但模板在父组件定义
  3. 响应式:子组件提供的数据是响应式的,父组件中的更新会影响子组件
  • 具名插槽还支持解构赋值的写法
// 子组件不变

<script setup>
  import {ref} from "vue";

  const user = ref({
    name:'张三',
    age:18
  })
</script>

<template>
  <div>
    <slot :user="user"></slot>
  </div>
</template>
......

// 父组件

<script setup>
  ......
  import DomainSlotSon3Demo from "./domainSlotSon3Demo.vue"
</script>

<template>
  <div>我是父组件</div>
  <DomainSlotSon3Demo>
    <template v-slot="{ user }"> <--解构赋值写法-->
      <p>姓名: {{ user.name }}</p>
      <p>年龄: {{ user.age }}</p>
    </template>
  </DomainSlotSon3Demo>
</template>

<style scoped>

</style>

Teleport组件(传输组件)

  • 作用: 子组件的内容"传送"到 DOM 的其他位置,解决 z-index、定位等问题
- 当需要显示模态框或弹出层时,我们可能会在Vue组件的模板中创建模态对话框,并将其作为DOM树的一部分。这意味着模态对话框将作为其父组件的一部分渲染。这样,如果页面中其他元素具有较高的z-index,可能会覆盖模态框。此外,可能会受到其他组件样式的干扰,需要额外的CSS规则来确保模态框的正确显示。

- vue3中,为我们提供了Teleport,方便设置模态框或弹出层。
  • Demo演示: 当点击登录按钮后,会弹出登录的模态框,随意位置一点,模态框即消失
<!-- App.vue -->
<script setup>
 import {
  ref
 } from 'vue';
 const showModal = ref(false);
 const username = ref('');
 const password = ref('');
 function submitForm() {
  // 处理表单提交逻辑
  console.log('用户名:', username.value);
  console.log('密码:', password.value);
  showModal.value = false; // 提交后关闭模态框
 }
</script>
<template>
 <div id="app">
  <button @click="showModal = true">登录</button>
  <Teleport to="body">
   <div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
    <div class="modal-content">
     <h2>登录</h2>
     <form @submit.prevent="submitForm">
      <div>
       <label for="username">用户名:</label>
       <input type="text" id="username" v-model="username" required>
      </div>
      <div>
       <label for="password">密码:</label>
       <input type="password" id="password" v-model="password" required>
      </div>
      <button type="submit">提交</button>
     </form>
     <button @click="showModal = false">关闭</button>
    </div>
   </div>
  </Teleport>
 </div>
</template>

<style scoped>
    /* modal-overlay用于创建一个覆盖整个屏幕的背景,并且当点击背景时,会关闭模态框。 */
 .modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
 }
    /* modal-content内部包含了登录表单和一个关闭按钮 */
 .modal-content {
  background: white;
  padding: 20px;
  border-radius: 5px;
  width: 300px;
 }
 .modal-content h2 {
  margin-top: 0;
 }
 .modal-content div {
  margin-bottom: 10px;
 }
</style>

核心功能分析

1. 响应式数据

const showModal = ref(false);    // 控制模态框显示/隐藏
const username = ref('');        // 用户名输入框数据
const password = ref('');        // 密码输入框数据

2. 表单提交函数

function submitForm() {
  console.log('用户名:', username.value);
  console.log('密码:', password.value);
  showModal.value = false; // 提交后关闭模态框
}

代码中的 Teleport

<Teleport to="body">
  <!-- 模态框内容会被渲染到 body 元素下 -->
</Teleport>

为什么要用 Teleport?

  1. 避免样式冲突:模态框不受父组件 CSS 影响
  2. 避免 z-index 问题:确保模态框在最顶层
  3. DOM 结构清晰:模态框直接位于 body 下

模态框显示/隐藏逻辑

1. 打开模态框

<button @click="showModal = true">登录</button>

2. 关闭模态框的三种方式

<!-- 1. 点击背景关闭 -->
<div class="modal-overlay" @click.self="showModal = false">
  <!-- .self 修饰符:只有点击自身才触发,避免点击内容区域也关闭 -->
</div>

<!-- 2. 提交表单后关闭 -->
<form @submit.prevent="submitForm">
  <!-- .prevent 修饰符阻止默认表单提交 -->
</form>

<!-- 3. 点击关闭按钮 -->
<button @click="showModal = false">关闭</button>

代码执行流程

  1. 初始状态showModal = false,模态框隐藏
  2. 点击登录按钮showModal = true,模态框显示
  3. 用户输入:通过 v-model 双向绑定到 usernamepassword
  4. 点击提交
    • 执行 submitForm() 函数
    • 输出用户名和密码到控制台
    • 设置 showModal = false,模态框关闭

CSS 样式详解

1. 模态框遮罩层

.modal-overlay {
  position: fixed;    /* 固定定位,覆盖整个视口 */
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);  /* 半透明黑色背景 */
  display: flex;      /* 使用 flex 居中内容 */
  align-items: center;
  justify-content: center;
}

2. 模态框内容区

.modal-content {
  background: white;   /* 白色背景 */
  padding: 20px;       /* 内边距 */
  border-radius: 5px;  /* 圆角 */
  width: 300px;        /* 固定宽度 */
}

总结

  1. Vue 3 的响应式数据:使用 ref 管理状态
  2. Teleport 组件:解决模态框的定位问题
  3. 表单处理:双向绑定和提交逻辑
  4. 事件修饰符.prevent.self 的使用
  5. 模态框交互:多种关闭方式

这是一个典型的模态框实现模式,在实际项目中非常实用。

Pinia---更强大的数据(状态)管理仓库(相比Vuex)

  • 安装: npm install pinia
  • main.js中作如下配置
import {
    createApp
} from 'vue'
import {
    createPinia
} from 'pinia' //1.引入pinia

import App from './App.vue'

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

app.use(pinia) //3.配置vue使用pinia插件
// 挂载应用
app.mount('#app')

  • 创建仓库store.index.js
import {
    defineStore
} from "pinia"


export const useCountStore = defineStore("count", {
    //存储数据(响应式)
    state: () => ({
        count: 100
    }),
    //计算属性
    getters: {
        //箭头函数,不能使用this
        double: (state) => state.count * 2,
        //普通函数
        //double(){return this.count * 2},
    },
    //操作方法
    actions: {
        //需要使用this访问state,不推荐使用箭头函数
        increment() {
            this.count++;
        }
    }
})

  • 在任意组件中使用
<script setup>
  import {useCountStore} from "@/store/index.js";

  const countStore = useCountStore();

</script>

<template>

  <div>我是父组件</div>
  <h3>count的值为: {{countStore.count}}</h3>
  <h3>count-double选项值为: {{countStore.double}}</h3>
  <button @click="countStore.$patch({count:countStore.count-1})">减法运算</button>
  <button @click="countStore.increment">加法运算</button>

</template>
......

Pinia 的核心概念

1. Store 定义

  • 使用 defineStore() 定义 store
  • 第一个参数是 store 的唯一 ID
  • 第二个参数是配置对象(state, getters, actions)

2. 三种修改状态的方式

方式1:直接修改

countStore.count = 200;

方式2:使用 $patch(批量修改)

// 对象形式
countStore.$patch({
    count: countStore.count - 1,
    name: "李四"
});

// 函数形式(推荐)
countStore.$patch((state) => {
    state.count--;
    state.name = "李四";
});

方式3:使用 actions

countStore.increment();  // 调用定义好的 action

3. 响应式处理

  • 在模板中直接使用 countStore.count 是响应式的
  • 如果需要解构,需要使用 storeToRefs() 保持响应性:
import { storeToRefs } from 'pinia'

const countStore = useCountStore();
const { count, double } = storeToRefs(countStore);  // 保持响应式

Pinia vs Vuex 对比

Pinia 的优势

  1. 更简单的 API:不需要 mutations,actions 可以直接修改 state
  2. 完美的 TypeScript 支持:完全类型安全
  3. 模块化:每个 store 都是独立的,不需要嵌套模块
  4. Composition API 友好:更符合 Vue 3 的设计理念
  5. 更轻量:体积更小,API 更简洁

Vuex 的对应概念

Vuex Pinia 说明
state state 存储数据
getters getters 计算属性
mutations - Pinia 不需要 mutations
actions actions 操作方法(可直接修改 state)
modules 独立 store 每个 store 都是一个模块

总结

Pinia 是一个现代化、简洁且强大的状态管理解决方案:

  1. 安装简单npm install pinia
  2. 配置直观:在 main.js 中注册即可
  3. API 简洁:state + getters + actions 的结构
  4. TypeScript 友好:完整的类型推断
  5. 模块化设计:每个 store 都是独立的
  6. 灵活性强:支持插件、持久化等高级特性

Pinia 已经成为 Vue 3 生态中官方推荐的状态管理库,特别适合新项目使用。对于已有的 Vuex 项目,Pinia 也提供了迁移指南和兼容性支持。

  • pinia也支持组合式的写法(推荐),现在实现一模一样的效果
import {
    defineStore
} from "pinia"

import {computed, ref} from "vue";


// export const useCountStore = defineStore("count", {
//     //数据
//     state: () => ({
//         count: 100
//     }),
//     //计算属性
//     getters: {
//         //箭头函数,不能使用this
//         double: (state) => state.count * 2,
//         //普通函数
//         //double(){return this.count * 2},
//     },
//     //方法
//     actions: {
//         //需要使用this访问state,不推荐使用箭头函数
//         increment() {
//             this.count++;
//         }
//     }
// })

export const useCountStore = defineStore("count",()=>{
    // 定义数据
    const count = ref(100)
    // 定义计算属性
    const double = computed(() => count.value*2)
    // 定义方法
    function increment() {
        count.value++
    }
    // 暴露状态
    return {count,double,increment}
})


// 应用.vue(通过生成useCountStore()实例,然后点点点数据,计算属性和方法...)

<script setup>
  ......
  import {useCountStore} from "@/store/index.js";
  const countStore = useCountStore();
</script>

<template>
  <div>我是父组件</div>
  <h3>count的值为: {{countStore.count}}</h3>
  <h3>count-double选项值为: {{countStore.double}}</h3>
  <button @click="countStore.$patch({count:countStore.count-1})">减法运算</button>
  <button @click="countStore.increment">加法运算</button>
</template>


  • 解构的写法: pinia示例如果需要解构,需要使用storeToRefs
    • 注意事项: 选项式计算属性不能结构,组合式方法不能解构
// 应用.vue

import {
        useCountStore
    } from ......
    import {
        storeToRefs
    } from "pinia";

    const countStore = useCountStore()
    // const { count, double } = countStore  // 直接解构会失去响应性
    const {
        count,
        double
    } = storeToRefs(countStore) // increment是方法,不支持解构写法


store的修改(数据修改)

  • pinia解构后,直接修改是没有效果的
    • 解构后的 ref 可以直接修改值,但不会更新 store
// 应用.vue
<div>
    <h1>解构后直接修改无效果</h1>
    <button @click="count.value++">直接修改</button> <!--按钮点击无效果-->
    <h2>现在count值为:{{count}}</h2>
</div>

  • 通过方法修改
// 应用.vue
<template>
    <div class="div">
        ......
        <button @click="countStore.increment">按钮</button><!-- 点击调用方法间接修改 -->
    </div>

</template>

  • 通过$patch补丁修改
    • pinia提供了$patch进行补丁修改,可以同时修改多个状态
    • $patch会直接替换之前的同名状态
const clickHandler = () => {
    countStore.$patch({
        count:600
    })
    // 或
    countStore.$patch((state) => {
        state.count = 300
    })
}


<template>
    <div>
    <h1>使用patch批量修改</h1>
    <button @click="clickHandler">直接修改</button>
    <h2>现在count值为:{{count}}</h2>
  </div>

</template>
  • $rest状态重置: 将修改后的状态重置回初始状态

    • 模板写法(只适用于选项式api)
     <button @click=“countStore.$reset()”>重置
    
    • 如果要使用组合式api,需要在main.js中重写$reset
    ......
    const app = createApp(App)
    pinia.use(({
        store
    }) => {
        const initialState = JSON.parse(JSON.stringify(store.$state));
        store.$reset = () => {//重写$rest
            store.$patch(initialState);
        }
    })
    app.use(pinia) //3.配置vue使用pinia插件
    ......
    
    
  • store的监听(订阅)

    • 使用$subsribe监听store是否变化,如果变化就会调用$subsribe中的方法。
      • $subsribe只会监听定义它的组件是否修改了store,不会监听其他组件有无修改
    • 语法:counterStore.$subscribe(callback, options)
      • callback: 每次 store 变化时执行的回调函数
      • options: 配置对象,detached: true 表示组件销毁后继续监听
    • 实例演示
// store.counter.js

import {defineStore} from "pinia";
import {ref} from "vue";

export const useMyCounterStore = defineStore("myCounter", () => {
    const count = ref(0)
    function increment() {
        count.value++
    }

    function decrement() {
        count.value--
    }

    function reset() {
        count.value = 0
    }

    return {count,increment,decrement,reset}

})

// CounterDemo.vue
<template>
  <div>
    <h2>Pinia Store 监听演示</h2>

    <div>
      <p>当前计数: {{ count }}</p>
      <button @click="counterStore.increment">+1</button>
      <button @click="counterStore.decrement">-1</button>
      <button @click="counterStore.reset">重置</button>
      <button @click="updateWithPatch">使用 $patch 修改</button>
    </div>

    <div class="log">
      <h3>修改日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { useMyCounterStore } from "@/store/counter.js";
import { storeToRefs } from 'pinia'
import { ref, onMounted } from 'vue'

const counterStore = useMyCounterStore()
const { count } = storeToRefs(counterStore)  // 使用 storeToRefs 解构

// 监听日志
const logs = ref([])

// 设置订阅
onMounted(() => {
  // 订阅 store 的变化
  counterStore.$subscribe((mutation, state) => {
    const timestamp = new Date().toLocaleTimeString()
    let logMessage = ''

    // 直接从 state 获取最新值,不依赖 mutation.key 和 mutation.newValue
    const currentCount = state.count

    // mutation 包含修改信息
    switch (mutation.type) {
      case 'direct':
        logMessage = `[${timestamp}] 直接修改,当前值:${currentCount}`
        break
      case 'patch object':
        logMessage = `[${timestamp}] $patch 对象形式修改:${JSON.stringify(mutation.payload)},当前值:${currentCount}`
        break
      case 'patch function':
        logMessage = `[${timestamp}] $patch 函数形式修改,当前值:${currentCount}`
        break
      default:
        logMessage = `[${timestamp}] 未知修改类型:${mutation.type},当前值:${currentCount}`
    }

    // 添加日志
    logs.value.push(logMessage)

    // 只保留最近5条日志
    if (logs.value.length > 5) {
      logs.value.shift()
    }
  }, { detached: true })
})

// 使用 $patch 修改
function updateWithPatch() {
  // 对象形式
  counterStore.$patch({
    count: counterStore.count + 100  // 注意:这里使用当前值+100
  })
}
</script>

<style scoped>
.log {
  margin-top: 20px;
  padding: 15px;
  background: #f5f5f5;
  border-radius: 5px;
}

.log-item {
  margin: 5px 0;
  padding: 5px;
  background: white;
  border-left: 3px solid #3498db;
}
</style>

mutation 对象

回调函数接收的 mutation 参数包含:

  • type: 修改类型
    • 'direct': 直接修改(如 count.value++
    • 'patch object': $patch({...})
    • 'patch function': $patch((state) => {...})
  • payload: 修改的数据(对于 patch object 类型)

state 对象

包含修改后的完整 store 状态,可以通过 state.count 访问最新值

注意事项

  • 不要在 $subscribe 回调中修改 state,避免无限循环
  • 使用 detached: true 可以让订阅在组件销毁后依然有效
  • 如果不使用 detached: true,组件销毁时订阅会自动取消

$onAction用来监听选项式api的actions或组合式api方法是否调用

countStore.$onAction(() => {
        console.log("actions执行了")
    })

......
onMounted(() => {
  counterStore.$onAction((name)=>{
    console.log("actions name:",name)
  })
  ......
  counterStore.$subscribe(......)
 
  
actions name: {args: Array(1), name: 'increment', store: Proxy(Object), after: ƒ, onError: ƒ}
actions name: {args: Array(1), name: 'decrement', store: Proxy(Object), after: ƒ, onError: ƒ}
actions name: {args: Array(1), name: 'reset', store: Proxy(Object), after: ƒ, onError: ƒ}

pinia-plugin-persistedstate 安装与使用

这是一个让 Pinia 状态持久化的插件,可以将数据保存到 localStorage 或 sessionStorage 中。

1. 安装

npm install pinia-plugin-persistedstate

2. 基本配置

main.js / main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

// 创建 Pinia 实例
const pinia = createPinia()

// 使用持久化插件
pinia.use(piniaPluginPersistedstate)

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

3. 简单示例

3.1 基本用法 - 自动持久化

// store.couter.js

import {defineStore} from "pinia";
import {ref} from "vue";

export const useMyCounterStore = defineStore("myCounter", () => {
 ......
},{
    persist: true  // 当修改页面数据以后,重新刷新页面,数据已变,说明存储成功
})

以下是AI的用法

// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    age: 0,
    token: ''
  }),
  
  actions: {
    login(userInfo) {
      this.name = userInfo.name
      this.age = userInfo.age
      this.token = 'fake-jwt-token'
    },
    
    logout() {
      this.name = ''
      this.age = 0
      this.token = ''
    }
  },
  
  // 开启持久化(最简单的方式)
  persist: true
})

3.2 指定存储字段

// stores/settings.js
import { defineStore } from 'pinia'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    theme: 'light',
    language: 'zh-CN',
    fontSize: 14,
    lastVisit: null  // 这个字段我们不希望持久化
  }),
  
  persist: {
    // 只持久化指定的字段
    paths: ['theme', 'language', 'fontSize']
  }
})

4. 完整示例

4.1 Store 定义

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    lastUpdated: null,
    history: []
  }),
  
  actions: {
    increment() {
      this.count++
      this.lastUpdated = new Date().toISOString()
      this.history.push({
        action: 'increment',
        time: this.lastUpdated,
        value: this.count
      })
    },
    
    decrement() {
      this.count--
      this.lastUpdated = new Date().toISOString()
      this.history.push({
        action: 'decrement',
        time: this.lastUpdated,
        value: this.count
      })
    },
    
    reset() {
      this.count = 0
      this.lastUpdated = new Date().toISOString()
      this.history = []
    }
  },
  
  persist: {
    key: 'my-counter',  // 自定义存储的 key
    storage: localStorage,  // 存储位置,默认 localStorage
    paths: ['count'],  // 只持久化 count 字段
    // beforeRestore: (ctx) => {  // 恢复前钩子
    //   console.log('准备恢复状态', ctx.store.$id)
    // },
    // afterRestore: (ctx) => {   // 恢复后钩子
    //   console.log('状态已恢复', ctx.store.$id)
    // }
  }
})

4.2 组合式 API Store

// stores/todo.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTodoStore = defineStore('todo', () => {
  // 状态
  const todos = ref([])
  const filter = ref('all')
  
  // 计算属性
  const filteredTodos = computed(() => {
    if (filter.value === 'active') {
      return todos.value.filter(todo => !todo.completed)
    }
    if (filter.value === 'completed') {
      return todos.value.filter(todo => todo.completed)
    }
    return todos.value
  })
  
  const remainingCount = computed(() => {
    return todos.value.filter(todo => !todo.completed).length
  })
  
  // 方法
  function addTodo(title) {
    todos.value.push({
      id: Date.now(),
      title,
      completed: false,
      createdAt: new Date().toISOString()
    })
  }
  
  function removeTodo(id) {
    const index = todos.value.findIndex(todo => todo.id === id)
    if (index !== -1) {
      todos.value.splice(index, 1)
    }
  }
  
  function toggleTodo(id) {
    const todo = todos.value.find(todo => todo.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
  
  function setFilter(newFilter) {
    filter.value = newFilter
  }
  
  function clearCompleted() {
    todos.value = todos.value.filter(todo => !todo.completed)
  }
  
  return {
    // 状态
    todos,
    filter,
    
    // 计算属性
    filteredTodos,
    remainingCount,
    
    // 方法
    addTodo,
    removeTodo,
    toggleTodo,
    setFilter,
    clearCompleted
  }
}, {
  persist: {
    paths: ['todos']  // 只持久化 todos 数组
  }
})

4.3 组件中使用

<template>
  <div>
    <h2>计数器</h2>
    <p>当前计数: {{ counter.count }}</p>
    <button @click="counter.increment">+1</button>
    <button @click="counter.decrement">-1</button>
    <button @click="counter.reset">重置</button>
    
    <hr>
    
    <h2>待办事项</h2>
    <div>
      <input 
        v-model="newTodo" 
        @keyup.enter="addNewTodo"
        placeholder="输入待办事项"
      >
      <button @click="addNewTodo">添加</button>
    </div>
    
    <div class="filters">
      <button @click="todoStore.setFilter('all')">全部</button>
      <button @click="todoStore.setFilter('active')">未完成 ({{ todoStore.remainingCount }})</button>
      <button @click="todoStore.setFilter('completed')">已完成</button>
      <button @click="todoStore.clearCompleted">清除已完成</button>
    </div>
    
    <ul>
      <li v-for="todo in todoStore.filteredTodos" :key="todo.id">
        <input 
          type="checkbox" 
          :checked="todo.completed"
          @change="todoStore.toggleTodo(todo.id)"
        >
        <span :class="{ completed: todo.completed }">{{ todo.title }}</span>
        <button @click="todoStore.removeTodo(todo.id)">删除</button>
      </li>
    </ul>
    
    <hr>
    
    <div class="debug-info">
      <h3>调试信息</h3>
      <p>localStorage 中的计数器: {{ localStorageCounter }}</p>
      <p>localStorage 中的待办事项数量: {{ localStorageTodosLength }}</p>
      <button @click="clearStorage">清除所有存储</button>
      <button @click="reloadPage">刷新页面测试持久化</button>
    </div>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
import { useTodoStore } from '@/stores/todo'
import { ref, onMounted } from 'vue'

const counter = useCounterStore()
const todoStore = useTodoStore()
const newTodo = ref('')

// 添加新待办事项
function addNewTodo() {
  if (newTodo.value.trim()) {
    todoStore.addTodo(newTodo.value.trim())
    newTodo.value = ''
  }
}

// 调试信息
const localStorageCounter = ref('')
const localStorageTodosLength = ref(0)

// 获取 localStorage 中的数据
function updateDebugInfo() {
  const counterData = localStorage.getItem('my-counter')
  const todoData = localStorage.getItem('pinia-todo')
  
  if (counterData) {
    const parsed = JSON.parse(counterData)
    localStorageCounter.value = parsed.state.count
  }
  
  if (todoData) {
    const parsed = JSON.parse(todoData)
    localStorageTodosLength.value = parsed.state.todos?.length || 0
  }
}

// 清除所有存储
function clearStorage() {
  localStorage.clear()
  sessionStorage.clear()
  updateDebugInfo()
  alert('已清除所有存储,请刷新页面')
}

// 刷新页面
function reloadPage() {
  location.reload()
}

onMounted(() => {
  updateDebugInfo()
})
</script>

<style scoped>
.completed {
  text-decoration: line-through;
  color: #888;
}

.filters {
  margin: 10px 0;
}

.filters button {
  margin-right: 5px;
}

.debug-info {
  background-color: #f5f5f5;
  padding: 15px;
  border-radius: 5px;
  margin-top: 20px;
}
</style>

5. 配置选项详解

persist: {
  // 自定义存储的 key,默认是 store 的 id
  key: 'custom-key',
  
  // 存储位置,可以是 localStorage、sessionStorage 或自定义对象
  storage: localStorage, // 或 sessionStorage
  
  // 指定要持久化的字段,默认全部
  paths: ['user.name', 'settings.theme'],
  
  // 存储前钩子函数
  beforeRestore: (context) => {
    console.log('准备恢复:', context.store.$id)
    // context.store 是 store 实例
  },
  
  // 恢复后钩子函数
  afterRestore: (context) => {
    console.log('已恢复:', context.store.$id)
    // 可以在这里进行一些初始化操作
  },
  
  // 自定义序列化/反序列化
  serializer: {
    serialize: JSON.stringify,
    deserialize: JSON.parse
  },
  
  // 调试模式
  debug: true
}

6. 手动控制持久化

import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 手动保存到持久化存储
userStore.$persist()

// 手动从持久化存储恢复
userStore.$hydrate()

// 手动清除持久化存储
userStore.$persist.clear()

7. 自定义存储适配器

// 创建自定义存储
const myStorage = {
  getItem: (key) => {
    // 从你的存储源获取数据
    return someCustomGetMethod(key)
  },
  setItem: (key, value) => {
    // 存储数据到你的存储源
    someCustomSetMethod(key, value)
  },
  removeItem: (key) => {
    // 从你的存储源删除数据
    someCustomRemoveMethod(key)
  }
}

// 在 store 中使用
persist: {
  storage: myStorage
}

8. TypeScript 支持

// stores/user.ts
import { defineStore } from 'pinia'

interface UserState {
  name: string
  age: number
  token: string
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    name: '',
    age: 0,
    token: ''
  }),
  
  persist: {
    key: 'user-store',
    storage: localStorage
  }
})

注意事项

  1. 存储限制:localStorage 通常有 5MB 限制
  2. 敏感信息:不要存储密码、密钥等敏感信息
  3. 数据结构:只能存储 JSON 可序列化的数据
  4. 性能:大数据量可能影响页面加载性能
  5. SSR:服务端渲染时要注意存储的可用性

这个插件让状态持久化变得非常简单,是 Pinia 生态中非常实用的工具之一。

路由管理Vue Router

  • 作用: 在组件页面之间切换

  • 安装:npm install vue-router@4

    • Vue 3 对应的是 Vue Router 的第 4 版本
  • 使用方式

    • 先简单创建两个组件页面
    // Home.vue
    ......
    <template>
      <h1>这是Home主页</h1>
    </template>
    ......
    
    // About.vue
    ......
    <template>
      <h1>这是About主页</h1>
    </template>
    ......
    
    • 创建路由实例
    // router.index.js
    
    import {createRouter,createWebHistory} from "vue-router";
    
    import Home from "@/components/router_test_directory/Home.vue";
    import About from "@/components/router_test_directory/About.vue";
    
    const routes = [
        {path:"/", component:Home},
        {path:"/about", component:About},
    ]
    
    const router = createRouter({
    	// 创建一个 history 模式的路由
        history: createWebHistory(),
        routes,
        // 滚动行为:控制滚动条位置
        scrollBehavior: () => ({ left: 0,top: 0 }),
    })
    
    export default router;
    
    • 在main.js中引入路由
    import './assets/main.css'
    import { createApp } from 'vue'
    import App from './App.vue'
    
    import router from "@/router/index.js";
    ......
    const app = createApp(App)
    app.use(router)
    app.mount('#app')
    
    
    • 展示页面效果: 页面在HomeAbout之间来回切换

      • router-link指明路由链接,Vue Router 会自动处理跳转,开发者无需手动调用任何 JavaScript 代码
      • router-view用来渲染组件
    // App.vue
    ......
    <div>
        <router-link to="/">去往Home的组件导航</router-link>
        <router-link to="about">去往About的组件导航</router-link>
        <router-view></router-view>
    </div>
    
    

    编程式导航(router.push和router.replace)

    • 先新建一个正确登录以后才能访问的页面
    // MyIndex.vue
    
    <script setup>
    
    </script>
    
    <template>
      <h1>这是登录无误以后才能访问的页面</h1>
    </template>
    
    <style scoped>
    
    </style>
    
    • Home.vue添加登录逻辑
    <script setup>
    import { useRouter } from 'vue-router';
    import { ref } from "vue";
    
    const router = useRouter();
    const username = ref('');
    const password = ref('');
    
    const handleSubmit = () => {
      if (username.value === 'admin' && password.value === '123456') {
        router.push({ path: '/my' }); // 用法
      } else {
        alert('账号或密码错误!');
      }
    };
    </script>
    
    
    <template>
      <h1>这是Home主页</h1>
      <form @submit.prevent="handleSubmit">
        <div>
          <label for="username">Username:</label>
          <input type="text" id="username" v-model="username" />
        </div>
    
        <div>
          <label for="password">Password:</label>
          <input type="password" id="password" v-model="password" />
        </div>
    
        <button type="submit">登录</button>
      </form>
    </template>
    
    <style scoped>
    
    </style>
    
    • 效果: 当正确登录以后,跳转到了/my页面

路由守卫

  • Vue Router 提供了全局守卫路由独享守卫组件内守卫,允许你控制路由的访问权限和导航行为。

    • 路由守卫是一种机制,用于在路由跳转之前、之后或失败时执行代码
  • 每个路由守卫接受一个回调函数作为参数,该回调函数有三个参数:tofromnext

    • to: 是一个即将进入的目标路由对象。
    • from:是一个当前导航正要离开的路由对象。
    • next: 是一个函数,必须调用它来解析这个钩子。它可以接受以下参数:
      • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是确认的。
      • next(false): 中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址。
      • next('/')next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
      • next(error): 如果传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调。
  • 全局守卫: 通常直接在路由声明文件(router/index.js)中直接声明。

    • 全局前置守卫(router.beforeEach): 是Vue Router中的一种导航守卫,它可以在每个路由跳转之前执行一些代码。
      • 它可以用来检查用户的登录状态、权限验证、加载进度条等。全局前置守卫会在路由跳转前被调用,它允许你定义一些全局的逻辑,这些逻辑需要在每次导航发生之前执行。
    //router/index.js
    router.beforeEach((to, from, next) => {
      if (to.path === '/admin') {
        if (isAuthenticated()) { //isAuthenticated是用户定义的用来检验用户是否登录的函数
          next();
        } else {
          next('/login');
        }
      } else {
        next();
      }
    });
    
    
    • 全局后置守卫(router.afterEach): 在路由匹配之后执行。可以用来记录日志、执行清理操作等。
    router.afterEach((to, from) => {
      console.log('Navigated from ' + from.path + ' to ' + to.path);
    });
    

    路由独享守卫

    • 概念: 直接在路由定义中使用的守卫,它们可以应用于特定的路由
    • 卫通常在路由声明文件(router/index.js)的路由定义中直接声明
    • 钩子:beforeEnter、 beforeLeave
    • 前置路由守卫
    const routes = [
      {
        path: '/admin',
        component: AdminComponent,
        beforeEnter: (to, from, next) => {
          if (isAuthenticated()) {
            next();
          } else {
            next('/login');
          }
        },
      },
    ];
    
    
    • 后置路由守卫
    const routes = [
      {
        path: '/admin',
        component: AdminComponent,
        afterEnter: (to, from) => {
          console.log('Admin route entered');
        },
      },
    ];
    
    

组件内守卫

  • 概念: 在组件内部定义的守卫,它们可以应用于组件的内部路由
<-- Home.vue -->
<script setup>
  beforeRouteEnter(to, from, next) {
    if (isAuthenticated()) {
      next();
    } else {
      next(false);
    }
  },
</script>

懒加载路由组件

  • 为了提高应用的性能,你可以使用动态导入来懒加载路由组件
const routes = [
  {
    path: '/',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('../views/About.vue')
  },
];

嵌套路由

  • 作用: 允许你在一个路由中嵌套其他路由,从而构建多层级的页面结构
  • 实现方式: 使用children配置项
const routes = [
  {
    path: '/parent',
    component: ParentComponent,
    children: [
      {
        path: 'child',
        component: ChildComponent
      }
    ]
  }
];

- 效果: 当访问"/parent/child"路径时,Vue Router会渲染ParentComponent和ChildComponent

路由参数

  • 实现方式一: 通过:符号来实现
routes :  [
  {
    path: '/user/:id/post/:postId', // 传id和postId两个值
    component: UserPostComponent
  }
];

  • 实现方式二: 通过params参数实现

    • 效果: 类似于post请求中的body,不会显示在地址栏
      • 注意事项: 只能配合 name参数 使用,如果提供了 pathparams 会失效
    // 使用 params 传参
    router.push({ name: 'user', params: { id: '123' } });
    
    
  • 实现方式三: 通过query实现

  • 效果: 就是地址栏中查询字符串的方式:/user?name=xxx?age=yyy

    • 注意事项: 既可以配合 path 使用,也能配合 name 使用
    // 使用 query 传参
    router.push({ path: '/user', query: { name: 'John' } });
    
    
posted @ 2025-11-25 14:33  清安宁  阅读(8)  评论(0)    收藏  举报