Vue3 + TypeScript 开发指南
0x00 概述
- 阅读以下内容需要具备一定的 Vue2 基础
- 代码采用规范为:TypeScript + 组合式 API + setup 语法糖
(1)Vue3 简介
- Vue3 第一个正式版发布于 2020 年 9 月 18 日
- Vue3 中文官网
- Vue3 相比 Vue2 的优势:
- 性能提升:打包体积减小,初次渲染和更新渲染都更快,内存使用减少
- 源码升级:使用 Proxy 实现响应式,重写虚拟 DOM 的实现和
Tree-Shaking - 支持 TypeScript
- 新增特性:组合式 API、新内置组件、新生命周期钩子等
(2)TypeScript 概述
- TypeScript 入门:学习 TypeScript | 稀土掘金-SRIGT
0x01 第一个项目
(1)创建项目
创建项目共有两种方法
-
使用 vue-cli 创建
-
在命令提示符中使用命令
npm install -g @vue/cli下载并全局安装 vue-cli如果已经安装过,则可以使用命令
vue -V查看当前 vue-cli 的版本,版本要求在 4.5.0 以上如果需要多版本 vue-cli 共存,则可以参考文章:安装多版本Vue-CLI的实现方法 | 脚本之家-webgiser
-
使用命令
vue create project_name开始创建项目 -
使用方向键选择
Default ([Vue3 babel, eslint]) -
等待创建完成后,使用命令
cd project_name进入项目目录 -
使用命令
npm serve启动项目
-
-
使用 vite 创建
相比使用 vue-cli 创建,使用 vite 的优势在于
- 轻量快速的热重载,实现极速的项目启动
- 对 TypeScript、JSX、CSS 等支持开箱即用
- 按需编译,减少等待编译的时间
- 使用命令
npm create vue@latest创建 Vue3 项目 - 输入项目名称
- 添加 TypeScript 支持
- 不添加 JSX 支持、Vue Router、Pinia、Vitest、E2E 测试、ESLint 语法检查、Prettier 代码格式化
- 使用命令
cd [项目名称]进入项目目录 - 使用命令
npm install添加依赖 - 使用命令
npm run dev启动项目
(2)项目结构
-
node_modules:项目依赖
-
public:项目公共资源
-
src:项目资源
-
assets:资源目录
-
components:组件目录
-
main.ts:项目资源入口文件
import './assets/main.css' // 引入样式文件 import { createApp } from 'vue' // 引入创建 Vue 应用方法 import App from './App.vue' // 引入 App 组件 createApp(App).mount('#app') // 创建以 App 为根组件的应用实例,并挂载到 id 为 app 的容器中(该容器为 index.html) -
App.vue:应用根组件
<template> <!-- 模型 --> </template> <script lang="ts"> /* 控制 */ </script> <style> /* 样式 */ </style>
-
-
env.d.ts:TypeScript 环境配置文件,包括文件类型声明等
-
index.html:项目入口文件
-
vite.config.ts:项目配置文件
0x02 核心语法
(1)组合式 API
-
首先使用 Vue2 语法完成一个组件
-
项目结构
graph TB src-->components-->Person.vue src-->App.vue & main.ts -
详细代码
-
main.ts
import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app') -
App.vue
<template> <div class="app"> <h1>App</h1> <Person /> </div> </template> <script lang="ts"> import Person from './components/Person.vue'; export default { name: 'App', // 当前组件名 components: { // 注册组件 Person } } </script> <style scoped> .app { padding: 20px; } </style> -
Person.vue
<template> <h2>Name: {{ name }}</h2> <h2>Age: {{ age }}</h2> <button @click="showDetail">Detail</button> <hr /> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> </template> <script lang="ts"> export default { name: "Person", data() { // 数据配置 return { // 包含组件的所有数据 name: "John", age: 18, telephone: "1234567890", email: "john@gmail.com" } }, methods: { // 方法配置 showDetail() { // 包含组件的全部方法 alert(`Detail: \ntelephone: ${this.telephone}\n email: ${this.email}`) }, changeName() { this.name = "Jane"; }, changeAge() { this.age += 1; } } } </script> <style> </style>
-
-
目前,使用 Vue2 语法完成的组件 Person 中使用了选项式 API(Options API),其中
name、data、methods均称为选项(或称配置项) -
选项式 API 的问题在于:数据、方法、计算属性等分散在
data、methods、computed等选项中,当需要新增或修改需求时,就需要分别修改各个选项,不便于维护和复用
-
-
Vue3 采用组合式 API
-
组合式 API(Composition API)优势在于:使用函数的方式组织代码,使相关代码更加有序的组织在一起,便于新增或修改需求
-
选项式 API 与 组合式 API 对比(下图来自《做了一夜动画,就为让大家更好的理解Vue3的Composition Api | 稀土掘金-大帅老猿》)
-
(2)setup
a. 概述
setup是 Vue3 中一个新的配置项,值是一个函数,其中包括组合式 API,组件中所用到的数据、方法、计算属性等均配置在setup中- 特点
setup函数返回的可以是一个对象,其中的内容可以直接在模板中使用;也可以返回一个渲染函数setup中访问this是undefined,表明 Vue3 已经弱化this的作用setup函数会在方法beforeCreate之前调用,在所有钩子函数中最优先执行
b. 使用 setup
修改本章第(1)小节采用 Vue2 语法的项目
-
App.vue
<template> <Person /> </template> <script lang="ts"> import Person from './components/Person.vue'; export default { name: 'App', components: { Person } } </script> <style scoped> </style> -
Person.vue
-
引入
setup<script lang="ts"> export default { name: "Person", setup() { } } </script> -
在
setup中声明数据<script lang="ts"> export default { name: "Person", setup() { let name = "John" let age = 18 let telephone = "1234567890" let email = "john@gmail.com" } } </script> -
将需要使用的数据返回到模板中
可以为返回的变量设置别名:如
n: name<template> <h2>Name: {{ n }}</h2> <h2>Age: {{ age }}</h2> </template> <script lang="ts"> export default { name: "Person", setup() { let name = "John" let age = 18 let telephone = "1234567890" let email = "john@gmail.com" return { n: name, age } } } </script> <style> </style> -
在
setup中声明方法并返回<template> <h2>Name: {{ n }}</h2> <h2>Age: {{ age }}</h2> <button @click="showDetail">Detail</button> <hr /> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> </template> <script lang="ts"> export default { name: "Person", setup() { // let ... function showDetail() { alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`) } function changeName() { name = "Jane"; } function changeAge() { age += 1; } return { n: name, age, showDetail, changeName, changeAge } } } </script> <style> </style>此时可以发现,点击“姓名修改”或“年龄修改”按钮后,页面并未发生改变,这是因为之前使用
let声明的数据不是响应式的,具体方法参考本章第 x 小节
-
-
setup可以与data和methods同时存在-
在
data或methods中使用this均可以访问到在setup中声明的数据与函数- 因为
setup的执行早于data和methods
<template> <h2>Age: {{ a }}</h2> </template> <script lang="ts"> export default { name: "Person", data() { return { a: this.age } }, setup() { let name = "John" let age = 18 return { n: name, age } } } </script> - 因为
-
在
setup中无法使用data或methods中声明的数据或函数
-
c. 语法糖
-
在上述项目中,每次在
setup中声明新数据时,都需要在return中进行“注册”,之后才能在模板中使用该数据,为解决此问题,需要使用setup语法糖 -
<script setup lang="ts"></script>相当于如下代码:<script lang="ts"> export default { setup() { return {} } } </script> -
修改 Person.vue
<script setup lang="ts"> let name = "John"; let age = 18; let telephone = "1234567890"; let email = "john@gmail.com"; function showDetail() { alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`); } function changeName() { name = "Jane"; } function changeAge() { age += 1; } </script> -
在
<script setup lang="ts"></script>中无法直接设置组件的name属性,因此存在以下两种方式进行设置:-
使用两个
<script>标签<script lang="ts"> export default { name: "Person" } </script> <script setup lang="ts"> // ... </script> -
(推荐)基于插件实现
-
使用命令
npm install -D vite-plugin-vue-setup-extend安装需要的插件 -
在 vite.config.ts 中引入并调用这个插件
// import ... import vpvse from 'vite-plugin-vue-setup-extend' export default defineConfig({ plugins: [ vue(), vpvse(), ], // ... }) -
重新启动项目
<script setup lang="ts" name="Person"> // ... </script> -
-
-
将 App.vue 中的 Vue2 语法修改为 Vue3
<template> <Person /> </template> <script setup lang="ts" name="App"> import Person from './components/Person.vue'; </script> <style scoped> </style>
(3)创建基本类型的响应式数据
使用
ref创建
-
引入
ref<script setup lang="ts" name="Person123"> import {ref} from 'vue' </script> -
对需要成为响应式的数据使用
ref<script setup lang="ts" name="Person123"> import {ref} from 'vue' let name = ref("John"); let age = ref(18); let telephone = "1234567890"; let email = "john@gmail.com"; </script>-
使用
console.log()可以发现使用ref的name变成了// RefImpl { "__v_isShallow": false, "dep": {}, "__v_isRef": true, "_rawValue": "John", "_value": "John" }未使用
ref的telephone依然是字符串'1234567890'
-
-
修改方法
<script setup lang="ts" name="Person123"> import {ref} from 'vue' let name = ref("John"); let age = ref(18); let telephone = "1234567890"; let email = "john@gmail.com"; function showDetail() { alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`); } function changeName() { name.value = "Jane"; } function changeAge() { age.value += 1; } </script>-
在函数方法中使用
name之类被响应式的数据,需要在属性value中获取或修改值,而在模板中不需要<template> <h2>Name: {{ name }}</h2> <h2>Age: {{ age }}</h2> <button @click="showDetail">Detail</button> <hr /> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> </template>
-
(4)创建对象类型的响应式数据
a. 使用 reactive 创建
-
创建对象以及函数方法,并在模板中使用
<template> <h2>Name: {{ Teacher.name }}</h2> <h2>Age: {{ Teacher.age }}</h2> <p><button @click="changeTeacherAge">Change Teacher Age</button></p> </template> <script setup lang="ts" name="Person"> let Teacher = { name: "John", age: 18 } function changeTeacherAge() { Teacher.age += 1; } </script> -
引入
reactive<script setup lang="ts" name="Person"> import {reactive} from 'vue' // ... </script> -
对需要成为响应式的对象使用
reactive<script setup lang="ts" name="Person"> import {reactive} from 'vue' let Teacher = reactive({ name: "John", age: 18 }) // ... </script>- 使用
console.log()可以发现John变成了 Proxy(Object) 类型的对象
- 使用
-
按钮方法不做调整:
Teacher.age += 1; -
添加对象数组以及函数方法,并在模板中遍历
<template> <!-- ... --> <hr /> <ul> <li v-for="student in Student" :key="student.id">{{ student.name }}</li> </ul> <button @click="changeFirstStudentName">Change First Student Name</button> </template> <script setup lang="ts" name="Person"> // ... let Student = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Charlie" } ] // ... function changeFirstStudentName() { Student[0].name = "Alex"; } </script> -
将对象数组变为响应式
<script setup lang="ts" name="Person"> // ... let Student = reactive([ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Charlie" } ]) // ... </script>此时点击按钮即可修改第一个学生的名字为 Alex
b. 使用 ref 创建
-
修改上述使用
reactive的组件内容<template> <h2>Name: {{ Teacher.name }}</h2> <h2>Age: {{ Teacher.age }}</h2> <p><button @click="changeTeacherAge">Change Teacher Age</button></p> <hr /> <ul> <li v-for="student in Student" :key="student.id">{{ student.name }}</li> </ul> <button @click="changeFirstStudentName">Change First Student Name</button> </template> <script setup lang="ts" name="Person"> let Teacher = { name: "John", age: 18 } let Student = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Charlie" } ] function changeTeacherAge() {} function changeFirstStudentName() {} </script> -
引入
ref并为需要成为响应式的对象使用<script setup lang="ts" name="Person"> import {ref} from 'vue' let Teacher = ref({ name: "John", age: 18 }) let Student = ref([ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Charlie" } ]) // ... </script> -
修改函数方法
<script setup lang="ts" name="Person"> // ... function changeTeacherAge() { Teacher.value.age += 1; } function changeFirstStudentName() { Student.value[0].name = "Alex"; } </script>
c. 对比 ref 与 reactive
-
功能上:
ref可以用来定义基本类型数据和对象类型数据- 在使用
ref创建响应式对象过程中,ref底层实际借用了reactive的方法
- 在使用
reactive可以用来定义对象类型数据
-
区别在于:
-
使用
ref创建的数据必须使用.value-
解决方法:使用插件 volar 自动添加
插件设置方法:
- 点击左下角齿轮图标(管理),并选择“设置”
- 在左侧选项列表中依次选择“扩展”-“Volar”
- 将功能“Auto Insert: Dot Value”打勾即可
-
-
使用
reactive重新分配一个新对象会失去响应式(reactive的局限性)-
解决方法:使用
Object.assign整体替换举例:修改
Teacher对象-
reactive<template> <h2>Name: {{ Teacher.name }}</h2> <h2>Age: {{ Teacher.age }}</h2> <p><button @click="changeTeacher">Change Teacher</button></p> </template> <script setup lang="ts" name="Person"> import {reactive} from 'vue' let Teacher = reactive({ name: "John", age: 18 }) function changeTeacher() { Object.assign(Teacher, { name: "Mary", age: 19 }) } </script> -
ref<template> <h2>Name: {{ Teacher.name }}</h2> <h2>Age: {{ Teacher.age }}</h2> <p><button @click="changeTeacher">Change Teacher</button></p> </template> <script setup lang="ts" name="Person"> import {ref} from 'vue' let Teacher = ref({ name: "John", age: 18 }) function changeTeacher() { Teacher.value = { name: "Mary", age: 19 } } </script>
-
-
-
-
使用原则:
- 若需要一个基本类型的响应式数据,则只能选择
ref - 若需要一个对象类型、层级较浅的响应式数据,则选择任何一个都可以
- 若需要一个对象类型、层级较深的响应式数据,则推荐选择
reactive
- 若需要一个基本类型的响应式数据,则只能选择
(5)toRefs 和 toRef
- 作用:将一个响应式对象中的每一个属性转换为
ref对象 toRefs与toRef功能相同,toRefs可以批量转换
-
修改 Person.vue
<template> <h2>Name: {{ John.name }}</h2> <h2>Age: {{ John.age }}</h2> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> </template> <script setup lang="ts" name="Person"> import {reactive} from 'vue' let John = reactive({ name: "John", age: 18 }) function changeName() { John.name += "~" } function changeAge() { John.age += 1 } </script> <style> </style> -
声明两个新变量,并使用
Person对其进行赋值,之后修改模板<template> <h2>Name: {{ name }}</h2> <h2>Age: {{ age }}</h2> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> </template> <script setup lang="ts" name="Person"> // ... let John = reactive({ name: "John", age: 18 }) let { name, age } = John // ... </script>此时,新变量
name和age并非响应式,点击按钮无法在页面上修改(实际上已发生修改) -
引入
toRefs,将变量name和age变为响应式<script setup lang="ts" name="Person"> import {reactive, toRefs} from 'vue' let John = reactive({ name: "John", age: 18 }) let { name, age } = toRefs(John) // ... </script> -
修改函数方法
<script setup lang="ts" name="Person"> // ... function changeName() { name.value += "~" } function changeAge() { age.value += 1 } </script> -
引入
toRef替代toRefs<script setup lang="ts" name="Person"> import {reactive, toRef} from 'vue' let John = reactive({ name: "John", age: 18 }) let name = toRef(John, 'name') let age = toRef(John, 'age') function changeName() { name.value += "~" } function changeAge() { age.value += 1 } </script>
(6)computed
computed是计算属性,具有缓存,当计算方法相同时,computed会调用缓存,从而优化性能
-
修改 Person.vue
<template> <h2>First Name: <input type="text" v-model="firstName" /></h2> <h2>Last Name: <input type="text" v-model="lastName" /></h2> <h2>Full Name: <span>{{ firstName }} {{ lastName }}</span></h2> </template> <script setup lang="ts" name="Person"> import {ref} from 'vue' let firstName = ref("john") let lastName = ref("Smith") </script> <style> </style> -
引入计算属性
computed<script setup lang="ts" name="Person"> import {ref, computed} from 'vue' // ... </script> -
将姓与名的首字母大写,修改模板与控制器
<template> <!-- ... --> <h2>Full Name: <span>{{ fullName }}</span></h2> </template> <script setup lang="ts" name="Person"> // ... let fullName = computed(() => { return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1) }) </script>此时的
fullName是只读的,无法修改,是一个使用ref创建的响应式对象 -
修改
fullName,实现可读可写<script setup lang="ts" name="Person"> // ... let fullName = computed({ get() { return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1) }, set() {} }) </script> -
在模板中添加按钮,用于修改
fullName,并在控制器中添加相应的函数方法<template> <!-- ... --> <button @click="changeFullName">Change Full Name</button> </template> <script setup lang="ts" name="Person"> // ... function changeFullName() { fullName.value = "bob jackson" } </script> -
修改计算属性中的
set()方法<script setup lang="ts" name="Person"> // ... let fullName = computed({ get() { return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1) }, set(value) { const [newFirstName, newLastName] = value.split(" ") firstName.value = newFirstName lastName.value = newLastName } }) // ... </script>
(7)watch
watch是监视属性- 作用:监视数据变化(与 Vue2 中的
watch作用一致) - 特点:Vue3 中的
watch只监视以下数据:ref定义的数据reactive定义的数据- 函数返回一个值
- 一个包含上述内容的数组
a. 情况一:监视 ref 定义的基本类型数据
直接写数据名即可,监视目标是
value值的改变
-
修改 Person.vue
<template> <h2>Sum: {{ sum }}</h2> <button @click="changeSum"> +1 </button> </template> <script setup lang="ts" name="Person"> import {ref} from 'vue' let sum = ref(0) function changeSum() { sum.value += 1 } </script> <style> </style> -
引入
watch<script setup lang="ts" name="Person"> import {ref, watch} from 'vue' // ... </script> -
使用
watch,一般传入两个参数,依次是监视目标与相应的回调函数<script setup lang="ts" name="Person"> import {ref, watch} from 'vue' // ... watch(sum, (newValue, oldValue) => { console.log("Sum changed from " + oldValue + " to " + newValue) }) </script> -
修改
watch,设置“停止监视”<script setup lang="ts" name="Person"> // ... const stopWatch = watch(sum, (newValue, oldValue) => { console.log("Sum changed from " + oldValue + " to " + newValue) if(newValue >= 10) { stopWatch() } }) </script>
b. 情况二:监视 ref 定义的对象类型数据
- 直接写数据名即可,监视目标是对象的地址值的改变
- 如需监视对象内部的数据,需要手动开启深度监视
- 若修改的是
ref定义的对象中的属性,则newValue和newValue都是新值,因为它们是同一个对象- 若修改整个
ref定义的对象,则newValue是新值,oldValue是旧值,因为它们不是同一个对象
-
修改 Person.vue
<template> <h2>Name: {{ person.name }}</h2> <h2>Age: {{ person.age }}</h2> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> <hr /> <p><button @click="changeAll">Change All</button></p> </template> <script setup lang="ts" name="Person"> import { ref, watch } from "vue" let person = ref({ name: "John", age: 18 }) function changeName() { person.value.name += '~' } function changeAge() { person.value.age += 1 } function changeAll() { person.value = { name: "Mary", age: 19 } } </script> <style> </style> -
使用
watch监视整个对象的地址值变化<script setup lang="ts" name="Person"> // ... watch(person, (newValue, oldValue) => { console.log(newValue, oldValue) }) </script>此时,只有点击“Change All” 按钮才会触发监视,
newValue与oldValue不同 -
手动开启深度监视,监视对象内部的数据
<script setup lang="ts" name="Person"> // ... watch( person, (newValue, oldValue) => { console.log(newValue, oldValue) }, { deep: true, } ) </script>此时,点击“Change Name”或“Change Age”也能触发监视,
newValue与oldValue相同,但是点击“Change All”时,newValue与oldValue依旧不同
c. 情况三:监视 reactive 定义的对象类型数据
该情况下,默认开启了深度监视且无法关闭
-
修改 Person.vue
<template> <h2>Name: {{ person.name }}</h2> <h2>Age: {{ person.age }}</h2> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> <hr /> <p><button @click="changeAll">Change All</button></p> </template> <script setup lang="ts" name="Person"> import { reactive, watch } from "vue" let person = reactive({ name: "John", age: 18, }) function changeName() { person.name += "~" } function changeAge() { person.age += 1 } function changeAll() { Object.assign(person, { name: "Mary", age: 19, }) } </script> <style> </style> -
使用
watch监视对象<script setup lang="ts" name="Person"> // ... watch(person, (newValue, oldValue) => { console.log(newValue, oldValue) }) </script>对于使用
reactive创建的对象,在使用Object.assign()时,仅修改了对象里的内容(覆盖原来的内容),并非创建了新对象,故无法监视对象地址值的变化(因为没有变化)
d. 情况四:监视 ref 或 reactive 定义的对象类型数据中的某个属性
若该属性值不是对象类型,则需写成函数形式
若该属性值是对象类型,则建议写成函数形式
-
修改 Person.vue
<template> <h2>Name: {{ person.name }}</h2> <h2>Age: {{ person.age }}</h2> <h2>Nickname: {{ person.nickname.n1 }}/{{ person.nickname.n2 }}</h2> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> <hr /> <p><button @click="changeNickname1">Change Nickname 1</button></p> <p><button @click="changeNickname2">Change Nickname 2</button></p> <p><button @click="changeNickname">Change Nickname</button></p> </template> <script setup lang="ts" name="Person"> import { reactive, watch } from "vue" let person = reactive({ name: "John", age: 18, nickname: { n1: "J", n2: "Jack" } }) function changeName() { person.name += "~" } function changeAge() { person.age += 1 } function changeNickname1() { person.nickname.n1 = "Big J" } function changeNickname2() { person.nickname.n2 = "Joker" } function changeNickname() { person.nickname = { n1: "Joker", n2: "Big J" } } </script> <style> </style> -
使用
watch监视全部的变化<script setup lang="ts" name="Person"> // ... watch(person, (newValue, oldValue) => { console.log(newValue, oldValue) }) </script> -
修改
watch,设置监视基本类型数据person.name<script setup lang="ts" name="Person"> // ... watch( () => { return person.name; }, (newValue, oldValue) => { console.log(newValue, oldValue); } }) </script> -
修改
watch,设置监视对象类型数据person.nickname<script setup lang="ts" name="Person"> // ... watch( () => person.nickname, (newValue, oldValue) => { console.log(newValue, oldValue); }, { deep: true } }) </script>-
以下写法仅能监视对象类型内部属性数据变化
<script setup lang="ts" name="Person"> // ... watch( person.nickname, (newValue, oldValue) => { console.log(newValue, oldValue); } }) </script> -
以下写法仅能监视对象类型整体地址值变化
<script setup lang="ts" name="Person"> // ... watch( () => person.nickname, (newValue, oldValue) => { console.log(newValue, oldValue); } }) </script>
-
e. 情况五:监视多个数据
-
修改 Person.vue
<template> <h2>Name: {{ person.name }}</h2> <h2>Age: {{ person.age }}</h2> <h2>Nickname: {{ person.nickname.n1 }}/{{ person.nickname.n2 }}</h2> <p><button @click="changeName">Change Name</button></p> <p><button @click="changeAge">Change Age</button></p> <hr /> <p><button @click="changeNickname1">Change Nickname 1</button></p> <p><button @click="changeNickname2">Change Nickname 2</button></p> <p><button @click="changeNickname">Change Nickname</button></p> </template> <script setup lang="ts" name="Person"> import { reactive, watch } from "vue" let person = reactive({ name: "John", age: 18, nickname: { n1: "J", n2: "Jack" } }) function changeName() { person.name += "~" } function changeAge() { person.age += 1 } function changeNickname1() { person.nickname.n1 = "Big J" } function changeNickname2() { person.nickname.n2 = "Joker" } function changeNickname() { person.nickname = { n1: "Joker", n2: "Big J" } } </script> <style> </style> -
使用
watch监视person.name和person.nickname.n1<script setup lang="ts" name="Person"> // ... watch( [() => person.name, () => person.nickname.n1], (newValue, oldValue) => { console.log(newValue, oldValue); }, { deep: true } ) </script>
(8)watchEffect
- 作用:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数
watchEffect与watch对比,两者都能监视响应式数据,但是监视数据变化的方式不同watch需要指明监视的数据watchEffect则不用指明,函数中用到哪些属性,就监视哪些属性
-
修改 Person.vue
<template> <h2>Sum1: {{ sum1 }}</h2> <h2>Sum2: {{ sum2 }}</h2> <p><button @click="changeSum1">Sum1 +1</button></p> <p><button @click="changeSum2">Sum2 +3</button></p> </template> <script setup lang="ts" name="Person"> import { ref, watch } from 'vue' let sum1 = ref(0) let sum2 = ref(0) function changeSum1() { sum1.value += 1 } function changeSum2() { sum2.value += 3 } </script> <style> </style> -
使用
watch监视sum1和sum2,获取最新的值<script setup lang="ts" name="Person"> // ... watch([sum1, sum2], (value) => { console.log(value) }) </script> -
对
sum1和sum2进行条件判断<script setup lang="ts" name="Person"> // ... watch([sum1, sum2], (value) => { let [newSum1, newSum2] = value if(newSum1 >= 10 || newSum2 >= 30) { console.log('WARNING: Sum1 or Sum2 is too high') } }) </script>此时,仅对
sum1和sum2进行监视,当需要监视的目标更多时,建议使用watchEffect -
引入
watchEffect<script setup lang="ts" name="Person"> import { ref, watch, watchEffect } from 'vue' // ... </script> -
使用
watchEffect<script setup lang="ts" name="Person"> // ... watchEffect(() => { if(sum1.value >= 10 || sum2.value >= 30) { console.log('WARNING: Sum1 or Sum2 is too high') } }) </script>
(9)标签的 ref 属性
- 作用:用于注册模板引用
- 用在普通 DOM 标签上,获取到 DOM 节点
- 用在组件标签上,获取到组件实例对象
-
修改 Person.vue
<template> <h2>Vue 3</h2> <button @click="showH2">Show Element H2</button> </template> <script setup lang="ts" name="Person"> function showH2() { console.log() } </script> <style> </style> -
引入标签的
ref属性<template> <h2 ref="title">Vue 3</h2> <button @click="showH2">Show Element H2</button> </template> <script setup lang="ts" name="Person"> import { ref } from 'vue' // ... </script> -
创建一个容器,用于存储
ref标记的内容容器名称与标签中
ref属性值相同<script setup lang="ts" name="Person"> import { ref } from 'vue' let title = ref() // ... </script> -
修改函数方法
showH2(),输出标签<script setup lang="ts" name="Person"> import { ref } from 'vue' let title = ref() function showH2() { console.log(title.value) } </script>此时,
ref属性用在普通 DOM 标签上,获取到 DOM 节点 -
在 App.vue 中,对组件 Person 设置
ref属性<template> <Person ref="person" /> <hr /> <button @click="showPerson">Show Element Person</button> </template> <script setup lang="ts" name="App"> import Person from './components/Person.vue'; import { ref } from 'vue' let person = ref() function showPerson() { console.log(person.value) } </script> <style scoped> </style>此时,
ref属性用在组件标签上,获取到组件实例对象 -
在 Person.vue 中引入
defineExpose实现父子组件通信<script setup lang="ts" name="Person"> import { ref, defineExpose } from 'vue' let title = ref() let number = ref(12345) function showH2() { console.log(title.value) } defineExpose({ title, number }) </script>此时,再次点击按钮“Show Element Person”便可在控制台中看到来自 Person.vue 组件中的
title和number -
重置 App.vue
<template> <Person /> </template> <script setup lang="ts" name="App"> import Person from './components/Person.vue' </script> <style scoped> </style>
(9.5)TypeScript 回顾
TypeScript 基本概述:学习 TypeScript | 稀土掘金-SRIGT
在 Vue3 项目中,TS 接口等位于 ~/src/types/index.ts 中
-
定义接口,用于限制对象的具体属性
interface IPerson { id: string, name: string, age: number } -
暴露接口
暴露接口有三种方法:默认暴露、分别暴露、统一暴露,以下采用分别暴露方法
export interface IPerson { id: string, name: string, age: number } -
修改 Person.vue,声明新变量,引入接口对新变量进行限制
<template> </template> <script setup lang="ts" name="Person"> import { type IPerson } from '@/types' let person:IPerson = { id: "dpb7e82nlh", name: "John", age: 18 } </script> <style> </style>此时,仅声明了一个变量,对于同类型的数组型变量,需要使用泛型
-
修改 Perosn.vue,声明一个数组,使用泛型
<script setup lang="ts" name="Person"> import { type IPerson } from '@/types' let personList:Array<IPerson> = [ { id: "dpb7e82nlh", name: "John", age: 18 }, { id: "u55dyu86gh", name: "Mary", age: 19 }, { id: "ad3d882dse", name: "Niko", age: 17 } ] </script> -
修改 index.ts,定义一个自定义类型,简化对数组限制的使用
export interface IPerson { id: string, name: string, age: number, } // 自定义类型 export type Persons = Array<IPerson>或:
export type Persons = IPerson[] -
修改 Person.vue
<script setup lang="ts" name="Person"> import { type Persons } from '@/types' let personList:Persons = [ { id: "dpb7e82nlh", name: "John", age: 18 }, { id: "u55dyu86gh", name: "Mary", age: 19 }, { id: "ad3d882dse", name: "Niko", age: 17 } ] </script>
(10)props
-
修改 App.vue
<template> <Person /> </template> <script setup lang="ts" name="App"> import Person from './components/Person.vue' import { reactive } from 'vue' let personList = reactive([ { id: "dpb7e82nlh", name: "John", age: 18 }, { id: "u55dyu86gh", name: "Mary", age: 19 }, { id: "ad3d882dse", name: "Niko", age: 17 } ]) </script> <style scoped> </style> -
引入接口并使用
<script setup lang="ts" name="App"> // ... import { type Persons } from '@/types'; let personList = reactive<Persons>([ { id: "dpb7e82nlh", name: "John", age: 18 }, { id: "u55dyu86gh", name: "Mary", age: 19 }, { id: "ad3d882dse", name: "Niko", age: 17 } ]) </script> -
修改父组件 App.vue 的模板内容,向子组件 Person.vue 发送数据
<template> <Person note="This is a note" :list="personList" /> </template> -
修改子组件 Person.vue,从父组件 App.vue 中接收数据
<template> <h2>{{ note }}</h2> <ul> <li v-for="person in list" :key="person.id">{{ person.name }}-{{ person.age }}</li> </ul> </template> <script setup lang="ts" name="Person"> import { defineProps } from 'vue' // 只接收 // defineProps(['note', 'list']) // 接收并保存 let personList = defineProps(['note', 'list']) console.log(personList) console.log(personList.note) </script> <style> </style>-
接收限制类型
<script setup lang="ts" name="Person"> import { defineProps } from 'vue' import type { Persons } from '@/types' defineProps<{ list:Persons }>() </script> -
接收限制类型并指定默认值
<script setup lang="ts" name="Person"> import { defineProps } from 'vue' import type { Persons } from '@/types' withDefaults(defineProps<{ list?: Persons }>(), { list: () => [{ id: "dpb7e82nlh", name: "John", age: 18 }], }) </script>
-
-
重置 App.vue
<template> <Person /> </template> <script setup lang="ts" name="App"> import Person from './components/Person.vue' </script> <style scoped> </style>
(11)生命周期
- 组件的生命周期包括:创建、挂载、更新、销毁/卸载
- 组件在特定的生命周期需要调用特定的生命周期钩子(生命周期函数)
a. Vue2 的生命周期
-
Vue2 生命周期包括八个生命周期钩子
生命周期 生命周期钩子 创建 创建前 beforeCreate创建完成后 created挂载 挂载前 beforeMount挂载完成后 mounted更新 更新前 beforeUpdate更新完成后 updated销毁 销毁前 beforeDestroy销毁完成后 destroyed
-
使用命令
vue create vue2_test或vue init webpack vue2_test创建一个 Vue2 项目 -
重置 ~/src/App.vue
<template> </template> <script> export default { name: 'App', } </script> <style> </style> -
在 ~/src/components 中新建 Person.vue
<template> <div> <h2>Sum: {{ sum }}</h2> <button @click="changeSum">+1</button> </div> </template> <script> export default { // eslint-disable-next-line name: 'Person', data() { return { sum: 0 } }, methods: { changeSum() { this.sum += 1 } } } </script> <style scoped> </style> -
在 App.vue 引入 Person.vue
<template> <PersonVue /> </template> <script> import PersonVue from './components/Person.vue' export default { name: 'App', components: { PersonVue } } </script> <style> </style> -
使用生命周期钩子
<script> export default { // eslint-disable-next-line name: 'Person', data() { // ... }, methods: { // ... }, // 创建前 beforeCreate() { console.log("beforeCreate") }, // 创建完成后 created() { console.log("created") }, // 挂载前 beforeMount() { console.log("beforeMount") }, // 挂载完成后 mounted() { console.log("mounted") }, // 更新前 beforeUpdate() { console.log("beforeUpdate") }, // 更新完成后 updated() { console.log("updated") }, // 销毁前 beforeDestroy() { console.log("beforeDestroy") }, // 销毁完成后 destroyed() { console.log("destroyed") } } </script> -
使用命令
npm run serve启动项目,在开发者工具中查看生命周期过程
b. Vue3 的生命周期
-
Vue3 生命周期包括七个生命周期钩子
生命周期 生命周期钩子 创建 setup挂载 挂载前 onBeforeMount挂载完成后 onMounted更新 更新前 onBeforeUpdate更新完成后 onUpdated卸载 卸载前 onBeforeUnmount卸载完成后 onUnmounted
-
在之前的 Vue3 项目中,修改 Person.vue
<template> <h2>Sum: {{ sum }}</h2> <button @click="changeSum">+1</button> </template> <script setup lang="ts" name="Person"> import { ref } from 'vue' let sum = ref(0) function changeSum() { sum.value += 1 } </script> <style> </style> -
引入并使用生命周期钩子
<script setup lang="ts" name="Person"> import { onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from 'vue' // ... // 创建 console.log("setup") // 挂载前 onBeforeMount(() => { console.log("onBeforeMount") }) // 挂载完成后 onMounted(() => { console.log("onMounted") }) // 更新前 onBeforeUpdate(() => { console.log("onBeforeUpdate") }) // 更新完成后 onUpdated(() => { console.log("onUpdated") }) // 卸载前 onBeforeUnmount(() => { console.log("onBeforeUnmount") }) // 卸载完成后 onUnmounted(() => { console.log("onUnmounted") }) </script>
(12)自定义Hooks
使用命令
npm install -S axios安装 Axios,用于网络请求
-
修改 Person.vue
<template> <h2>Sum: {{ sum }}</h2> <button @click="changeSum">+1</button> <hr /> <img v-for="(img, index) in imgList" :src="img" :key="index" style="height: 100px" /> <p><button @click="changeImg">Next</button></p> </template> <script setup lang="ts" name="Person"> import { reactive, ref } from "vue"; let sum = ref(0); let imgList = reactive([""]); function changeSum() { sum.value += 1; } function changeImg() {} </script> <style> </style> -
引入 Axios
<script setup lang="ts" name="Person"> import axios from 'axios' // ... </script> -
使用 Axios 获取图片地址
<script setup lang="ts" name="Person"> import axios from 'axios' // ... async function changeImg() { try { let result = await axios.get( "https://dog.ceo/api/breed/pembroke/images/random" ); imgList.push(result.data.message); } catch (error) { alert("Error! Please try again."); console.log(error); } } </script>此时,多个功能(求和、请求图片)同时在组件中互相交织,可以使用 Hooks 重新组织代码
-
在 src 目录下新建目录 hooks,其中分别新建 useSum.ts、useImg.ts
-
useSum.ts
import { ref } from 'vue' export default function () { let sum = ref(0) function changeSum() { sum.value += 1 } return { sum, changeSum } } -
useImg.ts
import axios from 'axios' import { reactive } from 'vue' export default function () { let imgList = reactive([""]) async function changeImg() { try { let result = await axios.get( "https://dog.ceo/api/breed/pembroke/images/random" ) imgList.push(result.data.message) } catch (error) { alert("Error! Please try again.") console.log(error) } } return { imgList, changeImg } }
-
-
在 Person.vue 中引入并使用两个 Hooks
<script setup lang="ts" name="Person"> import useSum from '@/hooks/useSum' import useImg from '@/hooks/useImg' const { sum, changeSum } = useSum() const { imgList, changeImg } = useImg() </script>
0x03 路由
(1)概述
- 此处的路由是指前后端交互的路由
- 路由(route)就是一组键值的对应关系
- 多个路由需要经过路由器(router)的管理
- 路由组件通常存放在 pages 或 views 文件夹,一般组件通常存放在 components 文件夹
(2)路由切换
-
重置 ~/src 目录结构
graph TB src-->components & pages & App.vue & main.ts components-->Header.vue pages-->Home.vue & Blog.vue & About.vue -
修改 App.vue(样式可忽略)
<template> <div class="app"> <Header /> <div class="navigation"> <a href="/home" class="active">Home</a> <a href="/blog">Blog</a> <a href="/about">About</a> </div> <div class="main-content"> Content </div> </div> </template> <script setup lang="ts" name="App"> import Header from './components/Header.vue' </script> <style scoped> .app { margin: 0; padding: 0; width: 100%; height: 100%; } .navigation { width: 15%; height: 506px; float: left; background: #03deff; } .navigation a { display: block; text-decoration-line: none; color: black; text-align: center; font-size: 28px; padding-top: 10px; padding-bottom: 10px; border-bottom: 3px solid #fff; } .navigation a.active { background: rgb(1, 120, 144); color: white; } .main-content { display: inline; width: 80%; height: 500px; float: left; margin-left: 10px; font-size: 26px; border: 3px solid #000; } </style> -
修改 Header.vue、Home.vue、Blog.vue、About.vue
-
Header.vue
<template> <h2 class="title">Route Test</h2> </template> <script setup lang="ts" name="Header"> </script> <style scope> .title { width: 100%; text-align: center; } </style> -
Home.vue、Blog.vue、About.vue
<template> <h2>Home</h2> <!-- <h2>Blog</h2> --> <!-- <h2>About</h2> --> </template> <script setup lang="ts"> </script> <style scope> </style>
-
-
创建路由器
-
使用命令
npm install vue-router -S安装路由器 -
在 ~/src 目录下新建目录 router,其中新建 index.ts,用于创建一个路由器并暴露
-
引入
createRouter和createWebHistoryimport { createRouter, createWebHistory } from 'vue-router'其中
createWebHistory是路由器的工作模式,在本章第(3)小节有详细介绍 -
引入子组件
// ... import Home from '@/pages/Home.vue' import Blog from '@/pages/Blog.vue' import About from '@/pages/About.vue' -
创建路由器
// ... const router = createRouter({ history: createWebHistory(), routes: [] })
-
-
制定路由规则
// ... routes: [ { path: '/home', component: Home }, { path: '/blog', component: Blog }, { path: '/about', component: About } ] // ... -
暴露路由
// ... export default router -
修改 main.ts,引入并使用路由器
// ... import router from './router' createApp(App).use(router).mount('#app') -
修改 App.vue,引入路由器视图,修改模板中的超链接
<template> <div class="app"> <Header /> <div class="navigation"> <RouterLink to="/home" active-class="active">Home</RouterLink> <RouterLink to="/blog" active-class="active">Blog</RouterLink> <RouterLink to="/about" active-class="active">About</RouterLink> </div> <div class="main-content"> <RouterView></RouterView> </div> </div> </template> <script setup lang="ts" name="App"> import Header from './components/Header.vue' import { RouterLink, RouterView } from 'vue-router' </script> <style scoped> /* ... */ </style>- 此时,点击导航后,“消失”的路由组件默认是被卸载的,需要的时候再重新挂载
- 可以在组件中使用生命周期钩子验证
<RouterLink>中的to属性有两种写法to="/home":to="{path:'/home'}"- 此写法的优点在本章第()小节介绍
- 此时,点击导航后,“消失”的路由组件默认是被卸载的,需要的时候再重新挂载
(3)路由器的工作模式
a. history 模式
-
优点:URL 更美观,更接近于传统网站的 URL
-
缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有 404 报错
-
使用方法:
-
Vue2:
mode: 'history' -
Vue3:
// router/index.ts import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), // ... }) -
React:
BrowserRouter
-
b. hash 模式
-
优点:兼容性更好,不需要服务端处理路径
-
缺点:URL 中带有
#号,在 SEO 优化方面相对较差 -
使用方法:
-
Vue3:
// router/index.ts import { createRouter, createWebHashHistory } from 'vue-router' const router = createRouter({ history: createWebHashHistory(), // ... })
-
(4)命名路由
- 作用:简化路由跳转与传参
-
在 router/index.ts 中为路由命名
// ... routes: [ { name: 'zy', path: '/home', component: Home }, { name: 'bk', path: '/blog', component: Blog }, { name: 'gy', path: '/about', component: About } ] // ... -
修改 App.vue,使用命名路由的方法进行跳转
<template> <!-- ... --> <div class="navigation"> <RouterLink :to="{ name: 'zy' }" active-class="active">Home</RouterLink> <RouterLink to="/blog" active-class="active">Blog</RouterLink> <RouterLink to="/about" active-class="active">About</RouterLink> </div> <!-- ... --> </template>此时,共有两类方式三种方法实现路由跳转:
字符串写法 to="/home"对象写法 命名跳转 :to="{ name: 'zy' }"路径跳转 :to="{ path: '/home' }"
(5)嵌套路由
在博客页面实现路由的嵌套,实现博客内容根据选择进行动态展示
-
在 pages 目录下新建 Detail.vue
<template> <ul> <li>id: id</li> <li>title: title</li> <li>content: content</li> </ul> </template> <script setup lang="ts" name="detail"> </script> <style scope> ul { list-style: none; padding-left: 20px; } ul>li { line-height: 30px; } </style> -
修改 router/index.ts,引入 Detail.vue
// ... import Detail from '@/pages/Detail.vue' const router = createRouter({ // ... routes: [ // ... { path: '/blog', component: Blog, children: [ { path: 'detail', component: Detail } ] }, // ... ] }) export default router -
修改 Blog.vue,添加博客导航列表、博客内容展示区、相关数据以及样式
<template> <h2>Blog</h2> <ul> <li v-for="blog in blogList" :key="blog.id"> <RouterLink to="/blog/detail">{{ blog.title }}</RouterLink> </li> </ul> <div class="blog-content"> <RouterView></RouterView> </div> </template> <script setup lang="ts" name="Blog"> import { reactive } from 'vue' const blogList = reactive([ { id: 'fhi27df4sda', title: 'Blog01', content: 'Content01' }, { id: 'opdcd2871cb', title: 'Blog02', content: 'Content02' }, { id: 'adi267f4hp5', title: 'Blog03', content: 'Content03' } ]) </script> <style scope> ul { float: left; } ul li { display: block; } ul li a { text-decoration-line: none; } .blog-content { float: left; margin-left: 200px; width: 70%; height: 300px; border: 3px solid black; } </style>
(6)路由传参
- 在 Vue 中,路由用两种参数:query 和 params
a. query
-
修改 Blog.vue,发送参数
修改
to属性为:to,使用模板字符串,在路由后添加?,之后使用格式key1=val1&key2=val2的方式传递参数<template> <!-- ... --> <li v-for="blog in blogList" :key="blog.id"> <RouterLink :to="`/blog/detail?id=${blog.id}&title=${blog.title}&content=${blog.content}`"> {{ blog.title }} </RouterLink> </li> <!-- ... --> </template> -
简化传参
<template> <!-- ... --> <RouterLink :to="{ path: '/blog/detail', query: { id: blog.id, title: blog.title, content: blog.content } }" > {{ blog.title }} </RouterLink> <!-- ... --> </template> -
修改 Detail.vue,接收参数
使用 Hooks
useRoute接收<template> <ul> <li>id: {{ route.query.id }}</li> <li>title: {{ route.query.title }}</li> <li>content: {{ route.query.content }}</li> </ul> </template> <script setup lang="ts" name="detail"> import { useRoute } from 'vue-router' const route = useRoute() </script> -
简化数据展示
<template> <ul> <li>id: {{ query.id }}</li> <li>title: {{ query.title }}</li> <li>content: {{ query.content }}</li> </ul> </template> <script setup lang="ts" name="detail"> import { toRefs } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() let { query } = toRefs(route) </script>此时,模板中仍旧使用了很多
query.xxx的语法,为省略query.,本章第(7)小节有相关处理方法
b. params
-
修改 router/index.ts
// ... children: [ { path: 'detail/:id/:title/:content', component: Detail } ] // ...可以在相关参数后添加
?设置参数的必要性,如:path: 'detail/:id/:title/:content?' -
修改 Blog.vue,发送参数
<template> <!-- ... --> <RouterLink :to="`/blog/detail/${blog.id}/${blog.title}/${blog.content}`"> {{ blog.title }} </RouterLink> <!-- ... --> </template> -
简化传参
-
修改 router/index.ts,为
/detail路由命名// ... children: [ { name: 'detail', path: 'detail/:id/:title/:content', component: Detail } ] // ... -
修改 Blog.vue
<template> <!-- ... --> <RouterLink :to="{ name: 'detail', params: { id: blog.id, title: blog.title, content: blog.content } }" > {{ blog.title }} </RouterLink> <!-- ... --> </template>
-
-
修改 Detail.vue,接收参数
<template> <ul> <li>id: {{ route.params.id }}</li> <li>title: {{ route.params.title }}</li> <li>content: {{ route.params.content }}</li> </ul> </template> <script setup lang="ts" name="detail"> import { useRoute } from 'vue-router' const route = useRoute() </script> -
简化数据展示
<template> <ul> <li>id: {{ params.id }}</li> <li>title: {{ params.title }}</li> <li>content: {{ params.content }}</li> </ul> </template> <script setup lang="ts" name="detail"> import { toRefs } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() let { params } = toRefs(route) </script>
(7)路由的 props 配置
- 作用:让路由组件更方便地接收参数
- 原理:将路由参数作为
props传递给组件
a. 对象写法
-
修改 Blog.vue
<template> <!-- ... --> <RouterLink :to="{ name: 'detail', query: { id: blog.id, title: blog.title, content: blog.content } }" > {{ blog.title }} </RouterLink> <!-- ... --> </template> -
修改 router/index.ts
{ name: 'detail', path: 'detail', component: Detail, props(route) { return route.query } }此时相当于将
<Detail />修改为<Detail id=xxx title=xxx content=xxx />,可以按照第二章第(10)小节的方法将参数从 props 中取出使用 -
修改 Detail.vue
<template> <ul> <li>id: {{ id }}</li> <li>title: {{ title }}</li> <li>content: {{ content }}</li> </ul> </template> <script setup lang="ts" name="detail"> defineProps(['id', 'title', 'content']) </script>
b. 布尔值写法
-
修改 Blog.vue
<template> <!-- ... --> <RouterLink :to="{ name: 'detail', params: { id: blog.id, title: blog.title, content: blog.content } }" > {{ blog.title }} </RouterLink> <!-- ... --> </template> -
修改 router/index.ts
// ... { name: 'detail', path: 'detail/:id/:title/:content', component: Detail, props: true } // ... -
修改 Detail.vue
c. 对象写法
不常用
-
修改 router/index.ts
// ... { name: 'detail', path: 'detail/:id/:title/:content', component: Detail, props: { id: xxx, title: yyy, content: zzz } } // ... -
修改 Detail.vue
(8)replace
-
默认情况下,采用 push 模式,即记录浏览先后顺序,允许前进和回退
-
replace 模式不允许前进和回退
-
修改 App.vue,在
RouterLink标签中添加replace属性<template> <!-- ... --> <RouterLink replace to="/home" active-class="active">Home</RouterLink> <RouterLink replace to="/blog" active-class="active">Blog</RouterLink> <RouterLink replace to="/about" active-class="active">About</RouterLink> <!-- ... --> </template>
(9)编程式导航
-
RouterLink标签的本质是a标签 -
编程式导航的目的是脱离
RouterLink标签,实现路由跳转-
修改 Home.vue,实现进入该页面 3 秒后,以 push 的模式跳转到 /blog
<script setup lang="ts"> import { onMounted } from 'vue' import { useRouter } from 'vue-router' const router = useRouter() onMounted(() => { setTimeout(() => { router.push('/blog') }, 3000) }) </script>
-
-
重置 Home.vue
-
修改 Blog.vue
<template> <h2>Blog</h2> <ul> <li v-for="blog in blogList" :key="blog.id"> <button @click="showDetail(blog)">More</button> <!-- ... --> </li> <!-- ... --> </template> <script setup lang="ts" name="Blog"> import { useRouter } from "vue-router"; // ... const router = useRouter(); interface IBlog { id: string; title: string; content: string; } function showDetail(blog: IBlog) { router.push({ name: "detail", query: { id: blog.id, title: blog.title, content: blog.content, }, }); } </script>
(10)重定向
-
修改 router/index.ts,将路径 / 重定向到 /home
routes: [ { path: '/', redirect: '/home' }, // ... ]此时,访问 http://localhost:5173/ 时,会重定向到 http://localhost:5173/home
0x04 pinia
(1)概述
-
pinia 是 Vue3 中的集中式状态管理工具
- 类似的工具有:redux(React)、vuex(Vue2)等
- 集中式:将所有需要管理的数据集中存放在一个容器中,相对的称为分布式
-
(2)准备
-
重置 ~/src 目录结构
graph TB src-->components & App.vue & main.ts components-->Count.vue & Text.vue -
Count.vue
<template> <div class="count"> <h2>Sum: {{ sum }}</h2> <select v-model.number="number"> <option value="1" selected>1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="add">Add</button> <button @click="sub">Sub</button> </div> </template> <script setup lang="ts" name="Count"> import { ref } from 'vue' let sum = ref(0) let number = ref(1) function add() { sum.value += number.value } function sub() { sum.value -= number.value } </script> <style scope> .count { text-align: center; width: 50%; height: 120px; background: #03deff; border: 3px solid black; } select, button { margin: 10px; } </style>当在
select标签中进行选择时,为向变量number中传入数字,可以使用以下方法之一:- 修改
select标签中的v-model为v-model.number(上面使用的方法) - 修改
option标签中的value为:value
- 修改
-
Text.vue
<template> <div class="text"> <button @click="getText">Get Text</button> <ul> <li v-for="text in textList" :key="text.id"> {{ text.content }} </li> </ul> </div> </template> <script setup lang="ts" name="Text"> import { reactive } from 'vue' import axios from 'axios' let textList = reactive([ { id: '01', content: "Text01" }, { id: '02', content: "Text02" }, { id: '03', content: "Text03" } ]) async function getText() { let { data:{result:{content}} } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish') textList.unshift({ id: Date.now().toString(), content}) console.log(content) } </script> <style scope> .text { width: 50%; height: 150px; background: #fbff03; border: 3px solid black; padding-left: 10px; } </style> -
App.vue
<template> <Count /> <br /> <Text /> </template> <script setup lang="ts" name="App"> import Count from './components/Count.vue' import Text from './components/Text.vue' </script> <style scope> </style> -
main.ts
import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
(3)搭建环境
-
使用命令
npm install -S pinia安装 pinia -
在 main.ts 中引入并安装 pinia
import { createApp } from 'vue' import App from './App.vue' import { createPinia } from 'pinia' createApp(App).use(createPinia()).mount('#app')
(4)存储与读取数据
-
store 是一个保存状态、业务逻辑的实体,每个组件都可以读写它
-
store 中有三个概念,与组件中的一些属性类似
store 属性 statedatagettercomputedactionsmethods -
store 目录下的 ts 文件命名与组件的相同
以下涉及对文件、变量等命名的格式均符合 pinia 官方文档规范
-
在 ~/src 目录下,新建 store 目录,其中新建 count.ts 和 text.ts
-
count.ts
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { state() { return { sum: 10 } } }) -
text.ts
import { defineStore } from 'pinia' export const useTextStore = defineStore('text', { state() { return { textList: [ { id: '01', content: "Text01" }, { id: '02', content: "Text02" }, { id: '03', content: "Text03" } ] } } })
-
-
修改 Count.vue 和 Text.vue
-
Count.vue
<template> <div class="count"> <h2>Sum: {{ countStore.sum }}</h2> <select v-model.number="number"> <option value="1" selected>1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="add">Add</button> <button @click="sub">Sub</button> </div> </template> <script setup lang="ts" name="Count"> import { ref } from 'vue' import { useCountStore } from '@/store/count' const countStore = useCountStore() let number = ref(1) function add() {} function sub() {} </script> -
Text.vue
<template> <div class="text"> <button @click="getText">Get Text</button> <ul> <li v-for="text in textStore.textList" :key="text.id"> {{ text.content }} </li> </ul> </div> </template> <script setup lang="ts" name="Text"> import { useTextStore } from '@/store/text' const textStore = useTextStore() async function getText() {} </script>
-
(5)修改数据
-
方式一:直接手动修改
修改 Count.vue
<template> <div class="count"> <!-- ... --> <br /> <button @click="change">Change</button> </div> </template> <script setup lang="ts" name="Count"> // ... import { useCountStore } from '@/store/count' const countStore = useCountStore() // ... function change() { countStore.sum = 100 } </script> -
方式二:手动批量修改
修改 Count.vue
<script setup lang="ts" name="Count"> // ... function change() { countStore.$patch({ sum: 100, num: 1 }) } </script> -
方式三:使用
actions修改-
修改 count.ts
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { // ... actions: { increment(value:number) { if(this.sum < 20) { this.sum += value } } } }) -
修改 Count.vue
<script setup lang="ts" name="Count"> // ... function add() { countStore.increment(number.value) } // ... </script>
-
(6)storeToRefs
-
对于从 store 中获得的数据,可以按以下方法在模板中展示
<template> <div class="count"> <h2>Sum: {{ countStore.sum }}</h2> <!-- ... --> </template> <script setup lang="ts" name="Count"> // ... import { useCountStore } from '@/store/count' const countStore = useCountStore() </script> -
为使模板更加简洁,可以使用
toRefs将sum从 store 中解构出来<template> <div class="count"> <h2>Sum: {{ sum }}</h2> <!-- ... --> </template> <script setup lang="ts" name="Count"> // ... import { toRefs } from 'vue' const { sum } = toRefs(countStore) </script> -
直接使用
toRefs会将 store 中携带的方法也变为响应式数据,因此可以使用 pinia 提供的storeToRefs<script setup lang="ts" name="Count"> // ... import { storeToRefs } from 'pinia' const { sum } = storeToRefs(countStore) </script>
(7)getters
- 在
state中的数据需要经过处理后再使用时,可以使用getters配置
-
修改 store/count.ts
export const useCountStore = defineStore('count', { state() { return { sum: 10 } }, getters: { bigSum(state) { return state.sum * 100 } }, // ... }) -
使用
this可以替代参数state的传入getters: { bigSum(): number { return this.sum * 100 } }, -
如果不使用
this则可以使用箭头函数getters: { bigSum: state => state.sum * 100 }, -
修改 Count.vue,使用
bigSum<template> <div class="count"> <h2>Sum: {{ sum }}, BigSum: {{ bigSum }}</h2> <!-- ... --> </template> <script setup lang="ts" name="Count"> // ... const { sum, bigSum } = storeToRefs(countStore) </script>
(8)$subscribe
- 作用:订阅,监听
state及其变化
-
修改 Text.vue
<script setup lang="ts" name="Text"> // ... textStore.$subscribe(() => { console.log('state.textList changed') }) </script>此时,点击按钮后,在开发者工具中可以看到输出了“state.textList changed”
-
$subscribe中可以添加参数mutate:本次修改的数据state:当前的数据
<script setup lang="ts" name="Text"> // ... textStore.$subscribe((mutate, state) => { console.log(mutate, state) }) </script> -
使用
state,借助浏览器本地存储,实现数据变化后,不会在刷新后初始化-
修改 Text.vue
<script setup lang="ts" name="Text"> import { useTextStore } from '@/store/text' const textStore = useTextStore() textStore.$subscribe((mutate, state) => { localStorage.setItem('textList', JSON.stringify(state.textList)) }) function getText() { textStore.getText() } </script> -
修改 store/text.ts
import axios from 'axios' import { defineStore } from 'pinia' export const useTextStore = defineStore('text', { state() { return { textList: JSON.parse(localStorage.getItem('textList') as string) || [] } }, actions: { async getText() { let { data:{result:{content}} } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish') this.textList.unshift({ id: Date.now().toString(), content}) console.log(content) } } })
-
(9)store 组合式写法
-
之前的内容使用了类似 Vue2 语法的选项式写法
-
修改 Text.ts
import axios from 'axios' import { defineStore } from 'pinia' import { reactive } from 'vue' export const useTextStore = defineStore('text', () => { const textList = reactive(JSON.parse(localStorage.getItem('textList') as string) || []) async function getText() { let { data: { result: { content } } } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish') textList.unshift({ id: Date.now().toString(), content }) console.log(content) } return { textList, getText } })
0x05 组件通信
(0)概述
-
Vue3 中移出了事件总线,可以使用
pubsub代替- Vuex 换成了 pinia
.sync优化到v-model中$listeners合并到$attrs中
-
重置 ~/src 目录结构
graph TB src-->components & App.vue & main.ts-
Parent.vue
<template> <div class="parent"> <h2>Parent</h2> <Child /> </div> </template> <script setup lang="ts" name="Parent"> import Child from './Child.vue' </script> <style scope> .parent { width: 50%; padding: 20px; background: #03deff; border: 3px solid black; } </style> -
Child.vue
<template> <div class="child"> <h2>Child</h2> </div> </template> <script setup lang="ts" name="Child"> </script> <style scope> .child { width: 50%; padding: 20px; background: #fbff03; border: 3px solid black; } </style> -
App.vue
<template> <Parent /> </template> <script setup lang="ts" name="App"> import Parent from './components/Parent.vue' </script> <style scope> </style> -
main.ts
import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
-
-
共有 \(7+2\) 种通信方式
(1)props
a. 父组件向子组件传递数据
-
修改 Parent.vue,在控制器中创建数据,在模板标签中发送数据
<template> <div class="parent"> <h2>Parent</h2> <h4>Parent's number: {{ numberParent }}</h4> <Child :number="numberParent" /> </div> </template> <script setup lang="ts" name="Parent"> import Child from './Child.vue' import { ref } from 'vue' let numberParent = ref(12345) </script> -
修改 Child.vue,接收数据
<template> <div class="child"> <h2>Child</h2> <h4>Child's number: {{ numberChild }}</h4> <h4>Number from Parent: {{ number }}</h4> </div> </template> <script setup lang="ts" name="Child"> import { ref } from 'vue' let numberChild = ref(56789) defineProps(['number']) </script>
b. 子组件向父组件传递数据
-
修改 Parent.vue,创建用于接收子组件发送数据的方法,将方法发送给子组件
<template> <div class="parent"> <h2>Parent</h2> <h4>Parent's number: {{ numberParent }}</h4> <h4 v-show="number">Number from Child: {{ number }}</h4> <Child :number="numberParent" :sendNumber="getNumber" /> </div> </template> <script setup lang="ts" name="Parent"> import Child from './Child.vue' import { ref } from 'vue' let numberParent = ref(12345) let number = ref(null) function getNumber(value: Number) { number.value = value } </script> -
修改 Child.vue,接收父组件发送的方法,并添加按钮使用该方法
<template> <div class="child"> <h2>Child</h2> <h4>Child's number: {{ numberChild }}</h4> <h4>Number from Parent: {{ number }}</h4> <button @click="sendNumber(numberChild)">Send Child's number to Parent</button> </div> </template> <script setup lang="ts" name="Child"> import { ref } from 'vue' let numberChild = ref(56789) defineProps(['number', 'sendNumber']) </script>
(2)自定义事件
-
获取事件,即使用
$event<template> <div> <button @click="getEvent($event)">Event</button> </div> </template> <script setup lang="ts"> function getEvent(event: Event) { console.log(event) } </script>
-
修改 Child.vue,声明自定义事件
<template> <div class="child"> <h2>Child</h2> <h4>Child's number: {{ numberChild }}</h4> <button @click="emit('custom-event', numberChild)">Send Child's number to Parent</button> </div> </template> <script setup lang="ts" name="Child"> import { ref } from 'vue' let numberChild = ref(12345) const emit = defineEmits(['custom-event']) </script> -
修改 Parent.vue,绑定自定义事件
<template> <div class="parent"> <h2>Parent</h2> <h4 v-show="number">Number from Child: {{ number }}</h4> <Child @custom-event="getNumber" /> </div> </template> <script setup lang="ts" name="Parent"> import Child from './Child.vue' import { ref } from 'vue' let number = ref(null) function getNumber(value: Number) { number.value = value } </script>
(3)mitt
a. 概述与准备
- 可以实现任意组件通信
- 与 pubsub、$bus 相类似,都是消息订阅与发布
- 接收方:提前绑定事件,即订阅消息
- 发送方:适时触发事件,即发布消息
-
使用命令
npm install -S mitt安装 mitt -
在 ~/src 目录下新建 utils 目录,其中新建 emitter.ts,引入、调用、暴露 mitt
import mitt from 'mitt' const emitter = mitt() export default emitteremitter.all():获取所有绑定事件emitter.emit():触发指定事件emitter.off():解绑指定事件emitter.on():绑定指定事件
b. 基本使用方法
-
在 main.ts 中引入 emitter.ts
// ... import emitter from './utils/emitter' -
修改 utils/emitter.ts,绑定事件
import mitt from 'mitt' const emitter = mitt() emitter.on('event1', () => { console.log('event1') }) emitter.on('event2', () => { console.log('event2') }) export default emitter -
触发事件
// ... setInterval(() => { emitter.emit('event1') emitter.emit('event2') }, 1000) export default emitter -
解绑事件
// ... setTimeout(() => { emitter.off('event1') console.log('event1 off') }, 3000) export default emitter-
解绑所有事件
setTimeout(() => { emitter.all.clear() console.log('All clear') }, 3000)
-
c. 实际应用
目录结构:
graph TB src-->components & utils & App.vue & main.ts components-->Child1.vue & Child2.vue & Parent.vue utils-->emitter.ts
Child1.vue(Child2.vue)
<template> <div class="child"> <h2>Child1</h2> <h4>Name: {{ name }}</h4> </div> </template> <script setup lang="ts" name="Child1"> import { ref } from 'vue' // Child1.vue let name = ref('Alex') // Child2.vue // let name = ref('Bob') </script> <style scope> .child { width: 50%; padding: 20px; background: #fbff03; border: 3px solid black; } </style>Parent.vue
<template> <div class="parent"> <h2>Parent</h2> <Child1 /> <br /> <Child2 /> </div> </template> <script setup lang="ts" name="Parent"> import Child1 from './Child1.vue' import Child2 from './Child2.vue' </script> <style scope> .parent { width: 50%; padding: 20px; background: #03deff; border: 3px solid black; } </style>emitter.ts
import mitt from 'mitt' const emitter = mitt() export default emitter
-
修改 Child2.vue,绑定事件
<template> <div class="child"> <!-- ... --> <h4 v-show="brotherName">Brother's name: {{ brotherName }}</h4> </div> </template> <script setup lang="ts" name="Child2"> // ... import emitter from '@/utils/emitter' let brotherName = ref('') emitter.on('send-name', (value: string) => { brotherName.value = value }) </script> -
修改 Child1.vue,发送数据
<template> <div class="child"> <!-- ... --> <button @click="emitter.emit('send-name', name)">Send Name</button> </div> </template> <script setup lang="ts" name="Child1"> // ... import emitter from '@/utils/emitter' </script> -
修改 Child2.vue,卸载组件时解绑事件
<script setup lang="ts" name="Child2"> // ... import { ref, onUnmounted } from 'vue' onUnmounted(() => { emitter.off('send-name') }) </script>
(4)v-model
- 实际开发中不常用,常见于 UI 组件库
a. HTML 标签
-
修改 Parent.vue,将
v-model用在input标签上<template> <div class="parent"> <h2>Parent</h2> <input type="text" v-model="name" placeholder="Enter your name" /> </div> </template> <script setup lang="ts" name="Parent"> import { ref } from 'vue' let name = ref("John") </script> <style scope> .parent { width: 50%; padding: 20px; background: #03deff; border: 3px solid black; } </style> -
在
input标签中使用v-model相当于:<input type="text" :value="name" @input="name=(<HTMLInputElement>$event.target).value" />(<HTMLInputElement>$event.target):TypeScript 断言检查
b. 组件标签
-
在 components 中新建 CustomInput.vue
-
修改 Parent.vue
<template> <div class="parent"> <h2>Parent</h2> <CustomInput :modelValue="name" @update:modelValue="name = $event" /> </div> </template> <script setup lang="ts" name="Parent"> import { ref } from 'vue' import CustomInput from './CustomInput.vue' let name = ref("John") </script> -
修改 CustomInput.vue
<template> <input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)" /> </template> <script setup lang="ts" name="CustomInput"> defineProps(["modelValue"]) const emit = defineEmits(["update:modelValue"]) </script> <style scope> </style>- 对于
$event的target判定:- 当触发的是原生事件时,
$event是事件对象,需要.target - 当触发的是自定义事件时,
$event是触发事件数据,不需要.target
- 当触发的是原生事件时,
- 对于
-
修改 Parent.vue,使用
v-model<template> <div class="parent"> <h2>Parent</h2> <!-- <CustomInput :modelValue="name" @update:modelValue="name = $event" /> --> <CustomInput v-model="name" /> </div> </template>此时,可以通过设置
v-model,实现修改modelValue以及添加多个v-model -
修改 Parent.vue
<template> <div class="parent"> <!-- ... --> <CustomInput v-model:m1="name1" v-model:m2="name2" /> </div> </template> <script setup lang="ts" name="Parent"> // ... let name1 = ref("John") let name2 = ref("Mary") </script> -
修改 CustomInput.vue
<template> <input type="text" :value="m1" @input="emit('update:m1', (<HTMLInputElement>$event.target).value)" /> <input type="text" :value="m2" @input="emit('update:m2', (<HTMLInputElement>$event.target).value)" /> </template> <script setup lang="ts" name="CustomInput"> defineProps(["m1", "m2"]) const emit = defineEmits(["update:m1", "update:m2"]) </script>
(5)$attrs
$attrs是一个对象,包含所有父组件传入的标签属性,用于实现祖父组件向孙子组件通信
目录结构:
graph TB components-->Grand.vue & Parent.vue & Child.vue
-
修改 Grand.vue,创建数据,并通过 props 发送
<template> <div class="grand"> <h2>Grand</h2> <h4>Number: {{ number }}</h4> <Parent :number="number" /> </div> </template> <script setup lang="ts" name="Grand"> import Parent from './Parent.vue' import { ref } from 'vue' let number = ref(123) </script> -
修改 Parent.vue,使用
$attrs将来自 Grand.vue 的数据以 props 的方式转发给 Child.vue<template> <div class="parent"> <h2>Parent</h2> <Child v-bind="$attrs"/> </div> </template> <script setup lang="ts" name="Parent"> import Child from './Child.vue' </script> -
修改 Child.vue,接收数据并展示
<template> <div class="child"> <h2>Child</h2> <h4>Number from Grand: {{ number }}</h4> </div> </template> <script setup lang="ts" name="Child"> defineProps(['number']) </script>此时,祖父组件 Grand.vue 也可以向孙子组件 Child.vue 传递方法,从而可以在孙子组件中修改祖父组件的数据
(6)$refs & $parent
-
父子组件通信
属性 作用 说明 $refs父组件向子组件通信 值
为
对
象包含所有被 ref属性标识的 DOM 元素或组件实例$parent子组件向父组件通信 值为对象,当前组件的父组件实例对象
目录结构:
graph TB components-->Parent.vue & Child1.vue & Child2.vue
a. $refs
-
修改 Child1.vue 和 Child2.vue,创建数据并允许访问
<template> <div class="child"> <h2>Child1</h2> <!-- Child2.vue <h2>Child2</h2> --> <h4>Number: {{ number }}</h4> </div> </template> <script setup lang="ts" name="Child1"> import { ref } from 'vue' let number = ref(456) // Child2.vue // let number = ref(789) defineExpose({ number }) </script> -
修改 Parent.vue,设置按钮使其能够修改子组件中的
number<template> <div class="parent"> <h2>Parent</h2> <button @click="changeNumber">Change Child1's number</button> <hr /> <Child1 ref="c1" /> <br /> <Child2 ref="c2" /> </div> </template> <script setup lang="ts" name="Parent"> import Child1 from './Child1.vue' import Child2 from './Child2.vue' import { ref } from 'vue' let c1 = ref() let c2 = ref() function changeNumber() { c1.value.number = 123 c2.value.number = 123 } </script> -
使用
$refs,使其能够批量修改<template> <div class="parent"> <!-- ... --> <button @click="getAll($refs)">Get All Child's number</button> <!-- ... --> </div> </template> <script setup lang="ts" name="Parent"> // ... function getAll(refs: any) { for (let key in refs) { refs[key].number = 123 } } </script>
b. $parent
-
修改 Parent.vue,创建数据并允许访问
<template> <div class="parent"> <h2>Parent</h2> <h4>Number: {{ number }}</h4> <hr /> <Child1 /> <br /> <Child2 /> </div> </template> <script setup lang="ts" name="Parent"> import Child1 from './Child1.vue' import Child2 from './Child2.vue' import { ref } from 'vue' let number = ref(123) defineExpose({ number }) </script> -
修改 Child1.vue
<template> <div class="child"> <h2>Child1</h2> <button @click="changeNumber($parent)">Change Number</button> </div> </template> <script setup lang="ts" name="Child1"> function changeNumber(parent: any) { parent.number = 456 } </script>
(7)provide & inject
-
祖孙组件通信
-
使用方法:
graph LR A(祖父组件)--provide 发送数据-->B(孙子组件)--inject 接收数据-->A
目录结构:
graph TB components-->Grand.vue & Parent.vue & Child.vue
-
修改 Grand.vue,创建数据
<template> <div class="grand"> <h2>Grand</h2> <h4>Number: {{ number }}</h4> <Parent /> </div> </template> <script setup lang="ts" name="Grand"> import Parent from './Parent.vue' import { ref } from 'vue' let number = ref(123) </script> -
使用
provide发送数据<script setup lang="ts" name="Grand"> // ... import { ref, provide } from 'vue' provide('number', number) </script> -
修改 Child.vue,接收并展示数据
<template> <div class="child"> <h2>Child</h2> <h4>Number from Grand: {{ number }}</h4> </div> </template> <script setup lang="ts" name="Child"> import { inject } from 'vue' let number = inject('number') </script> -
当
inject的数据不存在时,可以展示默认值<template> <div class="child"> <h2>Child</h2> <h4>Number from Grand: {{ number }}</h4> <h4>Number from Parent: {{ numberP }}</h4> </div> </template> <script setup lang="ts" name="Child"> import { inject } from 'vue' let number = inject('number') let numberP = inject('numberP', 456) </script> -
修改 Grand.vue,提供方法用于修改祖父组件中的数据
<script setup lang="ts" name="Grand"> // ... function changeNumber() { number.value = 456 } provide('number', { number, changeNumber }) </script> -
修改 Child.vue,接收方法并使用按钮触发
<template> <div class="child"> <h2>Child</h2> <h4>Number from Grand: {{ number }}</h4> <button @click="changeNumber">Change Number</button> </div> </template> <script setup lang="ts" name="Child"> import { inject } from 'vue' let { number, changeNumber } = inject('number') </script>
(8)pinia
参考第四章内容
(9)slot
- slot 翻译为“插槽”,分为默认插槽、具名插槽、作用域插槽
a. 默认插槽
-
修改 Parent.vue,创建数据
<template> <div class="parent"> <h2>Parent</h2> <div class="category"> <Child title="C1"></Child> <Child title="C2"></Child> <Child title="C3"></Child> </div> </div> </template> <script setup lang="ts" name="Parent"> import Child from './Child.vue' import { reactive } from 'vue' let c1List = reactive([ { id: "01", Text: "C1-Text1" }, { id: "02", Text: "C1-Text2" }, { id: "03", Text: "C1-Text3" } ]) let c2List = reactive([ { id: "01", Text: "C2-Text1" }, { id: "02", Text: "C2-Text2" }, { id: "03", Text: "C2-Text3" } ]) let c3List = reactive([ { id: "01", Text: "C3-Text1" }, { id: "02", Text: "C3-Text2" }, { id: "03", Text: "C3-Text3" } ]) </script> <style scope> .parent { width: 80%; padding: 20px; background: #03deff; border: 3px solid black; } .category { display: flex; justify-content: space-around; } .category div { margin: 10px; } </style> -
修改 Child.vue,接收数据并展示
<template> <div class="child"> <h2>{{ title }}</h2> </div> </template> <script setup lang="ts" name="Child"> defineProps(['title']) </script> <style scope> .child { width: 50%; padding: 20px; background: #fbff03; border: 3px solid black; } </style> -
修改 Parent.vue 中的模板内容
<template> <div class="parent"> <h2>Parent</h2> <div class="category"> <Child title="C1"> <ul> <li v-for="item in c1List" :key="item.id">{{ item.text }}</li> </ul> </Child> <Child title="C2"> <ul> <li v-for="item in c2List" :key="item.id">{{ item.text }}</li> </ul> </Child> <Child title="C3"> <!-- <ul> <li v-for="item in c3List" :key="item.id">{{ item.text }}</li> </ul> --> </Child> </div> </div> </template> -
修改 Child.vue,引入插槽并设置默认值
<template> <div class="child"> <h2>{{ title }}</h2> <slot>Default</slot> </div> </template>此时,C1 和 C2 的内容均正常展示;C3 的内容无法展示,而是显示默认值
b. 具名插槽
具有名字的插槽
-
修改 Child.vue,为插槽添加名字
<template> <div class="child"> <h2>{{ title }}</h2> <slot name="list">Default</slot> </div> </template> -
修改 Paren.vue,为引用的组件标签添加
v-slot:属性使用
#简便写法可以替代v-slot:写法<template> <div class="parent"> <h2>Parent</h2> <div class="category"> <Child title="C1" #list> <ul> <li v-for="item in c1List" :key="item.id">{{ item.text }}</li> </ul> </Child> <Child title="C2" v-slot:List> <ul> <li v-for="item in c2List" :key="item.id">{{ item.text }}</li> </ul> </Child> <Child title="C3"> <ul> <li v-for="item in c3List" :key="item.id">{{ item.text }}</li> </ul> </Child> </div> </div> </template>此时,C1 的内容正常展示;C2 和 C3 的内容均无法展示,而是显示默认值
-
修改 Child.vue,设置更多的具名插槽,使
title通过插槽进行传递<template> <div class="child"> <slot name="title"><h2>No Title</h2></slot> <slot name="slot">Default</slot> </div> </template> <script setup lang="ts" name="Child"> </script> -
修改 Parent.vue,使用
template标签<template> <div class="parent"> <h2>Parent</h2> <div class="category"> <Child> <template v-slot:title> <h2>C1</h2> </template> <template v-slot:list> <ul> <li v-for="item in c1List" :key="item.id">{{ item.text }}</li> </ul> </template> </Child> <Child> <template v-slot:list> <ul> <li v-for="item in c2List" :key="item.id">{{ item.text }}</li> </ul> </template> <template v-slot:title> <h2>C2</h2> </template> </Child> <Child> <template v-slot:list> <ul> <li v-for="item in c3List" :key="item.id">{{ item.text }}</li> </ul> <h2>C3</h2> </template> </Child> </div> </div> </template>此时,C1 和 C2 的内容均正常展示;C3 的内容无法展示,而是显示默认值
c. 作用域插槽
-
修改 Child.vue,原始数据在子组件中,使用 slot 将数据传递到父组件中
<template> <div class="child"> <slot :cl="cList">Default</slot> </div> </template> <script setup lang="ts" name="Child"> import { reactive } from 'vue' let cList = reactive([ { id: "01", text: "C-Text1" }, { id: "02", text: "C-Text2" }, { id: "03", text: "C-Text3" } ]) </script> -
修改 Parent.vue,接收并使用子组件传递来的数据
<template> <div class="parent"> <h2>Parent</h2> <div class="category"> <Child> <template v-slot="params"> <!-- 接收传递来的数据 --> <ul> <li v-for="item in params.cl" :key="item.id">{{ item.text }}</li> </ul> </template> </Child> <Child> <template v-slot="{ cl }"> <!-- 解构传递来的数据 --> <ol> <li v-for="item in cl" :key="item.id">{{ item.text }}</li> </ol> </template> </Child> <Child> <template v-slot:default="{ cl }"> <!-- 具名作用域插槽(default是插槽默认名) --> <h4 v-for="item in cl" :key="item.id">{{ item.text }}</h4> </template> </Child> </div> </div> </template> <script setup lang="ts" name="Parent"> import Child from './Child.vue' </script>
(10)总结
| 组件关系 | 通信方式 |
|---|---|
| 父传子 | props |
v-model |
|
$refs |
|
| 默认插槽 / 具名插槽 | |
| 子传父 | props |
| 自定义事件 | |
v-model |
|
$parent |
|
| 作用域插槽 | |
| 祖孙互传 | $attrs |
provide & inject |
|
| 任意组件 | mitt |
| pinia |
0x06 其他 API
- 以下 API 使用场景不多,但需要了解
(1)shallowRef & shallowReactive
- 两种方法一般用于绕开深度响应,提高性能,加快访问速度
a. shallowRef
-
作用:创建一个响应式数据,只对顶层属性进行响应式处理
-
特点:只跟踪引用值的变化,不跟踪值内部的属性的变化
-
用法
-
修改 App.vue,创建数据与方法,并在模板中展示
<template> <div class="app"> <h2>Name: {{ person.name }}</h2> <h2>Age: {{ person.age }}</h2> <p><button @click="changerName">Change Name</button></p> <p><button @click="changerAge">Change Age</button></p> <p><button @click="changerAll">Change All</button></p> </div> </template> <script setup lang="ts" name="App"> let person = { name: 'John', age: 18 } function changerName() { person.value.name = 'Mary' } function changerAge() { person.value.age = 19 } function changerAll() { person.value = { name: 'Mary', age: 19 } } </script> -
引入
shallowRef并使用<script setup lang="ts" name="App"> import { shallowRef } from 'vue' let person = shallowRef({ name: 'John', age: 18 }) // ... </script>此时,姓名和年龄均不可修改,而所有信息可以通过自定义的
changeAll()方法修改
-
b. shallowReactive
-
作用:创建一个浅层响应式对象,只将对象的最顶层属性变成响应式,其他属性不变
-
特点:对象的顶层属性是响应式的,嵌套对象的属性不是
-
用法
-
修改 App.vue,创建数据与方法,并在模板中展示
<template> <div class="app"> <h2>Name: {{ person.name }}</h2> <h2>Age: {{ person.detail.age }}</h2> <p><button @click="changerName">Change Name</button></p> <p><button @click="changerAge">Change Age</button></p> <p><button @click="changerAll">Change All</button></p> </div> </template> <script setup lang="ts" name="App"> let person = { name: 'John', detail: { age: 18 } } function changerName() { person.name = 'Mary' } function changerAge() { person.detail.age = 19 } function changerAll() { Object.assign(person, { name: 'Mary', detail: { age: 19 } }) } </script> -
引入
shallowReactive并使用<script setup lang="ts" name="App"> import { shallowReactive } from 'vue' let person = shallowReactive({ name: 'John', detail: { age: 18 } }) // ... </script>此时,姓名可以修改,但年龄不可修改,而所有信息可以通过自定义的
changeAll()方法修改
-
(2)readonly & shallowReadonly
a. readonly
-
作用:创建一个对象的深只读副本
-
特点:
- 对象的所有嵌套属性变为只读
- 阻止对象被修改
-
应用场景:
- 创建不可变的状态快照
- 保护全局状态/配置不可修改
-
用法
-
修改 App.vue,创建数据
<template> <div class="app"> <h2>Sum: {{ sum1 }}</h2> <h2>Sum readonly: {{ sum2 }}</h2> <p><button @click="changeSum1">Change Sum</button></p> <p><button @click="changeSum2">Change Sum readonly</button></p> </div> </template> <script setup lang="ts" name="App"> import { ref } from 'vue' let sum1 = ref(0) let sum2 = ref(0) function changeSum1() { sum1.value += 1 } function changeSum2() { sum2.value += 1 } </script> -
引入
readonly并使用<script setup lang="ts" name="App"> import { ref, readonly } from 'vue' // ... let sum2 = readonly(sum1) // ... </script>此时,
sum2会随着sum1改变而改变,但点击按钮“Change Sum readonly”不会对sum修改
-
b. shallowReadonly
-
作用:创建一个对象的浅只读副本
-
特点:只将对象的顶层属性设置为只读,对象内部的嵌套属性依旧可读可写
-
应用场景:仅需对对象顶层属性保护时使用
-
用法
-
修改 App.vue,创建数据
<template> <div class="app"> <h2>Name: {{ personRO.name }}</h2> <h2>Age: {{ personRO.detail.age }}</h2> <p><button @click="changerName">Change Name</button></p> <p><button @click="changerAge">Change Age</button></p> <p><button @click="changerAll">Change All</button></p> </div> </template> <script setup lang="ts" name="App"> import { reactive } from 'vue' let person = reactive({ name: 'John', detail: { age: 18 } }) let personRO = person function changerName() { personRO.name = 'Mary' } function changerAge() { personRO.detail.age = 19 } function changerAll() { Object.assign(personRO, { name: 'Mary', detail: { age: 19 } }) } </script> -
引入
shallowReadonly并使用<script setup lang="ts" name="App"> import { reactive, shallowReadonly } from 'vue' // ... let personRO = shallowReadonly(person) // ... </script>此时,姓名不可以修改,年龄可以修改,所有信息可以通过自定义的
changeAll()方法修改
-
(3)toRaw & markRaw
a. toRaw
-
作用:获取一个响应式对象的原始对象
-
特点:返回的对象不是响应式的,不会触发视图更新
-
应用场景:将响应式对象传递到非 Vue 的库或外部系统时使用
-
用法
-
修改 App.vue,创建数据
<script setup lang="ts" name="App"> import { reactive } from 'vue' let person = reactive({ name: 'John', age: 18 }) console.log(person) </script> -
引入
toRaw并使用<script setup lang="ts" name="App"> import { reactive, toRaw } from 'vue' let person = reactive({ name: 'John', age: 18 }) console.log(person) console.log(toRaw(person)) </script>
-
b. markRaw
-
作用:标记一个对象,使其永远不会变成响应式对象
-
应用场景:例如使用 Mock.js 插件时,为防止误把
mockjs变成响应式对象而使用 -
用法
-
修改 App.vue,创建数据
<script setup lang="ts" name="App"> let person = { name: 'John', age: 18 } </script> -
引入
markRaw并使用<script setup lang="ts" name="App"> import { reactive, markRaw } from 'vue' let person = markRaw({ name: 'John', age: 18 }) person = reactive(person) console.log(person) </script>此时,
person并未变成响应式对象
-
(4)customRef
-
作用:创建一个自定义的
ref,并对其依赖项跟踪和更新触发进行逻辑控制 -
用法
-
修改 App.vue,引入
customRef<template> <div class="app"> <h2>Message: {{ msg }}</h2> <input type="text" v-model="msg" /> </div> </template> <script setup lang="ts" name="App"> import { customRef } from 'vue' let msg = customRef(() => { return { get() {}, set() {} } }) </script>其中,
get()在变量msg被读取时调用;set()在变量msg被修改时调用 -
声明新变量
initMsg作为默认值在get()中返回<script setup lang="ts" name="App"> import { customRef } from 'vue' let initMsg = "Default" let msg = customRef(() => { return { get() { return initMsg }, set() {} } }) </script> -
对
msg的修改结果通过set()接收<script setup lang="ts" name="App"> // ... let msg = customRef(() => { return { // ... set(value) { console.log('set', value) } } }) </script> -
在
set()中修改initMsg<script setup lang="ts" name="App"> // ... let msg = customRef(() => { return { // ... set(value) { // console.log('set', value) initMsg = value } } }) </script> -
【核心内容】在
customRef的回调函数中,接收两个参数:track(跟踪)、trigger(触发)<script setup lang="ts" name="App"> // ... let msg = customRef((track, trigger) => { return { get() { track() return initMsg }, set(value) { initMsg = value trigger() } } }) </script>track():依赖项跟踪,告知 Vue 需要对变量msg持续关注,一旦变化立即更新trigger():更新触发,告知 Vue 变量msg发生了变化
-
可以在
set()通过设置延时函数实现指定时间后触发更新<script setup lang="ts" name="App"> // ... let msg = customRef((track, trigger) => { return { // ... set(value) { setTimeout(() => { initMsg = value trigger() }, 1000) } } }) </script> -
但是当输入过快时,会导致输入的数据被覆盖丢失,此时引入防抖方法
<script setup lang="ts" name="App"> // ... let timer: number let msg = customRef((track, trigger) => { return { // ... set(value) { clearTimeout(timer) timer = setTimeout(() => { initMsg = value trigger() }, 1000) } } }) </script>防抖原理:清除上一次
set()中生产的定时器,并以最新定时器为准 -
在 src 目录下新建 hooks 目录,其中新建 useMsgRef.ts,用于将上述自定义 ref 封装成 Hooks
import { customRef } from 'vue' export default function(initMsg: string, delaySecond: number) { let timer: number let msg = customRef((track, trigger) => { return { get() { track() return initMsg }, set(value) { clearTimeout(timer) timer = setTimeout(() => { initMsg = value trigger() }, delaySecond * 1000) } } }) return { msg } } -
修改 App.vue,使用上述 Hooks
<script setup lang="ts" name="App"> import useMsgRef from '@/hooks/useMsgRef' let { msg } = useMsgRef('Default', 1) </script>
-
0x07 新组件
(1)Teleport
-
是一种能够将组件 HTML 结构移动到指定位置的技术
-
举例:在页面上封装一个弹窗组件
目录结构:
graph TB src-->components & App.vue & main.ts components-->ModelDialog.vue-
修改 App.vue
<template> <div class="outer"> <h2>App Component</h2> <ModelDialog /> </div> </template> <script setup lang="ts" name="App"> import ModelDialog from './components/ModelDialog.vue' </script> <style scope> .outer { width: 80%; height: 500px; padding: 20px; background: #03deff; } </style> -
修改 ModelDialog.vue
<template> <button>Show Dialog</button> <div class="md"> <h2>Dialog Title</h2> <p>Content Here</p> <button>Close</button> </div> </template> <script setup lang="ts" name="ModelDialog"> </script> <style scope> .md { text-align: center; width: 50%; height: 30%; background: #fbff03; border: 3px solid black; } </style> -
设置弹窗的显示与隐藏
<template> <button @click="isShowDialog = true">Show Dialog</button> <div class="md" v-show="isShowDialog"> <h2>Dialog Title</h2> <p>Content Here</p> <button @click="isShowDialog = false">Close</button> </div> </template> <script setup lang="ts" name="ModelDialog"> import { ref } from 'vue' let isShowDialog = ref(false) </script> -
通过 CSS 调整弹窗位于视口正中央
<style scope> .md { // ... position: fixed; top: 5%; left: 50%; margin-left: -100px; } </style> -
修改 App.vue,通过 CSS 设置变灰
<style scope> .outer { // ... filter: saturate(0%); } </style>此时,弹窗并未按照第 4 步的设置显示在视口正中央,可以通过 Teleport 解决此问题
-
修改 ModelDialog.vue
<template> <button @click="isShowDialog = true">Show Dialog</button> <Teleport to="body"> <div class="md" v-show="isShowDialog"> <h2>Dialog Title</h2> <p>Content Here</p> <button @click="isShowDialog = false">Close</button> </div> </Teleport> </template>-
to属性用于指定移动的目的地,上述代码将弹窗内容移动到body标签中 -
此时,网页渲染后的 DOM 结构如下:
<!-- ... --> <body> <div id="app" data-v-app><!-- ... --></div> <!-- ... --> <div id="md"><!-- ... --></div> </body>
-
-
(2)Suspense
- 等待异步组件时,渲染一些额外的内容,改善用户体验
目录结构:
graph TB src-->components & App.vue & main.ts components-->Child.vue
-
修改 App.vue
<template> <div class="app"> <h2>App</h2> <Child /> </div> </template> <script setup lang="ts" name="App"> import Child from './components/Child.vue' </script> <style scope> .app { width: 80%; height: 500px; padding: 20px; background: #03deff; } </style> -
修改 Child.vue
<template> <div class="child"></div> </template> <script setup lang="ts" name="Child"> </script> <style scope> .child { width: 200px; height: 150px; background: #fbff03; } </style> -
添加异步任务
<script setup lang="ts" name="Child"> import axios from 'axios' let { data: { result: { content } } } = await axios.get("https://api.oioweb.cn/api/common/OneDayEnglish") console.log(content) </script>此时,子组件会在页面上“消失”,可以在 App.vue 中使用 Suspense 解决此问题
-
修改 App.vue,引入 Suspense 并使用
<template> <div class="app"> <h2>App</h2> <Suspense> <template #default> <Child /> </template> </Suspense> </div> </template> <script setup lang="ts" name="App"> import Child from './components/Child.vue' import { Suspense } from 'vue' </script>- Suspense 中预设了两个插槽,
default插槽用于展示异步完成的内容,fallback插槽用于展示异步进行中的内容 - 当网络状态不是很好时,子组件不会立即渲染完成,因此可以借助 Suspense 添加加载提示
- Suspense 中预设了两个插槽,
-
设置加载内容:Loading...
<template> <div class="app"> <h2>App</h2> <Suspense> <template #default> <Child /> </template> <template #fallback> <h2>Loading...</h2> </template> </Suspense> </div> </template>
(3)全局 API 转移到应用对象
graph LR A(Vue2 全局API<br/>Vue.xxx) --> B(Vue3 应用对象API<br/>app.xxx)
-
app.component
-
修改 main.ts,注册全局组件
import { createApp } from 'vue' import App from './App.vue' import Child from './components/Child.vue' const app = createApp(App) app.component('Child', Child) app.mount('#app') -
修改 App.vue,使用全局组件
<template> <div class="app"> <h2>App</h2> <Child /> </div> </template> <script setup lang="ts" name="App"> </script>
-
-
app.config
-
修改 main.ts,注册全局属性
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.config.globalProperties.x = 12345 declare module 'vue' { interface ComponentCustomProperties { x: number; } } app.mount('#app') -
修改 App.vue,使用全局属性
<template> <div class="app"> <h2>App</h2> <h4>{{ x }}</h4> </div> </template> <script setup lang="ts" name="App"> </script>实际开发中,不建议该使用方法,容易污染全局
-
-
app.directive
-
修改 main.ts,注册全局指令
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.directive('custom', (element, {value}) => { element.innerText += value element.style.fontSize = '50px'; }) app.mount('#app') -
修改 App.vue,使用全局指令
<template> <div class="app"> <h2>App</h2> <p v-custom="value">Hello,</p> </div> </template> <script setup lang="ts" name="App"> let value = "world!" </script>
-
-
app.mount
-
在 main.ts 中挂载应用
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.mount('#app')
-
-
app.unmount
-
在 main.ts 中卸载应用
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.mount('#app') setTimeout(() => { app.unmount() }, 3000)
-
-
app.use
-
在 main.ts 中安装插件,如 pinia
import { createApp } from 'vue' import App from './App.vue' import { PiniaVuePlugin } from 'pinia' const app = createApp(App) app.use(PiniaVuePlugin) app.mount('#app')
-
(4)非兼容性改变
详见官方文档
-End-
2024-11-11 浏览量 1000 次达成纪念

浙公网安备 33010602011771号