准备阶段 创建Vue3工程 基于vue-cli
官方文档:Home | Vue CLI (vuejs.org)
1 2 Vue CLI 现已处于维护模式! 现在官方推荐使用 create-vue 来创建基于 Vite 的新项目。另外请参考 Vue 3 工具链指南 以了解最新的工具推荐。
基于vite
(推荐) vite
是新一代前端构建工具,官网地址:https://vitejs.cn
vite
的优势如下:
轻量快速的热重载(HMR),能实现极速的服务启动
对TypeScript
、JSX
、CSS
等支持开箱即用
真正的按需编译,不再等待整个应用编译完成
具体操作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 npm create vue@latest Need to install the following packages: create-vue@3.11.0 Ok to proceed? (y) y > npx > create-vue Vue.js - The Progressive JavaScript Framework √ 请输入项目名称: ... demo1 √ 是否使用 TypeScript 语法? ... 否 / 是 √ 是否启用 JSX 支持? ... 否 / 是 √ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是 √ 是否引入 Pinia 用于状态管理? ... 否 / 是 √ 是否引入 Vitest 用于单元测试? ... 否 / 是 √ 是否要引入一款端到端(End to End)测试工具? » 不需要 √ 是否引入 ESLint 用于代码质量检测? ... 否 / 是 √ 是否引入 Prettier 用于代码格式化? ... 否 / 是 √ 是否引入 Vue DevTools 7 扩展用于调试? (试验阶段) ... 否 / 是 正在初始化项目 C:\Users\Zhao\Desktop\Vue3WithTs\demo1\demo1... 项目初始化完成,可执行以下命令: cd demo1 npm install npm run dev
Tips:如果文件爆红,是由于缺少依赖包导致的,在项目根目录下输入以下命令:
输入完后关闭vscode
并重新打开即可
目录结构 创建完成后的项目,包括了.vscode
、node_modules
、public
、src
文件夹,以及其他配置文件
.vscode
:不用关注,主要为了第一次使用时,在vscode编译器中安装官方插件
node_modules
:不用关注,存放项目的依赖包
public
:存放图片、图标等相关资源
src
:主要操作的文件夹,需要包含App.vue
和main.ts
文件
index.html
:项目的入口文件
App.vue文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!-- 一个.vue文件中只能存在一个顶层template --> <template> <!-- html --> <div class="title">Hello World</div> </template> <script lang="ts"> // TS或JS export default { name:'App' // 组件名 } </script> <style> /* css样式 */ .title { background-color: #ddd; color: aqua; text-align: center; } </style>
注意点:需要将组件名暴露出去,否则main.js不认为App.vue是一个组件
main.ts文件:
1 2 3 4 5 6 7 import { createApp } from 'vue' import App from './App.vue' createApp (App ).mount ('#app' )
自定义组件 在src文件下创建components
文件夹,并新建一个Person.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div class="person"> <h2>姓名:{{ name }}</h2> <h2>年龄:{{ age }}</h2> <button @click="showTel">点击查看手机号</button> </div> </template> <script lang="ts"> export default { name:'Person', // 组件名 data() { return { name:"张三", age:18, tel:138888888 } }, methods: { showTel(){ alert(this.tel); } } } </script> <style></style>
以上是vue2的写法,证明vue3可以对vue2向下兼容
写完后要注意在App.vue中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <div class="title">Hello World</div> <Person></Person> </template> <script lang="ts"> import Person from './components/Person.vue'; export default { name:'App', // 组件名 components: {Person} // 注册组件 } </script> <style> /* css样式 */ .title { background-color: #ddd; color: aqua; text-align: center; } </style>
Vue2和Vue3的差异 出处:Bilibili-大帅老猿
OptionAPI Vue2的API设计是Options(配置)风格的
缺点:
Options类型的API,数据、方法、计算属性等,是分散在:data
、methods
、computed
中的,若想新增或修改一个需求,就需要分别修改data
、methods
、computed
,不方便维护和复用
Composition Api
Vue3核心语法 setup setup
是vue3
中一个新的配置项,值是一个函数,它是Composition API
“表演的舞台”,组件中所用到的:
均配置在setup
中
特点:
setup函数返回的对象中的内容,可以直接在模板中使用
setup中访问this
是undefined
setup函数会在beforeCreate
之前调用,它是领先所有钩子执行的
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <template> <div class="person"> <h2>姓名:{{ name }}</h2> <h2>年龄:{{ age }}</h2> </div> <button @click="showTel">点击显示手机号码</button> <button @click="changeName">点击修改姓名</button> <button @click="changeAge">点击修改年龄</button> </template> <script lang="ts"> export default { name:'Person', // 组件名 setup() { // 数据 let name:string = "张三"; let age:number = 18; let tel:string = "138xxxxxxxx"; // 方法 function showTel():void { alert(tel); } function changeName():void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的 name = "李四"; } function changeAge():void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的 age++; } return {name,age,showTel,changeName,changeAge} } } </script>
set的返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> ... </template> <script lang="ts"> export default { name:'Person', // 组件名 setup() { ... ... ... return () => { return "Hello World"; } } } </script>
最终模板只会显示Hello World
setup、data、method
setup
、data
、method
是可以同时存在的
由于setup
是早于beforeCreate
的,所以data中是可以读取到setup
中的数据的,但是setup
不能读取data
中的数据
setup语法糖 在setup中创建数据、方法后,每次都需要手动return出去,相对繁琐,此时可以用到setup的语法糖
格式:
1 2 3 4 5 <script setup> // 数据... // 方法... </script>
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <template> <div class="person"> <h2>姓名:{{ name }}</h2> <h2>年龄:{{ age }}</h2> </div> <button @click="showTel">点击显示手机号码</button> <button @click="changeName">点击修改姓名</button> <button @click="changeAge">点击修改年龄</button> </template> <script lang="ts"> export default { name: 'Person', // 组件名 } </script> <script lang="ts" setup> // 这里的语言要一致 // 数据 let name: string = "张三"; let age: number = 18; let tel: string = "138xxxxxxxx"; // 方法 function showTel(): void { alert(tel); } function changeName(): void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的 name = "李四"; } function changeAge(): void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的 age++; } </script>
但是为了控制组件名而单独写一个script标签,又比较奇怪。
此时可以使用到一个插件:
1 npm i vite-plugin-vue-setup-extend -D
安装完插件后,找到根目录下的vite.config.ts
引入插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueSetupExtend from 'vite-plugin-vue-setup-extend' export default defineConfig ({ plugins : [ vue (), vueSetupExtend () ], resolve : { alias : { '@' : fileURLToPath (new URL ('./src' , import .meta .url )) } } })
使用方法:
在setup语法糖标签中,加入name="组件名"
即可
1 2 3 <script lang="ts" setup name="MyPerson"> ... </script>
响应式数据 基本类型(Ref) 在vue2中,data中的数据就已经是响应式的了
但是在vue3中,要将数据处理成响应式,需要引入响应式方法
引入完成后,要将数据改成响应式数据,只需要将值放入到ref
方法内
1 2 let name = ref ("张三" );let age = ref (18 );
需要注意的是,在模板中的值,并不需要改为name.value
,vue会进行自动处理
但是,如果要在setup
中对数据进行处理的话,就必须要加上name.value
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <template> <div class="person"> <h2>姓名:{{ name }}</h2> <h2>年龄:{{ age }}</h2> </div> <button @click="showTel">点击显示手机号码</button> <button @click="changeName">点击修改姓名</button> <button @click="changeAge">点击修改年龄</button> </template> <script lang="ts" setup name="MyPerson"> // 导入 import { ref } from 'vue'; // 数据 let name = ref("张三"); let age = ref(18); let tel: string = "138xxxxxxxx"; console.log(`姓名为:${name.value}`); console.log(`年龄为:${age.value}`); // 方法 function showTel(): void { alert(tel); } function changeName(): void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的 name.value = "李四"; } function changeAge(): void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的 age.value++; } </script>
对象类型数据(Reactive) Ref
只能对基本数据类型进行修改,如:string、number、boolean…
而Reactive
则可以对对象类型进行修改,如:Array、Function….
和Ref
一样,使用前需要先导入
1 import { reactive } from 'vue' ;
格式:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <template> <!-- 展示修改student对象 --> <h1>我叫{{ student.name }},性别是{{ student.gender }},今年{{ student.age }}岁了</h1> <button @click="alterGender">点击修改性别</button> <button @click="grow">点击增加一岁</button> <!-- 展示修改hobbies数组 --> <h2>爱好:</h2> <ul> <li v-for="hobby in hobbies" :key="hobby">{{ hobby }}</li> </ul> <button @click="deleteHobby">删除爱好</button> </template> <script lang="ts" setup name="MyPerson"> import { reactive } from 'vue'; // 定义 student 对象 let student = reactive({ name:"张三", age:18, gender:"男" }) // 定义 hobbies 数组 let hobbies = reactive(["打游戏","敲代码","看电影"]); // 年龄增长方法 function grow() { student.age++; } // 修改性别方法 function alterGender() { student.gender = student.gender == "男" ? "女" : "男"; } // 删除爱好方法 function deleteHobby() { hobbies.pop(); } </script>
注意 注意:Reactive只能定义对象类型的响应式数据
1 2 3 4 <script lang="ts" setup name="MyPerson"> import { reactive } from 'vue'; let name = reactive("张三"); // 类型“string”的参数不能赋给类型“object”的参数。 </script>
所以,Reactive是有局限性的
Ref在对象类型上的使用 ref也可以直接对对象进行使用:
但是在对对象值进行处理时,需要先.value
拿到值,再进行处理
1 2 3 function () { 对象名.value.对象属性 = 值; }
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <template> <!-- 展示修改student对象 --> <h1>我叫{{ student.name }},性别是{{ student.gender }},今年{{ student.age }}岁了</h1> <button @click="alterGender">点击修改性别</button> <button @click="grow">点击增加一岁</button> <!-- 展示修改hobbies数组 --> <h2>爱好:</h2> <ul> <li v-for="hobby in hobbies" :key="hobby">{{ hobby }}</li> </ul> <button @click="deleteHobby">删除爱好</button> </template> <script lang="ts" setup name="MyPerson"> import { ref } from 'vue'; // 定义 student 对象 let student = ref({ name:"张三", age:18, gender:"男" }) // 定义 hobbies 数组 let hobbies = ref(["打游戏","敲代码","看电影"]); // 年龄增长方法 function grow() { student.value.age++; } // 修改性别方法 function alterGender() { student.value.gender = student.value.gender == "男" ? "女" : "男"; } // 删除爱好方法 function deleteHobby() { hobbies.value.pop(); } </script>
Ref vs Reactive 宏观角度看:
ref
用来定义:基本类型数据、对象类型数据
reactive
用来定义:对象类型数据
区别:
ref
创建的变量必须使用.value
(可以使用vscode中的插件自动添加.value)
reactive
重新分配一个新对象,会失去响应式(可以使用Object.assign
去整体替换)
示例:
1 2 3 4 5 6 7 8 9 10 11 const person = reactive ({name :"张三" ,age :20 });function changePerson1 ( ) { person = {name :"李四" ,age :21 }; } function changePerson2 ( ) { Object .assign (person,{name :"李四" ,age :21 }); }
在利用ref的情况下实现以上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 let student = ref ({ name :"张三" , age :18 , gender :"男" }) function changeStudent ( ) { student.value = { name :"李四" , age :21 , gender :"女" } }
使用原则:
若需要一个基本类型的响应式数据,必须使用ref
若需要一个响应式对象,层级不深,ref
、reactive
都可以
若需要一个响应式对象,且层级较深,推荐使用reactive
toRefs 和 toRef
作用:将响应式对象中的每一个属性,转换为ref
对象
备注:toRefs
与toRef
的功能一致,但是toRefs
可以批量转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let student = ref ({ name :"张三" , age :18 , gender :"男" }) let name = student.value .name ;function changeStudent ( ) { student.value .name += '~' ; console .log (student.value .name ); console .log (name); }
上面说明了直接将响应式对象的值赋给一个变量,该变量的值并不会成为响应式
而使用toRefs
后,效果如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { ref, toRefs } from 'vue' ;let student = ref ({ name :"张三" , age :18 , gender :"男" }) let { name,age,gender } = toRefs (student.value );function changeStudent ( ) { student.value .name += '~' ; console .log (student.value .name ); console .log (name.value ); }
并且,如果直接对name修改,也会导致student.value.name的被修改
1 2 3 4 5 6 function changeStudent ( ) { name.value += '~' ; console .log (student.value .name ); console .log (name.value ); }
计算属性(Computed) 模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护
示例:
在不使用计算属性的情况下,完成如下需求:
根据输入框的内容,将全名显示出来
将全名中的首字母大写
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div>姓:<input type="text" v-model="firstName"></div> <div>名:<input type="text" v-model="lastName"></div> <div>全名:{{ firstName.slice(0,1).toUpperCase() + firstName.slice(1) }} - {{ lastName }}</div> </template> <script lang="ts" setup name="MyPerson"> import { computed, ref } from 'vue'; let firstName = ref("zhang"); let lastName = ref("san") </script>
使用computed
后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div>姓:<input type="text" v-model="firstName"></div> <div>名:<input type="text" v-model="lastName"></div> <div>全名:{{ fullName }}</div> </template> <script lang="ts" setup name="MyPerson"> import { computed, ref } from 'vue'; let firstName = ref("zhang"); let lastName = ref("san") let fullName = computed(() => { return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value; }) </script>
计算属性缓存 计算属性是带有缓存的
示例:
调用计算属性的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template> <div>姓:<input type="text" v-model="firstName"></div> <div>名:<input type="text" v-model="lastName"></div> <div>全名:{{ fullName }}</div> <div>全名:{{ fullName }}</div> <div>全名:{{ fullName }}</div> <div>全名:{{ fullName }}</div> <div>全名:{{ fullName }}</div> <!-- 调用fullName方法5次,控制台只会输出一次 1 --> </template> <script lang="ts" setup name="MyPerson"> import { computed, ref } from 'vue'; // 将全名的首字母大写 let firstName = ref("zhang"); let lastName = ref("san") let fullName = computed(() => { console.log(1); return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value; }) </script>
自定义方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template> <div>姓:<input type="text" v-model="firstName"></div> <div>名:<input type="text" v-model="lastName"></div> <div>全名:{{ withOutComputed() }}</div> <div>全名:{{ withOutComputed() }}</div> <div>全名:{{ withOutComputed() }}</div> <div>全名:{{ withOutComputed() }}</div> <div>全名:{{ withOutComputed() }}</div> <!-- 调用withOutComputed方法5次,控制台输出5次 1 --> </template> <script lang="ts" setup name="MyPerson"> import { computed, ref } from 'vue'; // 将全名的首字母大写 let firstName = ref("zhang"); let lastName = ref("san") let withOutComputed = function() { console.log(1); return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value; } </script>
可写计算属性 计算属性默认是只读的。当尝试修改一个计算属性时,会收到一个运行时警告。
1 2 3 4 5 6 7 8 let fullName = computed (() => { console .log (1 ); return firstName.value .slice (0 ,1 ).toUpperCase () + firstName.value .slice (1 ) + "-" + lastName.value ; }) function alertComputed ( ) { fullName.value = 123 ; }
只在某些特殊场景中可能才需要用到“可写”的属性,可以通过同时提供 getter 和 setter 来创建:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <template> <div>姓:<input type="text" v-model="firstName"></div> <div>名:<input type="text" v-model="lastName"></div> <div>全名:{{ fullName }}</div> <button @click="alertComputed">修改fullName方法的值</button> </template> <script lang="ts" setup name="MyPerson"> import { computed, ref } from 'vue'; // 将全名的首字母大写 let firstName = ref("zhang"); let lastName = ref("san") let fullName = computed({ get() { return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value; }, set(val) { console.log(val); // li-si let [str1,str2] = val.split("-"); console.log(str1,str2); // li-si firstName.value = str1; lastName.value = str2; }, }) function alertComputed() { fullName.value = "li-si"; } </script>
监视(watch) 作用:监视数据的变化(和Vue2中的watch作用一致)
特点:Vue3中的watch只能监视以下四种数据:
ref
定义的数据
reactive
定义的数据
函数返回一个值
一个包含上述内容的数组
监视Ref基本类型数据 监视ref
定义的基本数据类型,直接写数据名即可,监视的是其value
值的改变
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <h1>当前求和值为:{{ count }}</h1> <button @click="addSum">将求和值+1</button> </template> <script lang="ts" setup name="MyPerson"> import { ref,watch } from 'vue'; let count = ref(0) function addSum() { count.value++; } // 监视 let watchCount = watch(count,(newVal,oldVal) => { // count不需要.value,因为监视的是ref console.log("新的值:" + newVal + ";旧的值:" + oldVal); // 第一次点击,控制台返回:新的值:1;旧的值:0 }); </script>
上述例子中,watchCount
这个函数只要被调用,就会把watch
方法停止。
1 2 3 4 5 6 7 let watchCount = watch (count,(newVal,oldVal ) => { console .log ("新的值:" + newVal + ";旧的值:" + oldVal); if (newVal === 10 ) { watchCount (); } });
监视Ref对象类型数据 监视ref
定义的对象类型数据:直接写数据名,监视的是对象的地址值,若想监视对象内部的数据,要手动开启深度监视
注意:
若修改的是ref定义的对象中的属性,newVal
和oldVal
都是新值,因为它们是同一个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <template> <h1>姓名:{{ person.name }}</h1> <h1>年龄:{{ person.age }}</h1> <h1>性别:{{ person.gender }}</h1> <button @click="changeName">修改姓名</button> <button @click="changeAge">修改年龄</button> <button @click="changeGender">修改性别</button> <button @click="changePerson">整体修改</button> </template> <script lang="ts" setup name="MyPerson"> import { ref,watch } from 'vue'; // 定义数据 let person = ref({ name:"张三", age:19, gender:"男" }) // 定义方法 function changeName() { person.value.name += "~"; } function changeAge() { person.value.age++; } function changeGender() { person.value.gender = person.value.gender == "男" ? "女" : "男"; } function changePerson() { person.value = { name:"李四", age:20, gender: "女" } } // 监视 watch(person.value,(newVal,oldVal) => { // 点击第一次修改姓名,返回:newVal:张三~,oldVal:张三~ console.log(`newVal:${newVal.name},oldVal:${oldVal.name}`); }) </script>
上述例子中,watch
监视的是person.value.name
,其他值类似
若修改整个ref定义的对象,newVal
是新值,oldVal
是旧值,因为不是同一个对象了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 <template> <h1>姓名:{{ person.name }}</h1> <h1>年龄:{{ person.age }}</h1> <h1>性别:{{ person.gender }}</h1> <button @click="changeName">修改姓名</button> <button @click="changeAge">修改年龄</button> <button @click="changeGender">修改性别</button> <button @click="changePerson">整体修改</button> </template> <script lang="ts" setup name="MyPerson"> import { ref,watch } from 'vue'; // 定义数据 let person = ref({ name:"张三", age:19, gender:"男" }) // 定义方法 function changeName() { person.value.name += "~"; } function changeAge() { person.value.age++; } function changeGender() { person.value.gender = person.value.gender == "男" ? "女" : "男"; } function changePerson() { person.value = { name:"李四", age:20, gender: "女" } } // 监视 watch(person,(newVal,oldVal) => { // 点击整体修改 console.log(`newVal.name:${newVal.name},oldVal.name:${oldVal.name}`); // newVal.name:李四,oldVal.name:张三 console.log(`newVal.age${newVal.age},oldVal.age${oldVal.age}`); // newVal.age20,oldVal.age19 console.log(`newVal.gender${newVal.gender},oldVal.gender${oldVal.gender}`); // newVal.gender女,oldVal.gender男 }) </script>
上述例子中,watch
监视的是整个person
对象
根据上面两个例子,可以发现:
如果watch
的第一个参数是person
这个整体,那么只有当整体发生变化时,才会被监视到
而如果watch
的第一参数是person.value
,那么只有当person
内部的值发生变化时,才会被监视,而修改整个person
并不会被监视
此时可以给watch添加一个参数,表示深度监视
监视参数(深度监视等) 深度监视(deep) 格式:
1 watch(监视对象,回调函数,{deep:true});
示例:
1 2 3 4 5 6 watch (person,(newVal,oldVal ) => { console .log (`newVal.name:${newVal.name} ,oldVal.name:${oldVal.name} ` ); console .log (`newVal.age${newVal.age} ,oldVal.age${oldVal.age} ` ); console .log (`newVal.gender${newVal.gender} ,oldVal.gender${oldVal.gender} ` ); },{deep :true })
格式:
1 watch(监视对象,回调函数,{immediate:true});
示例:
1 2 3 4 watch (person,(newVal,oldVal ) => { console .log ("newVal.name:" , newVal.name , ",oldVal.name:" , oldVal?.name ); },{immediate :true })
监视Reactive对象类型数据 和监视ref
的区别在于,监视reactive
对象类型数据:自动开启深度监视(deep)
即:隐式开启深度监视
就算将watch
的深度监视参数手动改为false
,也无法关闭
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <template> <h1>姓名:{{ person.name }}</h1> <h1>年龄:{{ person.age }}</h1> <h1>性别:{{ person.gender }}</h1> <button @click="changeName">修改姓名</button> <button @click="changeAge">修改年龄</button> <button @click="changeGender">修改性别</button> <button @click="changePerson">整体修改</button> </template> <script lang="ts" setup name="MyPerson"> import { reactive,watch } from 'vue'; // 定义数据 let person = reactive({ name:"张三", age:19, gender:"男" }) // 定义方法 function changeName() { person.name += "~"; } function changeAge() { person.age++; } function changeGender() { person.gender = person.gender == "男" ? "女" : "男"; } function changePerson() { Object.assign(person,{ name:"李四", age:20, gender: "女" }); } // 监视 // 点击修改姓名、年龄、性别、整体都会监视 watch(person,(newVal,oldVal) => { console.log("newVal" , newVal, ",oldVal:" , oldVal); }) </script>
监视响应式数据中的具体属性 基本类型 如果要监视ref或reactive中的某一个具体属性(基本类型),直接写成以下形式是错误的:
1 2 3 watch (person.name ,(newVal,oldVal ) => { console .log ("newVal" , newVal, ",oldVal:" , oldVal); })
这是由于watch监视的对象是严格限制的,查看限制类型
解决方法:
写一个函数,并在函数中返回监视的对象
1 2 3 watch (()=> person.name ,(newVal,oldVal ) => { console .log ("newVal" , newVal, ",oldVal:" , oldVal); })
对象类型 假设现在的Person为以下形式:
1 2 3 4 5 6 7 8 9 let person = reactive ({ name :"张三" , age :19 , gender :"男" , address :{ province :"江苏" , city :"苏州" } })
如果要监视address里的属性,写成以下方式也是可以的
1 2 3 watch (person.address ,(n,o ) => { console .log (n,o) })
但是:
如果只修改address.province
,是能被监视到的
如果修改address
就不会被监视
解决方法也是将这个对象以函数的形式返回
1 2 3 watch (()=> person.address ,(n,o ) => { console .log (n,o) })
总结 不管是基本类型还是对象类型,都建议以函数的形式返回
监视多个类型 将多个类型包裹为一个数组传入到watch中就可以
1 2 3 watch ([()=> person.address ,()=> person.name ],(n,o ) => { console .log (n,o) })
WatchEffect 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数
watch
对比watchEffect
:
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch:需要明确指出监视的数据
watchEffect:不用明确的指出监视的数据(函数中用到哪些属性,就监视哪些属性)
假设有多个响应式数据:
1 2 3 let a = ref (0 );let b = ref (0 );...
如果要同时监视以上的所有数据,用watch
的方法:
1 2 3 4 5 6 7 8 9 10 11 watch ([a,b,...],(val ) => { if (val.[0 ] > 10 ) { console .log ("a > 10" ); } if (val.[1 ] > 10 ) { console .log ("a > 10" ); } ... })
可以看出需要将被监视的数据逐个填写到形参中
而使用watchEffect方法,则是如下的样子:
1 2 3 4 5 6 7 8 9 10 11 watchEffect (() => { if (a.value > 10 ) { console .log ("a > 10" ) } if (b.value > 10 ) { console .log ("a > 10" ) } ... })
标签的ref属性 作用:用于注册模板引用
用在普通DOM标签
上,获取的是DOM节点
用在组件标签上,获取的是组件实例对象
普通DOM标签 假设Person.vue
的内容如下:
1 2 3 4 5 6 7 8 9 10 <template> <h2 id="title">Person.vue</h2> <button @click="showLog">点击按钮</button> </template> <script lang="ts" setup name="Person"> function showLog() { console.log(document.getElementById("title")); } </script>
效果是点击按钮控制台会输出模板中的的h2标签
但是在App.vue
中,id的名称也是title
:
1 2 3 4 5 6 7 8 <template> <h2 id="title">App.vue</h2> <Person></Person> </template> <script lang="ts" setup name="App"> import Person from './components/Person.vue'; </script>
那么点击按钮后,控制台输出的结果是
1 <h2 id ="title" > App.vue</h2 >
这是由于id名称的冲突,谁先使用id就输出谁。
解决的办法就是用ref
示例:
Person.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <h2 ref="title">Person</h2> <button @click="showLog">点击按钮</button> </template> <script lang="ts" setup name="Person"> import { ref } from 'vue'; // 创建一个title,用于存储ref标记的内容 let title = ref(); function showLog() { console.log(title.value); } </script>
App.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <h2 ref="title">App.vue</h2> <button @click="showLog">点击按钮</button> <Person></Person> </template> <script lang="ts" setup name="App"> import { ref } from 'vue'; import Person from './components/Person.vue'; let title = ref(); function showLog() { console.log(title.value); } </script>
组件标签 如果将ref绑定在组件标签上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <h2 ref="title">App.vue</h2> <button @click="showLog">点击按钮</button> <Person ref="componentPerson"></Person> </template> <script lang="ts" setup name="App"> import { ref } from 'vue'; import Person from './components/Person.vue'; let title = ref(); let componentPerson = ref(); function showLog() { console.log(componentPerson.value); } </script>
那么输出的结果是:
1 Proxy(Object) {__v_skip: true}
但是并不能查看到有用的信息,这是由于vue
的安全机制。父级不能随意查看子级的内容,如果子级允许父级查看,可以使用defineExpose
(可以手动引入,不过最新的vue3已经自动引入了)
Person.vue中使用该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <h2 ref="title">Person</h2> <button @click="showLog">点击按钮</button> </template> <script lang="ts" setup name="Person"> import { ref } from 'vue'; let title = ref(); let a = ref(0); function showLog() { console.log(title.value); } defineExpose({title,a}) </script>
修改完后,输出的结果为:
1 Proxy(Object) {title: RefImpl, a: RefImpl, __v_skip: true}
Ts中的接口、泛型、自定义类型 接口 假设要将对象的属性类型限制,这时候就可以用到ts
语法中的接口
在src
目录下创建一个types
的目录,再创建一个index.ts
文件,文件内容如下:
1 2 3 4 5 export interface PersonInter { id :string ; name :string ; age :number ; }
随后引入到Person.vue
:
1 2 3 4 5 6 7 8 9 <template> </template> <script lang="ts" setup name="Person"> // 引入PersonInter前面一定要加type,说明这是一个规范接口 import {type PersonInter} from "@/types" const person:PersonInter = {id:"aaaa0001",name:"张三",age:20} </script>
泛型 假设要将数组中的对象的属性类型限制,这时候就可以用到ts
语法中的泛型
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> </template> <script lang="ts" setup name="Person"> import {type PersonInter} from "@/types" const personList:Array<PersonInter> = [ {id:"aaaa0001",name:"张三",age:20}, {id:"aaaa0002",name:"李四",age:22}, {id:"aaaa0003",name:"王五",age:18} ] </script>
自定义类型 personList:Array<PersonInter>
这样的写法,可读性较差,所以可以使用自定义类型来优化代码
首先在之前创建的index.ts中创建自定义类型:
1 2 3 4 5 6 7 export interface PersonInter { id :string ; name :string ; age :number ; } export type Persons = Array <PersonInter >
随后在Person.vue
中导入:
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> </template> <script lang="ts" setup name="Person"> import {type PersonInter,type Persons} from "@/types" const personList:Persons = [ {id:"aaaa0001",name:"张三",age:20}, {id:"aaaa0002",name:"李四",age:22}, {id:"aaaa0003",name:"王五",age:18} ] </script>
reactive的结合使用 如果在reactive
中使用泛型、接口、自定义类型,可以写为以下形式(不推荐):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> </template> <script lang="ts" setup name="Person"> import {type PersonInter,type Persons} from "@/types" import { reactive } from "vue"; const personList:Persons = reactive([ {id:"aaaa0001",name:"张三",age:20}, {id:"aaaa0002",name:"李四",age:22}, {id:"aaaa0003",name:"王五",age:18} ]) </script>
推荐的用法是把泛型应用到方法上:
1 2 3 4 5 6 7 8 9 10 <script lang="ts" setup name="Person"> import {type PersonInter,type Persons} from "@/types" import { reactive } from "vue"; const personList = reactive<Persons>([ {id:"aaaa0001",name:"张三",age:20}, {id:"aaaa0002",name:"李四",age:22}, {id:"aaaa0003",name:"王五",age:18} ]) </script>
Props的引用 将父组件的内容发送给子组件,可以在子组件中使用defineProps
方法用于接收
父组件:
1 2 3 4 5 6 7 <template> <Person a="你好"/> </template> <script lang="ts" setup name="App"> import Person from './components/Person.vue'; </script>
子组件:
1 2 3 4 5 6 7 <template> <h2>{{ a }}</h2> </template> <script lang="ts" setup name="Person"> defineProps(["a"]) </script>
注意:defineProps
中保存的是数组,且数组中的内容类型为字符串 ,字符串的内容是父组件中子组件标签的属性名称
由于数组的内容是字符串,所以不能在script
标签中直接使用,但是在模板中可以直接使用。如果想要用变量保存a
,则可以定义一个变量用于接收defineProps
的返回值
示例:
1 2 3 4 5 6 7 8 9 10 <template> <h2>{{ a }}</h2> </template> <script lang="ts" setup name="Person"> let x = defineProps(["a"]); console.log(x); console.log(x.a); </script>
输出内容如下:
1 2 Proxy(Object) {a: '你好'} 你好
将对象、变量通过Props传递 示例:
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <Person a="你好" :sayHi :personList/> </template> <script lang="ts" setup name="App"> import Person from './components/Person.vue'; import {type PersonInter,type Persons} from "@/types" import { reactive } from "vue"; const sayHi = "你好" const personList = reactive<Persons>([ {id:"aaaa0001",name:"张三",age:20}, {id:"aaaa0002",name:"李四",age:22}, {id:"aaaa0003",name:"王五",age:18} ]) </script>
需要注意的是,和之前直接传输字符串不同,由于这次传输的是变量、对象,此时需要在前面加上冒号表示动态数据
子组件:
1 2 3 4 5 6 7 8 9 10 11 12 <template> <h2>{{ a }}</h2> <h2>{{ sayHi }}</h2> <h2>{{ personList }}</h2> </template> <script lang="ts" setup name="Person"> let x = defineProps(["a","sayHi","personList"]); console.log(x); console.log(x.a); </script>
defineProps限制类型 为了防止父组件传输错误的类型,如<Person :personList="5"/>
,这会导致v-for
空循环五次,输出空内容
可以使用之前的泛型来约束传输类型,示例如下:
1 2 3 4 5 6 7 8 9 10 11 <template> <ul> <li v-for="list in personList" :key="list.id">姓名:{{ list.name }},年龄:{{ list.age }}</li> </ul> </template> <script lang="ts" setup name="Person"> import {type Persons} from "@/types" defineProps<{personList:Persons}>() </script>
defineProps<{personList:Persons}>()
的详细意思如下:
<>
表示是泛型
{}
表示接收的对象
personList
表示接收的对象是谁
Persons
表示约束类型
defineProps更多用法 除了限制类型,还有限制必要性以及指定默认值
限制必要性 当父组件没有传输内容,而子组件又接收了不存的内容,此时代码会报错,这时候可以使用ts
中的?
。它表示这是一个可选的内容,当父组件存在则接收,不存在则忽略。
示例:
1 2 3 4 5 6 7 8 9 10 11 <template> <ul> <li v-for="list in personList" :key="list.id">姓名:{{ list.name }},年龄:{{ list.age }}</li> </ul> </template> <script lang="ts" setup name="Person"> import {type Persons} from "@/types" defineProps<{personList?:Persons}>() </script>
默认值 既然父组件没有传输内容,那么子组件就需要一个默认值来顶替父组件本应该传输的内容,这里可以用到vue的withDefault
方法,该方法需要引入:
1 import { withDefaults } from "vue";
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> <ul> <li v-for="list in personList" :key="list.id">姓名:{{ list.name }},年龄:{{ list.age }}</li> </ul> </template> <script lang="ts" setup name="Person"> import {type Persons} from "@/types" import { withDefaults } from "vue"; withDefaults(defineProps<{personList?:Persons}>(),{ personList: () => [{id:"aaaa0000",name:"匿名",age:0}] }) </script>
withDefaults
说明:
第一个参数:表示接收的数据
第二个参数:当没有接收到数据时,使用第二个参数中的值,注意的是对象的值必须为函数
的返回值
生命周期 vue2
中,生命周期有四个阶段(创建、挂载、更新、销毁),每个阶段分前后两种,共以下8种:
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeDestroy
destroyed
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <template> <div> <p>{{ message }}</p> </div> </template> <script> export default { data() { return { message: 'Hello Vue!' }; }, beforeCreate() { console.log('beforeCreate: 组件实例被创建之前'); }, created() { console.log('created: 组件实例创建完成'); }, beforeMount() { console.log('beforeMount: 组件挂载到DOM之前'); }, mounted() { console.log('mounted: 组件挂载到DOM之后'); }, beforeUpdate() { console.log('beforeUpdate: 组件数据更新之前'); }, updated() { console.log('updated: 组件数据更新之后'); }, beforeDestroy() { console.log('beforeDestroy: 组件实例销毁之前'); }, destroyed() { console.log('destroyed: 组件实例销毁之后'); } }; </script>
在vue3
中,与vue2
类似,依然保留了创建、挂载、更新和销毁四个阶段,但在细节上有所调整和优化。
下面是 Vue 3 的生命周期钩子函数列表:
setup:创建
onBeforeMount: 在挂载开始之前被调用,相关的渲染函数首次被调用。
onMounted: 实例挂载完成后被调用,此时 DOM 元素已经插入文档中。
onBeforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
onUpdated: 组件更新完成后被调用,此时 DOM 已经更新。
onBeforeUnmount: 在卸载组件之前调用。
onUnmounted: 组件卸载后调用。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <script> // 创建 console.log('创建') // 挂载前 onBeforeMount(()=>{ console.log('挂载前') }) // 挂载完毕 onMounted(()=>{ console.log('挂载完毕') }) // 更新前 onBeforeUpdate(()=>{ console.log('更新前') }) // 更新完毕 onUpdated(()=>{ console.log('更新完毕') }) // 卸载前 onBeforeUnmount(()=>{ console.log('卸载前') }) // 卸载完毕 onUnmounted(()=>{ console.log('卸载完毕') }) </script>
自定义Hooks 类似于封装函数(自己的理解来说的话是这样的)
示例:
不使用自定义Hooks案例 在不使用hook的情况下实现如下要求:
页面展示数字,默认值为0,点击按钮后+1
页面展示图片,无默认图片,点击按钮后添加一张图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <template> <h2 id="showNum">{{ num }}</h2> <button @click="sum">点击按钮数字+1</button> <br> <img v-for="(dog,index) in dogList" :src="dog" :key="index"> <br> <button @click="getPicture">点击按钮获取图片</button> </template> <script lang="ts" setup name="Person"> import { ref } from 'vue'; import axios from 'axios'; // 定义数字 let num = ref(0) // 定义图片数组 let dogList:any = ref([]); // 定义数字增加函数 function sum() { num.value++; } // 定义获取图片函数 async function getPicture() { try { let src = await axios.get('https://dog.ceo/api/breeds/image/random'); dogList.value.push(src.data.message); } catch (error) { alert(error) } } </script> <style scoped> h2 { color: aqua; } img { width: 100px; margin: 20px; } </style>
使用自定义Hooks案例 将以上的内容改为使用自定义hooks的方法如下:
首先在src目录下(与components目录同级),创建一个hooks文件夹,将内容拆分,涉及到图片的就创建一个名为useImg.ts的文件将相关内容剪切到该文件中,涉及到数字的就创建一个useNum.ts的文件将相关内容剪切到该文件中。
useImg.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { ref } from 'vue' ;import axios from 'axios' ;export default function ( ) { let dogList :any = ref ([]); async function getPicture ( ) { try { let src = await axios.get ('https://dog.ceo/api/breeds/image/random' ); dogList.value .push (src.data .message ); } catch (error) { alert (error) } } return {dogList,getPicture} }
useNum.ts:
1 2 3 4 5 6 7 8 9 10 11 12 import { ref } from 'vue' ;export default function ( ) { let num = ref (0 ) function sum ( ) { num.value ++; } return {num,sum} }
注意:
相关内容必须要包含在函数内,且函数需要暴露出去,由于是匿名函数,所以必须使用export default
函数中将相关的东西要return
出去
在原来的模板中使用自定义hooks的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <template> <h2 id="showNum">{{ num }}</h2> <button @click="sum">点击按钮数字+1</button> <br> <img v-for="(dog,index) in dogList" :src="dog" :key="index"> <br> <button @click="getPicture">点击按钮获取图片</button> </template> <script lang="ts" setup name="Person"> import useImg from '@/hooks/useDog'; import useNum from '@/hooks/useNum'; const {dogList,getPicture} = useImg(); const {num,sum} = useNum(); </script> <style scoped> h2 { color: aqua; } img { width: 100px; margin: 20px; } </style>
路由 路由是 URL 与视图组件之间的映射。程序员可以定义路由规则,让特定的 URL 映射到特定的视图组件。
以下是基础的路由实现实例:
首先在App.vue中创建基本的样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 <template> <div id="test"> <h2>路由测试页面</h2> <!-- 导航栏 --> <div id="navigate"> <a src="/home" >首页</a> <a src="/news" >新闻</a> <a src="/about">关于</a> </div> <!-- 展示区 --> <div id="content"> 之后组件展示的区域 </div> </div> </template> <script lang="ts" setup name="App"> </script> <style> /* 页面总体样式 */ #test { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; text-align: center; color: #2c3e50; background: linear-gradient(to bottom, #ece9e6, #ffffff); padding: 30px; border-radius: 10px; box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); max-width: 900px; margin: 50px auto; } /* 标题样式 */ #test h2 { font-size: 28px; color: #2980b9; margin-bottom: 25px; } /* 导航栏样式 */ #navigate { margin-bottom: 25px; display: flex; justify-content: center; gap: 15px; } #navigate a { text-decoration: none; color: #fff; background-color: #3498db; padding: 12px 25px; border-radius: 20px; transition: background-color 0.3s, transform 0.3s; } #navigate a:hover { background-color: #1abc9c; transform: scale(1.05); } #navigate a.active { background-color: #1abc9c; } /* 展示区样式 */ #content { font-size: 18px; line-height: 1.8; background-color: #ffffff; padding: 25px; border-radius: 10px; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); } </style>
以上代码主要包含了基本的框架,如页面的标题、导航栏以及展示区,并不涉及路由
创建相对应的模板,Home.vue
、News.vue
、About.vue
1 2 3 4 5 <template> <h1>Home组件页面</h1> </template> <script lang="ts" setup name="Home"></script>
创建路由文件夹,用于存放路由器。路径为src/router
,在文件夹内创建路由器文件index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { createRouter,createWebHistory } from 'vue-router' import Home from '@/components/Home.vue' import About from '@/components/About.vue' import News from '@/components/News.vue' const router = createRouter ({ history :createWebHistory (), routes :[ { path :'/' , component :Home }, { path :'/home' , component :Home }, { path :'/news' , component :News }, { path :'/about' , component :About } ] }) export default router
如果显示缺少路由模块,可以手动下载:
然后引入createRouter,createWebHistory
,这两个的作用分别是创建路由器和设置路由器工作模式。
注意:如果不设置路由器工作模式代码会报错
最重要是要在main.ts
中修改相关内容,确保正确的使用到了路由器
1 2 3 4 5 6 7 8 9 10 import { createApp } from 'vue' import App from './App.vue' import router from './router' createApp (App ).use (router).mount ('#app' )
此时只需要在App.vue
文件中稍加修改就可以正确的引用路由了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 <template> <div id="test"> <h2>路由测试页面</h2> <!-- 导航栏 --> <div id="navigate"> <RouterLink to="/home" active-class="active">首页</RouterLink> <RouterLink to="/news" active-class="active">新闻</RouterLink> <RouterLink to="/about" active-class="active">关于</RouterLink> </div> <!-- 展示区 --> <div id="content"> <RouterView></RouterView> </div> </div> </template> <script lang="ts" setup name="App"> import { RouterView, RouterLink } from 'vue-router'; </script> <style> /* 页面总体样式 */ #test { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; text-align: center; color: #2c3e50; background: linear-gradient(to bottom, #ece9e6, #ffffff); padding: 30px; border-radius: 10px; box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); max-width: 900px; margin: 50px auto; } /* 标题样式 */ #test h2 { font-size: 28px; color: #2980b9; margin-bottom: 25px; } /* 导航栏样式 */ #navigate { margin-bottom: 25px; display: flex; justify-content: center; gap: 15px; } #navigate a { text-decoration: none; color: #fff; background-color: #3498db; padding: 12px 25px; border-radius: 20px; transition: background-color 0.3s, transform 0.3s; } #navigate a:hover { background-color: #1abc9c; transform: scale(1.05); } #navigate a.active { background-color: #1abc9c; } /* 展示区样式 */ #content { font-size: 18px; line-height: 1.8; background-color: #ffffff; padding: 25px; border-radius: 10px; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); } </style>
<RouterLink>
的作用<RouterLink>
组件用于创建导航链接,它允许用户在不同的路由之间进行切换。它的作用类似于 HTML 中的 <a>
标签,但是具有更多的 Vue Router
特性。
主要功能:
导航链接:可以使用 to
属性指定目标路由的路径。当用户点击链接时,会导航到指定的路由。
1 <RouterLink to="/home">首页</RouterLink>
动态路由:支持动态生成路由链接,根据数据或条件生成不同的目标路径。
1 <RouterLink :to="{ name: 'user', params: { userId: 123 }}">用户123</RouterLink>
激活类名:可以通过 active-class
属性设置当前路由激活时应用的类名,方便样式定制。
1 <RouterLink to="/home" active-class="active">首页</RouterLink>
替换模式:使用 replace
属性进行导航时,不会向历史记录中添加新记录。
1 <RouterLink to="/home" replace>首页</RouterLink>
<RouterView>
的作用<RouterView>
组件用于显示匹配的视图组件。它是 Vue Router 中的一个占位符,当路由匹配时,会在这个位置渲染对应的组件
主要功能:
视图渲染:根据当前路由,渲染相应的组件。当用户导航到不同路由时,<RouterView>
会动态地替换显示的组件。
1 <RouterView></RouterView>
嵌套路由:支持嵌套使用<RouterView>
,用于渲染子路由的组件。父级<RouterView>
渲染父路由的组件,子级<RouterView>
渲染子路由的组件。
1 2 <RouterView></RouterView> <RouterView name="child"></RouterView>
路由过渡:可以与 Vue 的<transition>
组件结合使用,实现视图切换的过渡动画。
1 2 3 <transition name="fade"> <RouterView></RouterView> </transition>
关于路由的注意点
路由组件通常存放在pages
或views
文件夹,一般组件通常存放在components
文件夹
通过点击导航,视觉效果上“消失”了的路由组件,默认是被卸载掉的,需要的时候再去挂载
路由工作模式 历史模式(History Mode) 工作原理:利用浏览器的 history.pushState 和 history.replaceState 方法来管理路由。URL 中没有哈希(#)符号,路径看起来像正常的 URL。
优点:URL 更加美观且符合 SEO(搜索引擎优化)要求。
缺点:需要服务端配置支持,否则刷新页面时会出现 404 错误。
哈希模式(Hash Mode) 工作原理:利用 URL 的哈希(#)符号来模拟一个完整的 URL。当 URL 改变时,页面不会重新加载。
优点:不需要服务端配置,适用于所有浏览器,特别是在开发环境中很方便。
缺点:URL 中包含哈希符号,不太美观。
命名路由 在 Vue 3 中,路由命名是一种为路由定义名称的方式,以便在导航和动态路由匹配时更容易引用。这对于大型应用程序尤为有用,因为它可以让代码更加简洁和易读。
使用方法
定义命名路由: 在定义路由时,通过 name 属性为路由指定一个名称。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { createRouter, createWebHistory } from 'vue-router' ;import Home from './views/Home.vue' ;import News from './views/News.vue' ;import About from './views/About.vue' ;const routes = [ { path : '/' , component : Home , name : 'home' }, { path : '/news' , component : News , name : 'news' }, { path : '/about' , component : About , name : 'about' } ]; const router = createRouter ({ history : createWebHistory (), routes }); export default router;
使用命名路由进行导航: 在使用<RouterLink>
组件进行导航时,可以通过 :to 属性指定目标路由的名称,而不是路径。
1 2 3 4 5 6 7 8 9 10 11 12 <template> <nav> <RouterLink :to="{ name: 'home' }">首页</RouterLink> <RouterLink :to="{ name: 'news' }">新闻</RouterLink> <RouterLink :to="{ name: 'about' }">关于</RouterLink> </nav> <RouterView></RouterView> </template> <script lang="ts" setup> import { RouterLink, RouterView } from 'vue-router'; </script>
路由嵌套 在 Vue 3 中,嵌套路由允许在父路由的基础上定义子路由,从而实现复杂的页面结构。这种设计使得开发多层次、多视图的单页应用变得更加容易和直观。
实例:
如果要求在之前代码的基础上,增加以下需求:
在新闻页面添加导航栏
导航栏下面添加不同的需要呈现的模板
模板内展示图片,且只用一个模板就能展示不同的图片
解决需求如下:
在News.vue中修改相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <template> <h1>News组件页面</h1> <div id="news_navigate"> <div> <div v-for="(img, index) in imgList" :key="index"> <RouterLink to="/news/details" active-class="active">{{ img.title }}</RouterLink> <br> </div> </div> <div id="imgBorder"><RouterView></RouterView></div> </div> </template> <script lang="ts" setup name="News"> import { reactive } from 'vue'; let imgList = reactive([ { id: "aaaa001", url: 'https://images.dog.ceo/breeds/shiba/shiba-1.jpg', title: '狗狗图01' }, { id: "aaaa002", url: 'https://images.dog.ceo/breeds/shiba/shiba-2.jpg', title: '狗狗图02' }, { id: "aaaa003", url: 'https://images.dog.ceo/breeds/shiba/shiba-3.jpg', title: '狗狗图03' }, { id: "aaaa004", url: 'https://images.dog.ceo/breeds/shiba/shiba-4.jpg', title: '狗狗图04' }, { id: "aaaa005", url: 'https://images.dog.ceo/breeds/shiba/shiba-5.jpg', title: '狗狗图05' }, { id: "aaaa006", url: 'https://images.dog.ceo/breeds/shiba/shiba-6.jpg', title: '狗狗图06' } ]) </script> <style scoped> #news_navigate { display: flex; align-items: center; justify-content: space-around; flex-wrap: wrap; } a { text-decoration: none; color: #fff; background-color: #3498db; padding: 6px 9px; border-radius: 7px; transition: background-color 0.3s, transform 0.3s; font-size: 12px; } a:hover { background-color: #1abc9c; transform: scale(1.05); } a.active { background-color: #1abc9c; } #imgBorder { width: 100px; border: 1px black; } </style>
将相关的数据存放在一个列表中,并用v-for
循环遍历展示出内容,由于内容中需要包含另外的路由模板,所以创建一个Details.vue
的路由模板:
1 2 3 4 5 6 7 <template> 123123123 </template> <script lang="ts" setup name="Details"> </script>
并在index.ts
路由器文件中修改代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { createRouter,createWebHistory } from 'vue-router' import Home from '@/components/Home.vue' import About from '@/components/About.vue' import News from '@/components/News.vue' import Details from '@/components/Details.vue' const router = createRouter ({ history :createWebHistory (), routes :[ { path :'/' , component :Home }, { path :'/home' , component :Home }, { path :'/news' , component :News , children :[ { path :"details" , component :Details } ] }, { path :'/about' , component :About } ] }) export default router
此时,路由嵌套就实现了,但是Details.vue
中的内容只是固定死的,点击哪个按钮展示哪张图片的功能需要之后的路由传参
路由传参 query 发送query参数 只需要将路径改为query
的格式即可:
query
参数的格式一般为abc.com/test?a=1&b=2&c=3
1 <RouterLink :to="{path:'/xxx/xxx',query:{a:123,b:123}}">{{ img.title }}</RouterLink>
示例:
News.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <template> <h1>News组件页面</h1> <div id="news_navigate"> <div> <div v-for="(img, index) in imgList" :key="index"> <RouterLink :to="{path:'/news/details',query:{url:img.url}}">{{ img.title }}</RouterLink> <br> </div> </div> <div id="imgBorder"><RouterView></RouterView></div> </div> </template> <script lang="ts" setup name="News"> import { reactive } from 'vue'; let imgList = reactive([ { id: "aaaa001", url: 'https://images.dog.ceo/breeds/shiba/shiba-1.jpg', title: '狗狗图01' }, { id: "aaaa002", url: 'https://images.dog.ceo/breeds/shiba/shiba-2.jpg', title: '狗狗图02' }, { id: "aaaa003", url: 'https://images.dog.ceo/breeds/shiba/shiba-3.jpg', title: '狗狗图03' }, { id: "aaaa004", url: 'https://images.dog.ceo/breeds/shiba/shiba-4.jpg', title: '狗狗图04' }, { id: "aaaa005", url: 'https://images.dog.ceo/breeds/shiba/shiba-5.jpg', title: '狗狗图05' }, { id: "aaaa006", url: 'https://images.dog.ceo/breeds/shiba/shiba-6.jpg', title: '狗狗图06' } ]) </script> <style scoped> #news_navigate { display: flex; align-items: center; justify-content: space-around; flex-wrap: wrap; } a { text-decoration: none; color: #fff; background-color: #3498db; padding: 6px 9px; border-radius: 7px; transition: background-color 0.3s, transform 0.3s; font-size: 12px; } a:hover { background-color: #1abc9c; transform: scale(1.05); } #imgBorder { width: 100px; border: 1px black; } </style>
这里只修改了RouterLink
的路径,因为它是发送query
参数的
接收query参数 利用useRoute
函数接收即可
示例:
而接收query则需要在Details.vue
中修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div><img :src="url" alt=""></div> </template> <script lang="ts" setup name="Details"> import { useRoute } from 'vue-router'; import { computed } from 'vue' // hooks let route = useRoute(); const url = computed(() => route.query.url as string); console.log(route.query); // 可以在控制台中看打印的内容,由于是传输的是query参数,所以接收到的也是query </script> <style scoped> img { width: 100px; } </style>
以上代码添加了一个hooks函数useRoute
,这个函数可以接收query
参数
params params
参数格式一般为abc.com/test/a/b/c
,其中a、b、c为传输的参数而不是路径
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <template> <h1>News组件页面</h1> <div id="news_navigate"> <div> <div v-for="(img, index) in imgList" :key="index"> <RouterLink :to="{name:'xiangxi',params:{url:img.url}}">{{ img.title }}</RouterLink> <br> </div> </div> <div id="imgBorder"><RouterView></RouterView></div> </div> </template> <script lang="ts" setup name="News"> import { reactive } from 'vue'; let imgList = reactive([ { id: "aaaa001", url: 'https://images.dog.ceo/breeds/shiba/shiba-1.jpg', title: '狗狗图01' }, { id: "aaaa002", url: 'https://images.dog.ceo/breeds/shiba/shiba-2.jpg', title: '狗狗图02' }, { id: "aaaa003", url: 'https://images.dog.ceo/breeds/shiba/shiba-3.jpg', title: '狗狗图03' }, { id: "aaaa004", url: 'https://images.dog.ceo/breeds/shiba/shiba-4.jpg', title: '狗狗图04' }, { id: "aaaa005", url: 'https://images.dog.ceo/breeds/shiba/shiba-5.jpg', title: '狗狗图05' }, { id: "aaaa006", url: 'https://images.dog.ceo/breeds/shiba/shiba-6.jpg', title: '狗狗图06' } ]) </script> <style scoped> #news_navigate { display: flex; align-items: center; justify-content: space-around; flex-wrap: wrap; } a { text-decoration: none; color: #fff; background-color: #3498db; padding: 6px 9px; border-radius: 7px; transition: background-color 0.3s, transform 0.3s; font-size: 12px; } a:hover { background-color: #1abc9c; transform: scale(1.05); } #imgBorder { width: 100px; border: 1px black; } </style>
注意:
为了不让vue误认为路径中的是子路由路径,所以要到路由文件index.ts
中修改
和query传参不一样,:to
内不能使用path
而是要使用name
index.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { createRouter,createWebHistory } from 'vue-router' import Home from '@/components/Home.vue' import About from '@/components/About.vue' import News from '@/components/News.vue' import Details from '@/components/Details.vue' const router = createRouter ({ history :createWebHistory (), routes :[ { path :'/' , component :Home }, { path :'/home' , component :Home }, { path :'/news' , component :News , children :[ { name :"xiangxi" , path :"details/:url" , component :Details } ] }, { path :'/about' , component :About } ] }) export default router
在子路由中的路径中加上:xxx
表示路径后的为参数而不是子路由路径,并且要给details
路由命名
接收params
也和接收query
类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div><img :src="url" alt=""></div> </template> <script lang="ts" setup name="Details"> import { useRoute } from 'vue-router'; import { computed } from 'vue' // hooks let route = useRoute(); const url = computed(() => route.params.url as string); console.log(route.query); // 可以在控制台中看打印的内容,由于是传输的是query参数,所以接收到的也是query </script> <style scoped> img { width: 100px; } </style>
注意:
传递params
参数时,若使用to
的对象写法,必须使用name
配置项,不能用path
。
传递params
参数时,需要提前在规则中占位。
路由props配置 作用:让路由组件更方便的收到参数(可以将路由参数作为props
传给组件)
写法一 第一种写法只适用于params参数。格式:props:true
在路由文件index.ts
中修改子路由的参数:
1 2 3 4 5 6 { name :"xiangxi" , path :"details/:url" , component :Details , props :true }
然后路由对应的模板组件就可以简化成以下方式:
1 2 3 4 5 6 7 <template> <div><img :src="url" alt=""></div> </template> <script lang="ts" setup name="Details"> defineProps(['url']) </script>
对比之前没有使用props
的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div><img :src="url" alt=""></div> </template> <script lang="ts" setup name="Details"> import { useRoute } from 'vue-router'; import { computed } from 'vue' // hooks let route = useRoute(); const url = computed(() => route.params.url as string); </script>
写法二 第二种写法既可以是query
也可以params
,不过既然存在第一种更加简洁的写法,第二种则可以默认针对query
参数
格式:
1 2 3 props (route ) { return route.query }
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { path :'/news' , component :News , children :[ { name :"xiangxi" , path :"details" , component :Details , props (route ) { return route.query } } ] },
replace属性 作用:控制路由跳转时操作浏览器历史记录的模式
浏览器的历史记录有两种写入方式:分别为push
和replace
push
是追加历史记录(默认值)
replace
是替换当前记录
开启replace
模式:
1 <RouterLink replace .....>XXX</RouterLink>
编程式导航 之前的案例,是通过点击链接,既<RouterLink>
实现跳转。
编程式导航是一种在代码中通过编程方式进行路由跳转的方法,而不是依赖用户的操作来触发路由变化。它在许多场景下非常有用,以下是几个常见的使用场景:
表单提交后跳转 :
用户提交表单后,开发者可能希望在表单验证和处理完成后,将用户重定向到一个新的页面。例如:用户注册成功后,跳转到欢迎页面。
登录验证 :
检查用户是否登录,如果未登录,重定向到登录页面。例如:访问一个需要登录的页面时,如果用户未登录,则编程式导航到登录页面。
多步骤表单 :
在多步骤表单中,根据用户的操作和输入,动态跳转到下一个或上一个步骤。例如:用户填写完第一步后,自动跳转到第二步。
错误处理 :
在处理请求或操作时,如果发生错误,可以编程式导航到错误页面或显示错误信息。例如:用户访问一个不存在的页面时,跳转到404页面。
权限控制 :
根据用户的权限,决定是否允许访问某个页面,如果没有权限,可以跳转到无权限提示页面或首页。例如:管理员权限页面的访问控制。
动态内容加载 :
根据用户的选择动态加载内容并跳转到相应的页面。例如:选择一个分类后,跳转到该分类的详情页面。
在使用编程式导航之前,需要先引入useRouter
:
1 import { useRouter } from 'vue-router';
然后通过调用 useRouter()
这个方法,将当前应用的路由实例赋值给常量 router
1 const router = useRouter();
假设现在有一个按钮,按钮上绑定了一个showDogImg
的方法,此时编程式导航的书写如下:
1 2 3 4 5 function showDogImg (img:ImgInter ) { router.push ( {path :'/news/details' ,query :{url :img.url }} ) }
完整示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <template> <h1>News组件页面</h1> <div id="news_navigate"> <div> <div v-for="(img, index) in imgList" :key="index"> <button @click="showDogImg(img)">点击按钮查看狗狗图</button> <RouterLink :to="{path:'/news/details',query:{url:img.url}}">{{ img.title }}</RouterLink> <br> </div> </div> <div id="imgBorder"><RouterView></RouterView></div> </div> </template> <script lang="ts" setup name="News"> import { reactive } from 'vue'; import { useRouter } from 'vue-router'; const router = useRouter(); let imgList = reactive([ ... ]) interface ImgInter{ id:string, url:string, title:string } function showDogImg(img:ImgInter) { // 这里如果是TS会报类型错误,可以用img:any或者接口规范类型 router.push( {path:'/news/details',query:{url:img.url}} ) } </script>
路由重定向 将指定的路径跳转到另一个路径
在之前编写路由器的时候,为了防止第一次输入网页不会自动跳转到首页,进行了如下操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const router = createRouter ({ history :createWebHistory (), routes :[ { path :'/' , component :Home }, { path :'/home' , component :Home }, { path :'/news' , component :News , children :[ { name :"xiangxi" , path :"details" , component :Details , props (route ) { return route.query } } ] }, { path :'/about' , component :About } ] })
现在有了更好的解决方案,既路由重定向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const router = createRouter ({ history :createWebHistory (), routes :[ { path :'/home' , component :Home }, { path :'/news' , component :News , children :[ { name :"xiangxi" , path :"details" , component :Details , props (route ) { return route.query } } ] }, { path :'/about' , component :About }, { path :'/' , redirect :'/home' } ] })
在 Vue Router 中,meta
属性用于存储与路由相关的自定义数据。这些数据不会影响路由的实际行为,但可以在导航守卫、组件中访问和使用。这使得 meta
属性非常适合用于存储如权限信息、页面标题、布局选项等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const router = createRouter ({ history : createWebHistory (), routes : [ { path : '/' , component : loginPage, meta : { centered : false } }, { path : '/login' , component : loginPage, meta : { centered : false } }, { path : '/home' , component : homePage, meta : { centered : true }, children : [ { path : 'camList' , component : camList, }, { path : '' , component : atHome, }, { path : 'atHome' , component : atHome, }, { path : 'monitorRealTime' , component : monitorRealTime, } ], } ] });
Pinia Pinia 是 Vue 的一个状态管理库,用于管理和共享应用程序中的全局状态
。它是 Vuex 的替代方案,提供了更简单、更直观的 API 和更好的性能。Pinia 可以帮助开发人员在多个组件之间共享数据,同时保持代码的清晰和可维护性。
以下是 Pinia 的一些主要功能和特点:
简单的 API :Pinia 提供了直观易懂的 API,简化了状态管理的过程。
模块化 :可以将状态分成不同的模块,每个模块独立管理自己的状态、动作和变更。
TypeScript 支持 :Pinia 对 TypeScript 有很好的支持,能够提供类型推断和类型安全。
插件支持 :Pinia 支持插件,允许开发人员扩展其功能,例如持久化状态、日志记录等。
SSR 支持 :Pinia 支持服务端渲染,可以在服务端和客户端之间共享状态。
测试框架 在使用之前,先搭建好测试的框架:
App.vue:
1 2 3 4 5 6 7 8 9 <template> <Count/> <Talk/> </template> <script lang="ts" setup name="" > import Count from './components/Count.vue'; import Talk from './components/Talk.vue'; </script>
以上代码表示还要创建两个组件Count
和Talk
Talk.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <template> <div> <ul v-for="(talk, index) in talkList" :key="index"> <li>{{ talk.content }}</li> </ul> </div> <button @click="addPoem">添加诗句</button> </template> <script lang="ts" setup name="Talk"> import axios from 'axios'; import { nanoid } from 'nanoid'; import { reactive } from 'vue'; const url = 'https://v1.jinrishici.com/rensheng.txt'; let talkList = reactive([ { id: "asdafdasfxz", content: '归志宁无五亩园,读书本意在元元。' }, { id: "asdgituwsad", content: '人老去西风白发,蝶愁来明日黄花。' }, { id: "sjiasdfjasa", content: '能令暂开霁,过是吾无求。' } ]) async function addPoem() { let result = await axios.get('https://v1.jinrishici.com/rensheng.txt'); console.log(result); let obj = {id:nanoid(),content:result.data}; talkList.push(obj); } </script>
Count.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div> <h2>当前数字累计为:{{ sum }}</h2> <select v-model.number="num"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="add">点击增加数字</button> <button @click="sub">点击减小数字</button> </div> </template> <script lang="ts" setup name="Count" > import { ref } from 'vue'; let sum = ref(0); // 累计数 let num = ref(1); // 选择数 // 增加数字方法 function add() { sum.value += num.value; } // 减小数字方法 function sub() { sum.value -= num.value; } </script>
样式可以自行设计
Pinia教程 第一步需要安装Pinia
第二步在main.ts中修改代码
1 2 3 4 5 6 7 8 9 10 11 12 import { createApp } from 'vue' import App from './App.vue' import { createPinia } from 'pinia' const pinia = createPinia ();createApp (App ).use (pinia).mount ('#app' )
验证是否安装成功需要在浏览器开发者模式中的vue devtools
查看,如果有一个菠萝的logo就表明安装并且引入成功。
数据存储 将之前测试框架中,talk.vue
的talkList
和count.vue
的sum
存储到pinia
中。
首先在src目录下创建一个store的文件夹,并且在该文件夹下创建对应的talk.ts
和count.ts
talk.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { defineStore } from "pinia" ;export const useTalkStore = defineStore ('talkList' ,{ state ( ) { return { talkList :[ { id : "asdafdasfxz" , content : '归志宁无五亩园,读书本意在元元。' }, { id : "asdgituwsad" , content : '人老去西风白发,蝶愁来明日黄花。' }, { id : "sjiasdfjasa" , content : '能令暂开霁,过是吾无求。' } ] } } })
count.ts:
1 2 3 4 5 6 7 8 9 10 import { defineStore } from "pinia" ;export const useCountStore = defineStore ('count' ,{ state ( ) { return { sum :0 } } })
根据以上案例,总结出以下步骤:
引入defineStore
使用defineStore
,参数第一个表示名称,参数第二个表示配置
向外暴露存储的数据
使用数据 在talk.vue
和count.vue
中引入暴露的数据
1 import { useTalkStore } from '@/store/talk' ;
1 import { useCountStore } from '@/store/count' ;
然后用变量接收数据:
1 const CountStore = useCountStore ();
1 const TalkStore = useTalkStore ();
使用数据:
完整案例如下:
Count.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <template> <div> <h2>当前数字累计为:{{ CountStore.sum }}</h2> <select v-model.number="num"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="add">点击增加数字</button> <button @click="sub">点击减小数字</button> </div> </template> <script lang="ts" setup name="Count" > import { ref } from 'vue'; import { useCountStore } from '@/store/count'; const CountStore = useCountStore(); let num = ref(1); // 选择数 // 增加数字方法 function add() { CountStore.sum += num.value; } // 减小数字方法 function sub() { CountStore.sum -= num.value; } </script>
Talk.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div> <ul v-for="(talk, index) in TalkStore.talkList" :key="index"> <li>{{ talk.content }}</li> </ul> </div> <button @click="addPoem">添加诗句</button> </template> <script lang="ts" setup name="Talk"> import axios from 'axios'; import { nanoid } from 'nanoid'; import { reactive } from 'vue'; import { useTalkStore } from '@/store/talk'; const url = 'https://v1.jinrishici.com/rensheng.txt'; const TalkStore = useTalkStore(); async function addPoem() { let result = await axios.get('https://v1.jinrishici.com/rensheng.txt'); console.log(result); let obj = {id:nanoid(),content:result.data}; TalkStore.talkList.push(obj); } </script>
修改数据 第一种方式 在之前的案例中,已经呈现了修改数据的第一种方式——直接调用修改:
1 2 3 4 5 6 7 8 9 10 const CountStore = useCountStore ();function add ( ) { CountStore .sum += num.value ; } function sub ( ) { CountStore .sum -= num.value ; }
1 2 3 4 5 6 7 const TalkStore = useTalkStore ();async function addPoem ( ) { let result = await axios.get ('https://v1.jinrishici.com/rensheng.txt' ); console .log (result); let obj = {id :nanoid (),content :result.data }; TalkStore .talkList .push (obj); }
第二种方式 第二种方式通常用于批量修改数据:
为了展示后面修改数据的特殊,将store文件夹中的count.ts进行稍微的修改:
增加了两个数据name
和age
1 2 3 4 5 6 7 8 9 10 11 12 import { defineStore } from "pinia" ;export const useCountStore = defineStore ('count' ,{ state ( ) { return { sum :0 , name :'张三' , age :20 } } })
触发Count.vue
的add()
方法,修改存储的数据值
name
的zhangsan
改为lisi
age
改为15
sum改为99999
使用第二种方法:
1 2 3 4 5 6 7 8 9 const CountStore = useCountStore ();function add ( ) { CountStore .$patch({ name :'lisi' , age :15 , sum :99999 }) }
第三种方式 第三种方式修改数据的位置在pinia
的文件中,使用到了actions
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { defineStore } from "pinia" ;export const useCountStore = defineStore ('count' ,{ actions :{ change (value:number ){ this .sum += value; this .name = 'lisi' ; this .age = 15 } }, state ( ) { return { sum :0 , name :'张三' , age :20 } } })
相当于存储数据的地方又存储了一个修改数据的方法
而使用该函数的方法就是和第一种方式直接调用一样:
1 2 3 4 5 const CountStore = useCountStore ();function add ( ) { CountStore .change (num.value ) }
小结 根据以上的所有示例,可以总结出:
pinia的功能就是存储全局变量,全局方法。(但全局变量是响应式的)
修改存储的数据,如果只是简单的修改,直接使用第一种方式,如果相对复杂,推荐使用第三种方式
小小的优化 在之前的案例中,每次想要获得数据,都需要用XXX.xxx
,这样写逻辑上虽然没问题,但是观感相对较差。
在之前,解决的方法是利用解构赋值:
1 2 const CountStore = useCountStore ();const {sum,name,age} = CountStore ;
但是问题就出现了,数据丢失了响应性,此前也有解决该问题的办法,就是使用toRefs
方法
1 const {sum,name,age} = toRefs (CountStore );
这么修改,虽然问题解决了,数据又恢复了响应式,但是在控制台中查看toRefs(CountStore)
,发现CountStore
中的所有内容全被添加上了响应式,即使不需要的内容也会被添加上响应式。
针对以上的问题,pinia
有对应的办法:
引入storeToRefs
1 import { storeToRefs } from 'pinia' ;
使用storeToRefs
1 const {sum,name,age} = storeToRefs (CountStore );
总结:解构赋值时,对于pinia
的数据要用storeToRefs
保证不会丢失响应性
getters pinia
的getters
类似于vue
中的computed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { defineStore } from "pinia" ;export const useCountStore = defineStore ('count' ,{ actions :{ change (value:number ){ this .sum += value; this .name = 'lisi' ; this .age = 15 } }, state ( ) { return { sum :0 , name :'张三' , age :20 } }, getters :{ bigSum ():number { return this .sum * 10 ; } } })
使用:
1 <h3 > 数字放大10倍为:{{ CountStore.bigSum }}</h3 >
subscribe 可以看作是vue
中的watch
具体使用方法如下:
1 2 3 4 5 6 CountStore .$subscribe((mutate,state ) => { console .log ("CountStore被修改了" ); console .log (mutate); console .log (state); console .log ("-------------------" ); })
store的组合式写法 原来的选项式写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { defineStore } from "pinia" ;export const useCountStore = defineStore ('count' ,{ actions :{ change (value:number ){ this .sum += value; this .name = 'lisi' ; this .age = 15 } }, state ( ) { return { sum :0 , name :'张三' , age :20 } }, getters :{ bigSum ():number { return this .sum * 10 ; } } })
修改成组合式写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { defineStore } from "pinia" ;import { ref } from "vue" ;export const useCountStore = defineStore ('count' ,() => { let sum = ref (0 ); let name = ref ('张三' ); let age = ref (20 ) function change (value:number ){ sum.value += value; name.value = 'lisi' ; age.value = 15 } return {sum,name,age,change} })
组件通信 在Vue3中,有多种方式可以用于组件之间的通信,每种方式都有其适用的场景和优缺点。以下是一些主要的组件通信方式:
Props 和 Emit :
Props :父组件通过props向子组件传递数据。
Emit :子组件通过emit向父组件发送事件或数据。
Provide 和 Inject :
Provide :祖先组件提供数据或方法。
Inject :后代组件注入并使用提供的数据或方法。适用于跨越多层组件的场景。
全局事件总线 :
使用一个空的Vue实例作为中央事件总线,通过$emit
和$on
在非父子关系的组件之间传递消息。
Vuex :
Vuex是Vue的状态管理库,用于管理全局状态。通过store,任何组件都可以访问和修改状态。
Composition API :
使用组合式API中的ref
、reactive
和computed
等,可以在多个组件中共享状态和逻辑。例如,自定义hook(composables)。
Pinia :
Pinia是Vue3的状态管理库,作为Vuex的替代方案,提供了更简单和更高性能的状态管理。
EventEmitter :
可以使用Node.js的EventEmitter或者其他第三方库来实现组件之间的事件通信。
通过浏览器的LocalStorage、SessionStorage或Cookies :
用于持久化和共享数据,不过这种方式主要用于在不同组件之间保留状态,而不是实时通信。
URL查询参数或路由参数 :
Props 和 Emit 在Vue3中,父组件和子组件之间的通信主要通过Props和Emit来实现。以下是一个简单的示例,展示了如何使用Props和Emit在父子组件之间传递数据和事件
父组件示例(ParentComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div> <ChildComponent :message="parentMessage" @updateMessage="updateMessage"></ChildComponent> <p>来自子组件的消息: {{ childMessage }}</p> </div> </template> <script lang="ts" setup> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue'; const parentMessage = '这是来自父组件的消息'; const childMessage = ref(''); function updateMessage(newMessage: string) { childMessage.value = newMessage; } </script>
子组件示例(ChildComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <div> <p>父组件消息: {{ message }}</p> <input v-model="inputMessage" placeholder="输入新的消息"> <button @click="sendMessage">发送消息给父组件</button> </div> </template> <script lang="ts" setup> import { defineProps, defineEmits, ref } from 'vue'; // 定义Props const props = defineProps<{ message: string; }>(); // 定义Emits const emit = defineEmits<{ (e: 'updateMessage', newMessage: string): void; }>(); const inputMessage = ref(''); function sendMessage() { emit('updateMessage', inputMessage.value); } </script>
解释
父组件(ParentComponent.vue) :
通过parentMessage
传递数据到子组件的message
属性。
监听子组件的updateMessage
事件,并使用updateMessage
方法更新childMessage
。
子组件(ChildComponent.vue) :
使用defineProps
定义message
属性,接收来自父组件的消息。
使用defineEmits
定义updateMessage
事件,以便将新的消息发送给父组件。
用户在输入框输入新消息并点击按钮时,调用sendMessage
方法,触发updateMessage
事件并将新消息传递给父组件。
Mitt Mitt 是一个轻量级的事件发射器,用于在 Vue 3 中实现组件之间的通信,特别适用于兄弟组件 之间的通信
首先,安装 Mitt:
创建事件总线
1 2 3 4 5 import mitt from 'mitt' ;const emitter = mitt ();export default emitter;
在需要通信的组件中使用事件总线
发送事件的组件(SenderComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 <template> <button @click="sendMessage">发送消息</button> </template> <script lang="ts" setup> import emitter from '../eventBus'; function sendMessage() { emitter.emit('customEvent', 'Hello from SenderComponent'); } </script>
接收事件的组件(ReceiverComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <p>{{ message }}</p> </template> <script lang="ts" setup> import { ref, onMounted, onUnmounted } from 'vue'; import emitter from '../eventBus'; const message = ref(''); function handleMessage(payload: string) { message.value = payload; } onMounted(() => { emitter.on('customEvent', handleMessage); }); onUnmounted(() => { emitter.off('customEvent', handleMessage); }); </script>
解释
创建事件总线 :
使用 Mitt 创建一个事件总线实例 emitter
,并将其导出以供其他组件使用。
发送事件的组件 :
在 SenderComponent.vue
中,使用 emitter.emit
方法发送一个自定义事件 customEvent
,并附带消息数据。
接收事件的组件 :
在 ReceiverComponent.vue
中,定义一个响应事件的处理函数 handleMessage
,并在组件挂载时通过 emitter.on
监听 customEvent
事件。
在组件卸载时,通过 emitter.off
移除事件监听,以避免内存泄漏。
v-model v-model
是 Vue 的双向数据绑定指令,用于将数据在组件之间进行同步。
在Vue3中,v-model
可以用在父组件和子组件 之间传递和同步数据。以下是一个简单的示例,展示了如何使用 v-model
进行父子组件通信。
父组件示例(ParentComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div> <ChildComponent v-model:message="parentMessage"></ChildComponent> <p>来自子组件的消息: {{ parentMessage }}</p> </div> </template> <script lang="ts" setup> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue'; const parentMessage = ref('这是来自父组件的消息'); </script>
子组件示例(ChildComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div> <p>父组件消息: {{ modelValue }}</p> <input v-model="inputMessage" placeholder="输入新的消息"> <button @click="updateMessage">发送消息给父组件</button> </div> </template> <script lang="ts" setup> import { ref } from 'vue'; import { defineProps, defineEmits } from 'vue'; // 定义Props const props = defineProps<{ modelValue: string; }>(); // 定义Emits const emit = defineEmits<{ (e: 'update:modelValue', newMessage: string): void; }>(); const inputMessage = ref(props.modelValue); function updateMessage() { emit('update:modelValue', inputMessage.value); } </script>
解释
父组件(ParentComponent.vue) :
使用 v-model:message
将父组件的 parentMessage
绑定到子组件的 modelValue
属性。
parentMessage
的变化将自动反映在子组件中,反之亦然。
子组件(ChildComponent.vue) :
使用 defineProps
定义 modelValue
属性,接收来自父组件的消息。
使用 defineEmits
定义 update:modelValue
事件,以便将新的消息发送给父组件。
用户在输入框中输入新消息并点击按钮时,调用 updateMessage
方法,触发 update:modelValue
事件并将新消息传递给父组件。
$attrs $attrs
经常用于祖孙组件 之间的通信,特别是当祖先组件需要将一些属性直接传递给孙组件,而中间的子组件不需要处理这些属性时 。这样可以避免逐级传递 props
,使代码更简洁。
祖先组件(GrandParentComponent.vue):
1 2 3 4 5 6 7 8 9 10 <template> <div> <ParentComponent :title="grandParentTitle"></ParentComponent> </div> </template> <script lang="ts" setup> import ParentComponent from './ParentComponent.vue'; const grandParentTitle = '这是来自祖先组件的标题'; </script>
父组件(ParentComponent.vue):
1 2 3 4 5 6 7 8 9 <template> <div> <ChildComponent v-bind="$attrs"></ChildComponent> </div> </template> <script lang="ts" setup> import ChildComponent from './ChildComponent.vue'; </script>
孙组件(ChildComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div> <p>{{ title }}</p> </div> </template> <script lang="ts" setup> import { defineProps } from 'vue'; const props = defineProps<{ title: string; }>(); </script>
解释
祖先组件(GrandParentComponent.vue) :
定义一个属性 grandParentTitle
并将其传递给 ParentComponent
。
父组件(ParentComponent.vue) :
使用 v-bind="$attrs"
将所有未声明的属性传递给 ChildComponent
。
孙组件(ChildComponent.vue) :
使用 defineProps
接收 title
属性,并在模板中显示。
provide-inject provide
和 inject
是一种用于组件间传递依赖的机制,特别适用于跨越多层级组件的通信。这种机制允许祖先组件为其后代组件提供数据或方法,而不需要逐级传递props
provide
和 inject
的作用
provide :
祖先组件使用 provide
方法提供数据或方法,供后代组件使用。
通常在组件的 setup
函数中调用 provide
。
inject :
后代组件使用 inject
方法注入并使用祖先组件提供的数据或方法。
通常在组件的 setup
函数中调用 inject
。
示例:
GrandParent.vue:
1 2 3 4 5 6 7 8 9 10 11 12 <template> <div> <ParentComponent></ParentComponent> </div> </template> <script lang="ts" setup> import { provide } from 'vue'; const providedData = '这是来自祖先组件的数据'; provide('sharedData', providedData); </script>
Parent.vue:
1 2 3 4 5 6 7 8 9 <template> <div> <ChildComponent></ChildComponent> </div> </template> <script lang="ts" setup> import ChildComponent from './ChildComponent.vue'; </script>
Child.vue:
1 2 3 4 5 6 7 8 9 10 11 <template> <div> <p>注入的数据: {{ sharedData }}</p> </div> </template> <script lang="ts" setup> import { inject } from 'vue'; const sharedData = inject<string>('sharedData', '默认数据'); </script>
插槽 默认插槽 默认插槽(Default Slot
)是指在子组件中未指定名称的插槽内容。它允许父组件将内容插入到子组件的预定义位置,从而实现更灵活的组件内容配置。默认插槽是最基础也是最常用的插槽类型。
子组件(ChildComponent.vue):
1 2 3 4 5 6 7 8 9 <template> <div> <h2>我是子组件</h2> <slot>默认插槽内容</slot> </div> </template> <script lang="ts" setup> </script>
父组件(ParentComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> <div> <ChildComponent> <p>这是插入到子组件中的内容1</p> </ChildComponent> <ChildComponent> <p>这是插入到子组件中的内容2</p> </ChildComponent> </div> </template> <script lang="ts" setup> import ChildComponent from './ChildComponent.vue'; </script>
解释
子组件 :
使用 <slot>
标签定义插槽,表示这个位置的内容可以由父组件提供。
如果父组件没有提供内容,则会显示 slot
标签内的默认内容(例如 “默认插槽内容”)。
父组件 :
使用子组件 <ChildComponent>
时,在其标签内放置需要插入的内容(例如 <p>这是插入到子组件中的内容</p>
)。
这些内容将被插入到子组件的 <slot>
标签位置,替换默认内容。
具名插槽 具名插槽(Named Slots)是Vue中一种更高级的插槽类型,它允许为插槽指定一个名称,从而在父组件中可以更精确地将内容插入到子组件的特定位置。
子组件(ChildComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div> <header> <slot name="header">默认头部内容</slot> </header> <main> <slot>默认主体内容</slot> </main> <footer> <slot name="footer">默认尾部内容</slot> </footer> </div> </template> <script lang="ts" setup> </script>
父组件(ParentComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template> <div> <ChildComponent> <template #header> <h1>这是插入到头部的内容</h1> </template> <template #default> <p>这是插入到主体的内容</p> </template> <template #footer> <p>这是插入到尾部的内容</p> </template> </ChildComponent> </div> </template> <script lang="ts" setup> import ChildComponent from './ChildComponent.vue'; </script>
解释
子组件(ChildComponent.vue) :
使用 <slot name="header">
和 <slot name="footer">
定义具名插槽,允许父组件插入特定内容到这些位置。
使用 <slot>
定义默认插槽,允许父组件插入默认主体内容。
父组件(ParentComponent.vue) :
使用 <template #header>
插入内容到子组件的头部插槽。
使用 <template #default>
插入内容到子组件的默认插槽。
使用 <template #footer>
插入内容到子组件的尾部插槽。
作用域插槽 作用域插槽(Scoped Slots)是Vue中的一种强大功能,允许子组件将数据传递回父组件,从而使父组件能够在插槽内容中使用这些数据。与普通插槽不同,作用域插槽不仅仅是内容插入,还能实现数据共享和逻辑解耦。
示例 假设有一个列表组件(ListComponent),它接收一个数组并通过插槽渲染每个项。
子组件(ListComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div> <slot :items="items"></slot> </div> </template> <script lang="ts" setup> import { defineProps } from 'vue'; const props = defineProps<{ items: Array<{ id: number; name: string; }>; }>(); </script>
父组件(ParentComponent.vue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template> <div> <ListComponent :items="items"> <template #default="{ items }"> <ul> <li v-for="item in items" :key="item.id"> {{ item.name }} </li> </ul> </template> </ListComponent> </div> </template> <script lang="ts" setup> import { ref } from 'vue'; import ListComponent from './ListComponent.vue'; const items = ref([ { id: 1, name: '苹果' }, { id: 2, name: '香蕉' }, { id: 3, name: '橘子' } ]); </script>
解释
子组件(ListComponent.vue) :
接收一个 items
数组作为 props
。
使用 <slot :items="items"></slot>
传递 items
数据给插槽内容。这里的 :items="items"
是为插槽提供的数据,父组件可以通过插槽接收这些数据。
父组件(ParentComponent.vue) :
使用 ListComponent
并传递 items
数据。
使用作用域插槽 template #default="{ items }"
接收来自子组件的数据,并在插槽内容中使用这些数据。
在插槽内容中,通过 v-for
循环渲染列表项。
优势
灵活性 :父组件可以完全控制如何渲染插槽内容,同时还可以使用子组件提供的数据。
解耦 :子组件不需要关心插槽内容的具体实现,只需提供数据即可。
重用性 :同一个子组件可以在不同的父组件中以不同的方式渲染插槽内容,从而提高了组件的重用性。
重要的API shallowRef与shallowReactive shallowRef
和 shallowReactive
是 Vue 3 中用于创建浅层响应式状态的方法,它们与 ref
和 reactive
有些类似,但有一些关键区别。
shallowRef
和 shallowReactive
的作用
shallowRef
:
创建一个浅层响应的引用。当改变其值时,Vue 会追踪这个改变,但对其内部对象的变动不会进行深层次的响应式追踪。
shallowReactive
:
创建一个浅层响应的对象。这个对象的顶层属性会是响应式的,但内部嵌套对象的属性不会进行深层次的响应式追踪。
示例 shallowRef
示例
1 2 3 4 5 6 7 8 9 import { shallowRef } from 'vue' ;const shallowValue = shallowRef ({ name : 'Alice' });shallowValue.value = { name : 'Bob' }; shallowValue.value .name = 'Charlie' ;
shallowReactive
示例:
1 2 3 4 5 6 7 8 9 import { shallowReactive } from 'vue' ;const shallowObj = shallowReactive ({ name : 'Alice' , info : { age : 25 } });shallowObj.name = 'Bob' ; shallowObj.info .age = 26 ;
区别
深度响应式 vs 浅层响应式 :
ref
和 reactive
会进行深层响应式追踪,意味着它们不仅仅追踪顶层属性的变化,还会追踪嵌套对象属性的变化。
shallowRef
和 shallowReactive
只会追踪顶层属性的变化,不会对嵌套对象属性的变化进行响应式处理。
使用场景 :
ref
和 reactive
适用于需要深度响应式的数据结构,适用于大多数常见场景。
shallowRef
和 shallowReactive
适用于不需要深层响应式的情况,可以提高性能和减少不必要的响应式开销。
readonly与shallowReadonly readonly
:当使用 readonly
时,它会使整个对象成为只读的。意味不能对对象的任何属性进行修改,包括添加、删除或更改属性。这对于确保对象在整个生命周期内保持不变非常有用。
示例:
1 2 3 4 5 6 7 8 9 10 import { readonly } from 'vue' ;const obj = readonly ({ name : 'John' , age : 30 });
shallowReadonly
:当使用 shallowReadonly
时,它会使对象的顶层属性成为只读的,但不会阻止嵌套对象的修改。意味着可以修改嵌套对象的属性,但不能修改顶层对象的属性。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { shallowReadonly } from 'vue' ;const obj = shallowReadonly ({ name : 'John' , address : { city : 'New York' , zip : '10001' } });
toRaw与markRaw toRaw
是一个内部方法,用于获取一个响应式对象的原始(非响应式)版本。这个方法通常在开发过程中不会直接使用,因为它会破坏响应式系统的功能。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 import { reactive, toRaw } from 'vue' ;const state = reactive ({ count : 0 }); const rawState = toRaw (state);rawState.count = 10 ; console .log (state.count ); console .log (rawState.count );
markRaw
是 Vue 3 中的一个方法,用于标记一个对象或属性为原始(非响应式)的。这意味着,通过 markRaw
标记的对象或属性将不再被 Vue 的响应式系统跟踪和观察变化。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 import { markRaw } from 'vue' ;const state = { count : 0 }; markRaw (state.count );state.count = 10 ; console .log (state.count );
customRef customRef
是 Vue 3 中的一个方法,用于创建一个自定义的响应式引用(Ref)。它允许在组件内部创建一个响应式引用,并且可以在组件外部访问和修改这个引用的值。
这个方法通常用于复杂的场景,例如需要在组件外部访问或修改组件内部的响应式状态。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <template> <div> <button @click="increment">增加</button> <button @click="decrement">减少</button> <p>计数器值:{{ counter }}</p> </div> </template> <script lang="ts" setup> import { ref, customRef } from 'vue'; const internalCounter = ref(0); const counter = customRef((get, set) => { return { get: () => internalCounter.value, set: (newValue) => { internalCounter.value = newValue; } }; }); function increment() { counter.value++; } function decrement() { counter.value--; } </script>
customRef
的 track
和 trigger
是两个用于自定义响应式引用的方法。这些方法允许更精细地控制响应式系统的行为。
track track
方法用于跟踪一个对象或属性的变化。当调用 track
时,Vue 的响应式系统会开始监控该对象或属性的变化。这对于需要在响应式系统中触发更新非常有用。
trigger trigger
方法用于触发一个响应式对象的更新。当调用 trigger
时,Vue 的响应式系统会重新计算所有依赖于该对象或属性的组件。这对于需要手动触发更新非常有用。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <template> <div> <button @click="increment">增加</button> <button @click="decrement">减少</button> <p>计数器值:{{ counter }}</p> </div> </template> <script lang="ts" setup> import { ref, customRef, track, trigger } from 'vue'; const internalCounter = ref(0); const counter = customRef((get, set) => { return { get: () => internalCounter.value, set: (newValue) => { internalCounter.value = newValue; trigger(internalCounter); } }; }); track(internalCounter); function increment() { counter.value++; } function decrement() { counter.value--; } </script>
teleport teleport
是一个用于将组件的内容插入到 DOM 的外部位置的指令。这个指令非常有用,因为它允许在组件内部定义内容,但将其渲染到页面的任何地方。
用法 teleport
的用法非常简单。你只需要在组件的模板中使用 <teleport>
标签,并指定一个目标元素的 to
属性,这个目标元素的 id
将被 teleport
使用。
示例:
1 2 3 4 5 6 7 <template> <div> <teleport to="body"> <div>这是被传送的内容</div> </teleport> </div> </template>
正常情况下,这个组件是作为App.vue
的子组件渲染到父组件的范围中
但是在这个示例中,<teleport to="body">
将会将 div
元素插入到页面的 <body>
元素中。
优点
灵活性 :可以将组件的内容插入到页面的任何地方,而不需要担心组件的位置。
简单 :使用 teleport
只需要简单的标签和属性,非常方便。
注意事项
目标元素 :确保指定的目标元素存在,并且它的 id
是唯一的。
组件渲染顺序 :teleport
插入的内容会在目标元素的子节点中,所以它的渲染顺序可能会影响页面的布局。
suspense Suspense
是一个用于处理组件加载状态的功能。它允许在组件加载期间显示一个占位符,并在组件加载完成后自动替换为实际内容。这对于处理慢加载组件或异步数据非常有用。
不使用suspense 举例:
App.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <template> <div id="fu"> <h2>我是父组件</h2> <Child></Child> </div> </template> <script lang="ts" setup name="" > import Child from './components/Child.vue'; </script> <style scoped> #fu { width: 500px; height: 200px; background-color: #ddd; border-radius: 20px; box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3); padding: 20px; font-family: Arial, sans-serif; color: #333; text-align: center; display: flex; /* 添加 Flexbox */ flex-direction: column; justify-content: center; /* 水平居中 */ align-items: center; /* 垂直居中 */ } </style>
Child.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <h2 id="zi">我是子组件</h2> </template> <script lang="ts" setup name="" > import axios from 'axios'; let {data:{content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json'); console.log(content); </script> <style scoped> #zi { width: 400px; height: 100px; background-color: #f0f0f0; border-radius: 15px; box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3); padding: 20px; font-family: Arial, sans-serif; color: #333; text-align: center; } </style>
解释:
在App.vue
中,包含了子组件Child.vue
在Child.vue
中,使用了Axios
来异步获取数据
结果:
控制台可以正常输出数据
页面无法渲染出Child.vue
使用suspense 为了在App.vue
中正常渲染子组件,可以利用suspense
修改后的App.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <template> <div id="fu"> <h2>我是父组件</h2> <Suspense> <template v-slot:default> <Child></Child> </template> <template v-slot:fallback> 加载中... </template> </Suspense> </div> </template> <script lang="ts" setup name="" > import { Suspense } from 'vue'; import Child from './components/Child.vue'; </script> <style scoped> #fu { width: 500px; height: 200px; background-color: #ddd; border-radius: 20px; box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3); padding: 20px; font-family: Arial, sans-serif; color: #333; text-align: center; display: flex; /* 添加 Flexbox */ flex-direction: column; justify-content: center; /* 水平居中 */ align-items: center; /* 垂直居中 */ } </style>
解释:
引入suspense
:
1 import { Suspense } from 'vue' ;
用suspense
包裹住要异步渲染的子组件
利用插槽将子组件标签包裹起来
v-slot:default
表示的是默认,用于展示子组件,v-slot:fallback
表示在异步请求完成前的操作(信息)
全局API转移到应用对象 app.component 全局组件
是指可以在应用的任何地方使用的组件,而不需要每次在本地引用。
步骤
定义组件 : 创建一个新的 Vue 组件文件,例如 MyComponent.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> <div class="my-component"> 我是一个全局组件 </div> </template> <script lang="ts" setup> </script> <style scoped> .my-component { color: blue; } </style>
注册全局组件 : 在你的 main.ts
文件中,将这个组件注册为全局组件。
1 2 3 4 5 6 7 8 9 import { createApp } from 'vue' ;import App from './App.vue' ;import MyComponent from './components/MyComponent.vue' ; const app = createApp (App );app.component ('MyComponent' , MyComponent ); app.mount ('#app' );
使用全局组件 : 现在就可以在应用的任何地方使用这个全局组件,而不需要在本地引用。例如,在 App.vue
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div id="app"> <h1>欢迎使用Vue 3</h1> <MyComponent></MyComponent> <!-- 使用全局组件 --> </div> </template> <script lang="ts" setup> </script> <style scoped> #app { font-family: Avenir, Helvetica, Arial, sans-serif; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
app.config app.config
是一个用于配置 Vue 应用的对象。通过 app.config
,你可以设置一些全局配置项,来影响整个应用的行为。下面是一些常见的 app.config
选项及其使用方法:
常见的 app.config
选项
app.config.globalProperties
: 这个选项用于在全局属性中注册自定义的全局方法或变量,以便在整个应用中访问。
app.config.errorHandler
: 自定义全局错误处理器,以处理应用中的错误。
app.config.warnHandler
: 自定义全局警告处理器,以处理 Vue 产生的警告。
app.config.isCustomElement
: 自定义判断某个标签是否是自定义元素。
使用示例 下面是一个示例,展示如何使用这些配置选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { createApp } from 'vue' ;import App from './App.vue' ;const app = createApp (App );app.config .globalProperties .$myGlobalMethod = () => { console .log ('这是一个全局方法' ); }; app.config .errorHandler = (err, instance, info ) => { console .error ('全局错误处理器:' , err, instance, info); }; app.config .warnHandler = (msg, instance, trace ) => { console .warn ('全局警告处理器:' , msg, instance, trace); }; app.config .isCustomElement = (tag ) => { return tag.startsWith ('my-' ); }; app.mount ('#app' );
解释
app.config.globalProperties
: 在全局属性中注册一个自定义方法 $myGlobalMethod
,可以在任何组件中通过 this.$myGlobalMethod()
访问这个方法。
app.config.errorHandler
: 设置一个全局错误处理器,当应用中发生错误时,会调用这个处理器,输出错误信息。
app.config.warnHandler
: 设置一个全局警告处理器,当 Vue 产生警告时,会调用这个处理器,输出警告信息。
app.config.isCustomElement
: 设置一个判断函数,用于判断某个标签是否是自定义元素。在这个示例中,所有以 my-
开头的标签都会被认为是自定义元素。
app.directive app.directive
方法来定义和注册自定义指令。自定义指令允许在 DOM 元素上应用自定义行为,这些指令可以在应用的任何地方使用,非常强大和灵活。
使用步骤
定义自定义指令 : 创建一个自定义指令对象,并定义它的钩子函数,如 mounted
、updated
等。
注册全局自定义指令 : 在 main.ts
文件中使用 app.directive
方法将自定义指令注册为全局指令。
在模板中使用自定义指令 : 在组件的模板中应用自定义指令。
示例 创建一个简单的自定义指令 v-focus
,它会在元素挂载时自动获得焦点。
定义和注册自定义指令 main.ts :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { createApp } from 'vue' ;import App from './App.vue' ;const app = createApp (App );const vFocus = { mounted (el: HTMLElement ) { el.focus (); } }; app.directive ('focus' , vFocus); app.mount ('#app' );
在模板中使用自定义指令 App.vue :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div id="app"> <h1>欢迎使用Vue 3</h1> <input v-focus placeholder="自动获得焦点的输入框"> </div> </template> <script lang="ts" setup> </script> <style scoped> #app { font-family: Avenir, Helvetica, Arial, sans-serif; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
解释
定义自定义指令 : 在 main.ts
文件中创建一个自定义指令对象 vFocus
,并定义 mounted
钩子函数,让元素在挂载时自动获得焦点。
注册全局自定义指令 : 使用 app.directive('focus', vFocus)
将 vFocus
注册为全局自定义指令 v-focus
。
在模板中使用自定义指令 : 在 App.vue
的模板中,使用 v-focus
自定义指令,使得输入框在页面加载时自动获得焦点。
非兼容性改变 来自:非兼容性改变 | Vue 3 迁移指南
Vue 3 引入了一些重要的非兼容性变化,这些变化在从 Vue 2 迁移时需要特别注意。以下是一些主要的非兼容性变化:
全局 API 变化 :
全局 Vue API 现在使用应用实例来调用,而不是全局对象。
全局和内部 API 被重新构建,以支持 tree-shaking。
模板指令变化 :
v-model
在组件中的使用方式被重新设计,现在使用 v-bind.sync
替代。
<template v-for>
和非 v-for
节点的 key
使用方式有所变化。
v-if
和 v-for
在同一个元素上使用时的优先级发生了变化。
v-bind="{object}"
现在是顺序敏感的。
v-on:event.native
修饰符被移除。
组件变化 :
函数型组件只能通过普通函数来创建。
单文件组件(SFC)中的 <template>
和 functional
属性已被弃用。
非同步组件现在需要使用 defineAsyncComponent
方法来创建。
组件事件现在应该通过 emits
选项来声明。
渲染函数变化 :
渲染函数 API 发生了变化。
$scopedSlots
属性被移除,所有的 slots 都通过 $slots
作为函数公开。
$listeners
被移除并合并到 $attrs
。
$attrs
现在包含 class
和 style
属性。
自定义元素变化 :
自定义元素检查现在在模板编译时进行。
特殊的 is
属性仅限于 <component>
标签。
其他小变化 :
destroyed
生命周期选项被重命名为 unmounted
。
beforeDestroy
生命周期选项被重命名为 beforeUnmount
。
props
的默认工厂函数不再有 this
上下文。
自定义指令 API 被调整以与组件生命周期一致。
data
选项应始终声明为函数。
mixins
的 data
选项现在浅合并。
属性强制转换策略发生了变化。
一些过渡类名被重命名。
<TransitionGroup>
默认不再渲染包装元素。
监控数组时,回调只在数组被替换时触发。