- UID
- 670997
- 積分
- 0
- 金币
- 0
- 精華
- 0
- 威望
- 0
- 贡献
- 0
- 閲讀權限
- 220
- 註冊時間
- 2009-6-4
- 最後登錄
- 2026-5-6
- 在線時間
- 0 小時
热心网友
- 金币
- 0
- 閲讀權限
- 220
- 精華
- 0
- 威望
- 0
- 贡献
- 0
- 在線時間
- 0 小時
- 註冊時間
- 2009-6-4
|
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)项目结构
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
(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
- 若需要一个对象类型、层级较浅的响应式数据,则选择任何一个都可以
- 若需要一个对象类型、层级较深的响应式数据,则推荐选择
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 和 createWebHistory
import {
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 模式
b. hash 模式
(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)编程式导航
-
重置 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)等
- 集中式:将所有需要管理的数据集中存放在一个容器中,相对的称为分布式
-
pinia 官网
(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)搭建环境
(4)存储与读取数据
以下涉及对文件、变量等命名的格式均符合 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
-
修改 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)概述
(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)自定义事件
-
修改 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 emitter
emitter.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
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
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 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
(1)shallowRef & shallowReactive
- 两种方法一般用于绕开深度响应,提高性能,加快访问速度
a. shallowRef
b. shallowReactive
(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
(3)toRaw & markRaw
a. toRaw
b. markRaw
(4)customRef
0x07 新组件
(1)Teleport
(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 添加加载提示
-
设置加载内容: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
-
app.unmount
-
app.use
(4)非兼容性改变
详见官方文档
-End-
2024-11-11 浏览量 1000 次达成纪念
2026-02-08 浏览量 2000 次达成纪念
来源:https://www.cnblogs.com/SRIGT/p/18062140 |
|