Vue3基础知识
目录
- Vue3基础知识
一、创建Vue3项目
1、创建方式
通过vue_cli创建
vue_cli
底层是基于webpack
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 执行创建命令
vue create vue_test
## 随后选择3.x
## Choose a version of Vue.js that you want to start the project with (Use arrow keys)
## > 3.x
## 2.x
## 启动
cd vue_test
npm run serve
通过vite创建
vite
是新一代前端构建工具,官网地址:https://vitejs.cn
npm create vue@latest
Need to install the following packages:
[email protected]
Ok to proceed? (y) y
Vue.js - The Progressive JavaScript Framework
√ 请输入项目名称: ... hello_vue3
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
√ 是否引入 Vue DevTools 7 扩展用于调试? (试验阶段) ... 否 / 是
正在初始化项目 ...\hello_vue3...
项目初始化完成,可执行以下命令:
cd hello_vue3
npm install
npm run dev
安装官方插件Vue-official
安装依赖:
npm install
2、项目文件
3、源码解析
main.ts
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
import { createApp } from 'vue'
: 从 Vue 模块中导入createApp
方法,该方法用于创建一个 Vue 应用程序实例。import App from './App.vue'
: 导入名为App.vue
的 Vue 组件,App.vue
相当于根组件。createApp(App).mount('#app')
: 通过调用createApp
方法并传入App
组件,创建一个 Vue 应用程序实例,并将其挂载到具有指定 ID (#app
) 的DOM元素上,这里是挂载到index.html的文件上。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
用于挂载main.ts中的应用实例
App.vue
<template>
<!-- HTML code here -->
<div class="app">
<h1>Hello Vue 3</h1>
</div>
</template>
<script lang="ts">
// js\ts code here
export default {
name: 'App'
}
</script>
<style>
/* CSS code here */
.app {
text-align: center;
background-color: antiquewhite;
}
</style>
二、Vue语法
1、 API风格
Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API。
选项式 API (Options API)
使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data
、methods
和 mounted
。选项所定义的属性都会暴露在函数内部的 this
上,它会指向当前的组件实例。
vue
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},
// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
}
},
// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
组合式 API (Composition API)
通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与<script setup>
搭配使用。这个 setup
attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup>
中的导入和顶层变量/函数都能够在模板中直接使用。
下面是使用了组合式 API 与 <script setup>
改造后和上面的模板完全一样的组件:
vue
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。
选项式 API 以“组件实例”的概念为中心 (即上述例子中的 this
),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。
组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。
目前vue3更推荐组合式API,下面内容全部采用组合式API
2、setup
setup简介
setup
是Vue3
中一个新的配置项,值是一个函数,它是 Composition API
“表演的舞台”,组件中所用到的:数据、方法、计算属性、监视…等等,均配置在setup
中。
option api写法
<template>
<div>
<p>{{ message }}</p>
<button @click="logMessage">按钮</button>
</div>
</template>
<script lang="ts">
export default {
name: 'Test',
data () {
return {
message = 'this is message'
}
},
methods: {
logMessage () {
this.message = 'new message'
console.log(message)
}
}
}
</script>
composition api写法
<template>
<div>
<p>{{ message }}</p>
<button @click="logMessage">按钮</button>
</div>
</template>
<script lang="ts">
export default {
name: 'Test',
setup(){
const message = 'this is message'
const logMessage = ()=>{
console.log(message)
}
// 必须return才可以
return {
message,
logMessage
}
}
}
</script>
在setup函数中写的数据和方法需要在末尾以对象的方式return,才能给模版使用
执行时机:在beforeCreate钩子之前执行,所以在setup
函数中没有this,this此时还未定义
注意:data、methods可以和setup同时存在,且data、methods中可以读取setup中的数据,但setup无法读取data、methods中的数据。
setup语法糖
script标签添加 setup
标记,不需要再写导出语句,默认会添加导出语句
简便写法
<script lang="ts">
export default {
name: 'Test',
</script>
<script lang="ts" setup>
const message = 'this is message'
const logMessage = ()=>{
console.log(message)
}
</script>
setup中无法使用name等与setup平级的属性,一旦需要就得再添加一个普通的
这样就会存在两个
defineOptions后面会讲到,这里提供一个插件可以直接在script中自定义name
1、在项目中导入插件
npm i vite-plugin-vue-setup-extend -D
2、在vite.config.ts中增加配置引入
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [ VueSetupExtend() ]
})
3、在script
中直接定义
<script lang="ts" setup name="TestSB">
const message = 'this is message'
const logMessage = ()=>{
console.log(message)
}
</script>
3、响应式数据
在Vue2中,写在data
中,return里的数据都是响应式的
Vue3中,可以通过ref
和reactive
实现数据响应式
ref
接收基本类型或者对象类型的数据传入并返回一个响应式的对象
<script setup>
// 导入
import { ref } from 'vue'
// 执行函数 传入参数 变量接收
const count = ref(10)
const setCount = ()=>{
// 错误:count++
// 修改数据更新视图必须加上.value
count.value++
}
</script>
<template>
<button @click="setCount">{{ count }}</button>
</template>
返回值是一个RefImpl
的实例对象,简称ref对象
或ref
,ref
对象的value
属性是响应式的。
由于返回的都是一个对象,数据都被存储在对象中的value属性中,所以在函数中数值时,修改的是对象value属性,但在模板中不需要操作到具体的value属性,因为模板会默认帮你解析value值。
还有一个注意点,ref定义的数据作为结构体中的一个字段时,操作这个字段时也不用.value
,系统会默认帮你结构。
若ref
接收的是对象类型,内部其实也是调用了reactive
函数。
reactive
接受对象类型数据的参数传入并返回一个响应式的对象
<script setup>
// 导入
import { reactive } from 'vue'
// 执行函数 传入参数 变量接收
const state = reactive({
msg:'this is msg',
num: 250
})
const setSate = ()=>{
// 修改数据更新视图
state.msg = 'this is new msg'
}
</script>
<template>
{{ state.msg }}
<button @click="setState">change msg</button>
</template>
返回值是一个Proxy
的实例对象,简称:响应式对象。
ref对比 reactive
- 都是用来生成响应式数据
- 不同点
- reactive不能处理简单类型的数据
- reactive重新分配一个新对象,会失去响应式(可以使用
Object.assign
去整体替换) - ref参数类型支持更好,但是必须通过.value做访问修改
- ref函数内部的实现依赖于reactive函数
- 使用原则
- 若需要一个基本类型的响应式数据,必须使用ref
- 若需要一个响应式对象,层级不深,ref、reactive都可以。
- 若需要一个响应式对象,且层级较深,推荐使用reactive
4、toRef和toRefs
将一个响应式对象中的每一个属性,转换为ref
对象。
toRefs
与toRef
功能一致,但toRefs
可以批量转换。
<template>
<div class="person">
<h2>name: {{ name }}</h2>
<h2>Age: {{ age }} - {{ age1 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
</div>
</template>
<script lang="ts" setup name="Person123">
import { reactive, toRefs, toRef } from 'vue'
const person = reactive({
name: 'John',
age: 30,
})
// let { name, age } = person 失去响应式
let { name, age } = toRefs(person)
let age1 = toRef(person, 'age')
console.log(toRefs(person))//{name: ObjectRefImpl, age: ObjectRefImpl}
console.log(name)
console.log(age)
function changeName() {
name.value += '-'
console.log(name.value, person.name)
console.log(name)
}
function changeAge() {
age.value += 1
age1.value += 1
console.log(age)
}
</script>
5、computed计算属性
只读的计算属性
概念
基于现有的数据,计算出来的新属性。 依赖的数据变化,自动重新计算。
语法
- 声明在
computed
中,一个计算属性对应一个函数 - 使用起来和普通属性一样使用 {{ 计算属性名}}
注意
- computed声明的函数和普通数据项是同级的
- computed声明虽然是函数的写法,但他依然是一个属性
- 使用computed声明的函数和使用普通数据是一样的用法
<template>
<div class="person">
姓:<input type="text" v-model="firstName"> <br>
名:<input type="text" v-model="lastName"> <br>
全名:<span>{{ firstName.slice(0,1).toUpperCase() + firstName.slice(1) }} - {{ lastName }}</span> <br> <!-- 应该尽可能让模板变得简单,此处写法不推荐 -->
全名:<span> {{ fullName }}</span> <br>
全名:<span> {{ fullName }}</span> <br>
全名:<span> {{ fullName }}</span> <br>
全名:<span> {{ fullName2() }}</span> <br>
全名:<span> {{ fullName2() }}</span> <br>
全名:<span> {{ fullName2() }}</span> <br>
</div>
</template>
<script lang="ts" setup name="Person123">
import { ref, computed } from 'vue'
const firstName = ref('zhang')
const lastName = ref('san')
function fullName2 () {
console.log('函数打印一次')
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + ' - ' + lastName.value
}
const fullName = computed(() => {
console.log('计算属性打印一次')
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + ' - ' + lastName.value
})
打印结果
计算属性打印一次
函数打印一次
函数打印一次
函数打印一次
用computed声明的函数,无论用几次,只要数据没变,它都只会计算一次,computed会将计算的结果缓存。
普通的函数方法,调几次,执行几次
可写的计算属性
上述写法,computed
属性只能读取,无法修改
提供set方法可修改computed内容
<template>
<div class="person">
姓:<input type="text" v-model="firstName"> <br>
名:<input type="text" v-model="lastName"> <br>
<button @click="changFullName">改名</button> <br>
全名:<span> {{ fullName }}</span> <br>
</div>
</template>
<script lang="ts" setup name="Person123">
import { ref, computed } from 'vue'
const firstName = ref('zhang')
const lastName = ref('san')
// const fullName = computed(() => {
// console.log('计算属性打印一次')
// return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + ' - ' + lastName.value
// })
const fullName = computed({
// 读取
get(){
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + ' - ' + lastName.value
},
// 修改
set(val){ // val='li-si'
console.log('有人修改了fullName',val)
firstName.value = val.split('-')[0]
lastName.value = val.split('-')[1]
}
})
const changFullName = () => {
fullName.value = 'li-si'
}
</script>
set
携带一个参数val
,val
表示修改后的值
6、watch监视数据变化
作用
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
watch()
默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
第一个参数是侦听器的源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- …或是由以上类型的值组成的数组
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,支持以下这些选项:
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush
:调整回调函数的刷新时机。参考回调的刷新时机及watchEffect()
。onTrack / onTrigger
:调试侦听器的依赖。参考调试侦听器。once
:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。
监视ref定义数据
<template>
<div class="person">
<h1>情况一:监视【ref】定义的【基本类型】数据</h1>
<h2>当前求和为:{{sum}}</h2>
<button @click="changeSum">点我sum+1</button>
<h1>情况二:监视【ref】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,watch} from 'vue'
// 数据
let sum = ref(0)
// 方法
function changeSum(){
sum.value += 1
}
// 监视【ref】定义的【基本类型】数据
// watch返回一个函数,类似计数器,可以在里面关闭监视
const stopWatch = watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
//大于10时停止监视
if(newValue >= 10){
stopWatch()
}
})
</script>
<script lang="ts" setup name="Person">
import {ref,watch} from 'vue'
// 数据
let person = ref({
name:'张三',
age:18
})
// 方法
function changeName(){
person.value.name += '~'
}
function changeAge(){
person.value.age += 1
}
function changePerson(){
person.value = {name:'李四',age:90}
}
/*
监视【ref】定义的【对象类型】数据,监视的是对象的地址值,
若想监视对象内部属性的变化,需要手动开启深度监视
watch的第一个参数是:被监视的数据
watch的第二个参数是:监视的回调
watch的第三个参数是:配置对象(deep、immediate等等.....)
*/
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{deep:true, immediate:true})
</script>
注意:
- 若修改的是
ref
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。 - 若修改整个
ref
定义的对象,newValue
是新值,oldValue
是旧值,因为不是同一个对象了。
监视reactive定义数据
<template>
<div class="person">
<h1>情况三:监视【reactive】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
<hr>
<h2>测试:{{obj.a.b.c}}</h2>
<button @click="test">修改obj.a.b.c</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18
})
let obj = reactive({
a:{
b:{
c:666
}
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changePerson(){
Object.assign(person,{name:'李四',age:80})
}
function test(){
obj.a.b.c = 888
}
// 监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
})
watch(obj,(newValue,oldValue)=>{
console.log('Obj变化了',newValue,oldValue)
})
</script>
注意:
无论是修改reacitve对象中的属性还是修改对象,newValue
和 oldValue
都是新值,因为它们是同一个对象。
监视某个具体属性
监视ref或reactive定义的对象类型数据中的某个属性,注意点如下:
- 若该属性值不是对象类型,需要写成函数形式。
- 若该属性值是依然是对象类型,可直接编写,也可写成函数,建议写成函数。
总结:
监视的要是对象里的属性,那么最好写函数式,注意,若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
代码解释
<template>
<div class="person">
<h1>监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeC1">修改第一台车</button>
<button @click="changeC2">修改第二台车</button>
<button @click="changeCar">修改整个车</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changeC1(){
person.car.c1 = '奥迪'
}
function changeC2(){
person.car.c2 = '大众'
}
function changeCar(){
person.car = {c1:'雅迪',c2:'爱玛'}
}
</script>
若要监视person对象中某个基本类型数据,比如name
如果把person.name传给watch,会直接报错:Invalid watch source: 张三 A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types
需要写成函数形式,才能避免报错
且由于是传递的具体属性,所以
newValue
是新值,oldValue
是旧值,不会出现二者一致
<script lang="ts" setup name="Person">
// watch(person.name,(newValue,oldValue)=>{
// console.log('person.name变化了',newValue,oldValue)
// })
// 监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
watch(()=> person.name,(newValue,oldValue)=>{
console.log('person.name变化了',newValue,oldValue)
})
</script>
若要监视person对象中的对象类型,比如car
<script lang="ts" setup name="Person">
// 监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写
watch(person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
})
</script>
把person.car传给watch可以监视person.car中对象的变化
点击changeC1(改c1的值)和changeC2(改c2的值)都能监视person.car的变化,且由于car的对象不变,
newValue
和oldValue
都是新值点击changeCar(修改car值)无法监视到变化,且无论是否启用
deep:true
,都无法监视因为changeCar函数中,person.car = {c1:‘雅迪’,c2:‘爱玛’},已经将person.car的原对象替换掉了,所以watch监视不到变化
<script lang="ts" setup name="Person">
// 监视响应式对象中的某个属性,属性是对象类型的,采用函数写法
watch(()=> person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
写成函数形式,点击changeCar可以监视到变化
监听的是地址,所以能监视到car整体的变化
但要加上
deep:true
,才能监听car中c1或c2单个的变化
监视多个属性
监视多个数据,包括简单类型,对象类型,可以传递一个数组
<script lang="ts" setup name="Person">
// 监视,情况五:监视上述的多个数据
watch([()=>person.name,()=>person.car.c1],(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
watch([()=>person.name,person.car],(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
修改数组中的值时,能监听变化
7、watchEffect
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。
第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。
<template>
<div class="person">
<h2>水温:{{ temp }}</h2>
<h2>水位:{{ height }}</h2>
<button @click="addTemp">水温+10</button>
<button @click="addHeight">水位+10</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref, watch, watchEffect } from 'vue'
const temp = ref(0)
const height = ref(0)
function addTemp() {
temp.value += 10
}
function addHeight() {
height.value += 10
}
// 用watch实现,需要明确的指出要监视:temp、height
/* watch([temp, height], (value) => {
const [newTemp, newHeight] = value
if (newTemp >= 60 || newHeight >= 80) {
console.log('温度过高或水位过高,请注意!')
}
}) */
// watchEffect不用具体监听某一个属性,默认帮你监听
watchEffect(() => {
//函数启动默认立即执行一次
console.log('立即执行一次')
if (temp.value >= 60 || height.value >= 80) {
console.log('温度过高或水位过高,请注意!')
}
})
watch
对比watchEffect
- 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
:要明确指出监视的数据watchEffect
:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
8、标签ref属性(模板引用)
作用:模板引用
虽然 Vue 的声明性渲染模型已抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。
普通HTML元素上ref
用在普通DOM标签上,获取的是DOM节点。
在Vue中不推荐使用id获取DOM元素,推荐使用声明同名ref的方式
<template>
<div class="person">
<h2 id="title1">生活很大</h2>
<h2 ref="title2">宇宙很大</h2>
<button @click="test">点击</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref } from 'vue'
// 创建一个title2,用于存储ref标记的内容
const title2 = ref()
const test = () => {
//不推荐
console.log(document.getElementById('title1'))
//推荐
console.log(title2.value)
}
</script>
打印结果
<h2 id=‘title1’>生活很大</h2>
<h2>宇宙很大</h2>
使用获取id的方式来获取DOM元素时,一旦其它组件中有同名id=‘title1’,Vue中就无法区分具体是哪个title1
比如在父组件中也有一个相同id,你在子页面中获取同名元素时
<template>
<h2 id="title1">人生太长</h2>
<Person />
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
</script>
打印结果
<h2 id=‘title1’> 人生太长</ h2>
<h2>宇宙很大</h2>
Vue无法识别你要的是哪个组件中的id,所以只能打印先加载的
ref则能避免这个问题,但要提前声明一个容器,用于存储ref标记的内容
组件元素上的ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例
子组件
<script lang="ts" setup>
const a = ref(1)
const b = ref(2)
const c = ref(3)
defineExpose({
a,
b,
c
})
</script>
父组件
<template>
<Person ref="ren" />
<button @click="test">获取子组件</button>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Person from './components/Person.vue'
const ren = ref()
const test = () => {
console.log(ren.value)
}
</script>
使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露
9、Props
父组件传递数据给子组件,需要子组件显式声明它所接受的 props
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
除了使用字符串数组来声明 prop 外,还可以使用对象的形式,对象形式要标明数据的类型
<script setup>
defineProps({
title: String,
likes: Number
})
</script>
当不确定父组件是否有数据传递时,可以加个?号
<script setup lang="ts">
defineProps<{
title?: string
likes?: number
}>()
</script>
可以使用自定义类型进行限制
export interface PersonInter {
id: string;
name: string;
age: number;
gender?: string;
}
父组件
<template>
<Person :list="personList" />
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import { reactive } from 'vue'
import { type Persons } from './types'
let personList = reactive<Persons>([
{ id: 'e98219e12', name: '张三', age: 18 },
{ id: 'e98219e13', name: '李四', age: 19 },
{ id: 'e98219e14', name: '王五', age: 20 }
])
</script>
子组件
<script lang="ts" setup>
import { type Persons } from '@/**'
defineProps<{list?:Persons}>()
</script>
当数据不存在时,指定默认值
<script lang="ts" setup>
import { type Persons } from '@/**'
import { withDefaults } from 'vue'
// 限制类型+默认值
withDefaults(defineProps<{list?:Persons}>(),{
list:() => [{id:'sdfdasw123',name:'张非',age:18}]
})
</script>
10、生命周期钩子
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
Vue2和Vue3的生命周期有点不同
Vue2
创建阶段:
beforeCreate
、created
挂载阶段:
beforeMount
、mounted
更新阶段:
beforeUpdate
、updated
销毁阶段:
beforeDestroy
、destroyed
官网Vue2的生命周期图示可以自行参考官网:https://v2.cn.vuejs.org/v2/guide/instance.html#%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%9B%BE%E7%A4%BA
Vue3
创建阶段:
setup
挂载阶段:
onBeforeMount
、onMounted
更新阶段:
onBeforeUpdate
、onUpdated
卸载阶段:
onBeforeUnmount
、onUnmounted
Vue3的生命周期:
<template>
<div class="person">
<h2> {{ name }}</h2>
<button @click="changeName">按钮</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue';
let name = ref('张三')
let changeName = () => {
name.value += '-'
}
console.log('setup')
// 生命周期钩子
onBeforeMount(()=>{
console.log('挂载之前')
})
onMounted(()=>{
console.log('子--挂载完毕')
})
onBeforeUpdate(()=>{
console.log('更新之前')
})
onUpdated(()=>{
console.log('更新完毕')
})
onBeforeUnmount(()=>{
console.log('卸载之前')
})
onUnmounted(()=>{
console.log('卸载完毕')
})
</script>
每个组件都有生命周期,子组件加载完毕才会加载父组件
11、自定义Hooks(组合式函数)
在Vue3中,Hooks
是基于Composition API
实现的,它允许我们在组件的逻辑代码中更好地组织和复用代码。
Hooks
本质上是一组可复用的函数,它们可以写入Vue组件的生命周期,让我们能够在组件的不同生命周期阶段执行特定的逻辑。
当多个组件需要共享相同的逻辑时,我们可以将这些逻辑封装成一个Hook
,然后在需要的组件中导入并使用它。这样可以避免代码重复,提高代码的复用性。
在src目录下,建立hooks目录,具体的文件名,一般什么数据用 use数据名
命名
useName.ts
import { ref } from 'vue'
export default () => {
const name = ref('张三')
const changeName = () => {
name.value += '-'
}
return { name, changeName }
}
在组件中使用该Hook:
<template>
<div class="person">
<h2> {{ name }}</h2>
<button @click="changeName">按钮</button>
</div>
</template>
<script lang="ts" setup name="Person">
import useName from '@/hooks/useName'
const { name, changeName } = useName()
</script>
详细参考文档:https://segmentfault.com/a/1190000044673851
12、自定义指令(事件)
除了 Vue 内置的一系列指令 (比如 v-model
或 v-show
) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。
- 原生事件:
- 事件名是特定的(
click
、mosueenter
等等) - 事件对象
$event
: 是包含事件相关信息的对象(pageX
、pageY
、target
、keyCode
)
- 事件名是特定的(
- 自定义事件:
- 事件名是任意名称
- 事件对象
$event
: 是调用emit
时所提供的数据,可以是任意类型!!!
-
示例:
<!--在父组件中,给子组件绑定自定义事件:--> <Child @send-game="game = $event"/> <!--注意区分原生事件与自定义事件中的$event--> <button @click="game = $event">测试</button>
//子组件中,触发事件: this.$emit('send-game', 具体数据)
三、路由
服务端路由指的是服务器根据用户访问的 URL 路径返回不同的响应结果。当我们在一个传统的服务端渲染的 web 应用中点击一个链接时,浏览器会从服务端获得全新的 HTML,然后重新加载整个页面。
然而,在单页面应用中,客户端的 JavaScript 可以拦截页面的跳转请求,动态获取新的数据,然后在无需重新加载的情况下更新当前页面。这样通常可以带来更顺滑的用户体验,尤其是在更偏向“应用”的场景下,因为这类场景下用户通常会在很长的一段时间中做出多次交互。
1、Vue Router
Vue Router 是 Vue.js的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。
安装
npm install vue-router@4
路由配置文件
import { createRouter, createWebHistory } from "vue-router"
import Home from "@/views/Home.vue"
import About from "@/views/About.vue"
import News from "@/views/News.vue"
const router = createRouter({
history: createWebHistory(),
routes:[
{
path: '/home',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/news',
component: News
},
]
})
export default router
在main.ts中配置router
// 导入名为createApp的函数,用于创建Vue应用程序
import { createApp } from 'vue'
// 从'./App.vue'文件中导入App组件
import App from './App.vue'
// 引入路由器
import router from './router'
// 创建实例
const app = createApp(App)
// 使用
app.use(router)
// 挂载
app.mount('#app')
在vue文件中应用路由组件
<template>
<div class="app">
<h2 class="title">Vue路由测试</h2>
<!-- 导航区 -->
<div class="navigate">
<RouterLink to="/home" active-class="active">首页</RouterLink>
<RouterLink to="/news" active-class="active">新闻</RouterLink>
<RouterLink to="/about" active-class="active">关于</RouterLink>
</div>
<!-- 展示区 -->
<div class="main-content">
<!-- 此处是要展示的各种组件 -->
<router-view></router-view>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
</div>
</div>
</template>
<script lang="ts" setup>
import { RouterView, RouterLink } from 'vue-router'
</script>
<style scoped>
//自行配置
</style>
路由组件与一般组件:
- 路由组件:靠路由规则渲染出来的,通常存放于
pages
或views
文件夹 - 一般组件:用标签形式声明的,通常存放于
components
文件夹
路由组件默认是被卸载掉的,只有在需要的时候,点击跳转,才会去挂载组件
2、路由器工作模式
history模式
用 createWebHistory()
创建history模式(官网上也叫 HTML5 模式),推荐使用这个模式
当使用这种历史模式时,URL 会看起来很 “正常”,例如 https://example.com/user/id
。
由于我们的应用是一个单页的客户端应用,如果没有适当的服务器配置,用户在浏览器中直接访问 https://example.com/user/id
,就会得到一个 404 错误。
要解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 index.html
相同的页面。
优点:URL更加美观,不带有#,更接近传统的网站URL。
缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误。
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
//...
],
})
Hash模式
hash 模式是用 createWebHashHistory()
创建的
它在内部传递的实际 URL 之前使用了一个哈希字符(#
)。由于这部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。
优点:兼容性更好,因为不需要服务器端处理路径。
缺点:URL带有#不太美观,且在SEO优化方面相对较差。
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
//...
],
})
3、to的写法
RouterLink
里面to用于指示跳转的路径,有两种写法
<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>
<!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>
4、命名路由
在路由配置文件中,除了path,还可以为任何路由提供 name
。
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/user',
name: 'user',
component: User,
},
]
})
要链接到一个命名的路由,可以向 router-link
组件的 to
属性传递一个对象,然后通过name直接跳转:
<router-link :to="{ name: 'user'}">
User
</router-link>
5、嵌套路由
一些应用程序的 UI 由多层嵌套的组件组成。如下图:
要将组件渲染到这个嵌套的 router-view
中,我们需要在路由中配置 children
import { createRouter, createWebHistory } from "vue-router"
import News from "@/views/News.vue"
import Detail from "@/views/Detail.vue"
const router = createRouter({
history: createWebHistory(),
routes:[
{
name: 'xinwen',
path: '/news',
component: News,
children: [
{
path: 'detail',
component: Detail,
}
]
},
]
})
export default router
注意,以 /
开头的嵌套路径将被视为根路径。children
中path不用加 /
点击路由跳转
<router-link to="/news/detail">xxxx</router-link>
<!-- 或 -->
<router-link :to="{path:'/news/detail'}">xxxx</router-link>
同时注意留一个<router-view>
组件用于展示
6、路由传参
query参数
在<router-link>
的to
里面传入
<router-link to="/news/detail?id=1&title=天气&content=今天雨天">xxxx</router-link>
在子路由组件中,用route
接收
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印query参数,query是一个结构体,包含传递的参数
console.log(route.query)
传递动态参数
<!-- to的字符串写法,携带动态参数 -->
<RouterLink :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">
{{ news.title }}
</RouterLink>
<!-- 跳转并携带query参数(to的对象写法) -->
<RouterLink
:to="{
//name:'xiangqing', //用name也可以跳转,看不懂请忽略
path:'/news/detail',
query:{
id:news.id,
title:news.title,
content:news.content
}
}"
>
{{news.title}}
</RouterLink>
params参数
同样在<router-link>
的to
里面的路径写入参数,不过不用再使用key-value形式,也不需要?
和&
直接在/
后写入要传递的值
<router-link to="/news/detail/1/天气/今天晴天">xxxx</router-link>
在路由配置项中,在path
里面,声明相应的参数名去接收传递的参数,也就是需要在规则中占位
const router = createRouter({
history: createWebHistory(),
routes:[
{
name: 'xinwen',
path: '/news',
component: News,
children: [
{
name: 'xiangqing'
path: 'detail/:id/:title/:content?',
component: Detail,
}
]
},
]
})
同样在子路由组件中,用route
接收,在route
中的params
参数接收
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印params参数,params也是一个结构体,包含传递的参数
console.log(route.params)
同样可以传递动态参数
<!-- 跳转并携带params参数(to的字符串写法) -->
<RouterLink :to="`/news/detail/${news.id}/${news.title}/${news.content}`">{{ news.title }} </RouterLink>
<!-- 跳转并携带params参数(to的对象写法),对象写法,只能用name,无法用path -->
<RouterLink
:to="{
name:'xiangqing', //用name跳转
params:{
id:news.id,
title:news.title,
content:news.title
}
}"
>
{{news.title}}
</RouterLink>
注意点:
当你传参数时,不确定是否有参数,可以在路由配置项中,在path
里面,声明相应的参数名去接收传递的参数的时候加?
符号
7、路由器的prop配置
让路由组件更方便的收到参数(可以将路由参数作为props
传给组件)
1、props的布尔值写法
使用params传参时
<RouterLink
:to="{
name:'xiangqing', //用name跳转
params:{
id:news.id,
title:news.title,
content:news.title
}
}"
>
{{news.title}}
</RouterLink>
路由配置中,开启props
const router = createRouter({
history: createWebHistory(),
routes:[
{
name: 'xinwen',
path: '/news',
component: News,
children: [
{
name: 'xiangqing'
path: 'detail/:id/:title/:content?',
component: Detail,
// 将路由收到的所有params参数作为props传给路由组件
props: true
}
]
},
]
})
子路由组件接收参数
<template>
<ul>
<!-- <li>编号:{{ route.params.id }}</li>
<li>标题:{{ route.params.title }}</li>
<li>内容:{{ route.params.content }}</li> -->
<li>编号:{{ id }}</li>
<li>标题:{{ title }}</li>
<li>内容:{{ content }}</li>
</ul>
</template>
<script setup lang="ts">
// 旧方法
// import {useRoute} from 'vue-router'
// const route = useRoute()
// 直接通过defineProps接收
defineProps(['id', 'title', 'content'])
</script>
2、props的函数写法和对象写法
使用query传参时
<RouterLink
:to="{
//name:'xiangqing', //用name也可以跳转
path:'/news/detail',
query:{
id:news.id,
title:news.title,
content:news.content
}
}"
>
{{news.title}}
</RouterLink>
路由配置,自定义传递的props组件,注意,该方法不仅可以传递query,还可以传递params
const router = createRouter({
history: createWebHistory(),
routes:[
{
name: 'xinwen',
path: '/news',
component: News,
children: [
{
name: 'xiangqing'
path: 'detail/:id/:title/:content?',
component: Detail,
// 函数写法:自己决定将什么作为props给路由组件
props(route) {
return route.query
}
// props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件
// 传递的数据是不变的,使用场景少
// props:{a:1,b:2,c:3},
}
]
},
]
})
接收参数时
<template>
<ul>
<li>编号:{{ id }}</li>
<li>标题:{{ title }}</li>
<li>内容:{{ content }}</li>
</ul>
</template>
<script setup lang="ts">
// 直接通过defineProps接收
defineProps(['id', 'title', 'content'])
</script>
8、replace属性
控制路由跳转时操作浏览器历史记录的模式
浏览器的历史记录有两种写入方式:分别为push
和replace
:
push
是追加历史记录(默认值)。replace
是替换当前记录。
用法
<RouterLink replace>路由组件</RouterLink>
9、编程式导航
除了使用 <router-link>
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
在组件内部,组合式 API中,可以通过调用 useRouter()
来访问路由器。
想要导航到不同的 URL,可以使用 router.push
方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。
还有replace
方法,它的作用类似于 router.push
,唯一不同的是,它在导航时不会向 history 添加新记录,正如它的名字所暗示的那样——它取代了当前的条目。
当你点击 <router-link>
时,内部会调用这个方法,所以点击 <router-link :to="...">
相当于调用 router.push(...)
:
声明式 | 编程式 |
---|---|
<router-link :to=“…”> | router.push(…) |
<router-link :to=“…” replace> | router.replace(…) |
在setup中实现跳转
<script lang="ts" setup>
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/users')
// 字符串路径
router.push('/users/eduardo')
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })
</script>
总结:<router-link :to="...">
中,:to
中的语法与router.push()
中的语法一致。
10、重定向redirect
将特定的路径,重新定向到已有路由。
默认进首页,进行重定向
import { createRouter, createWebHistory, } from "vue-router"
const router = createRouter({
history: createWebHashHistory(),
routes:[
{
path: '/',
redirect: '/home'
},
{
name: '首页',
path: '/home',
component: Home
},
]
})
四、Pinia
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。
类似于Vue2中的Vuex,与 Vuex 相比,Pinia 不仅提供了一个更简单的 API,也提供了符合组合式 API 风格的 API,最重要的是,搭配 TypeScript 一起使用时有非常可靠的类型推断支持。
1、安装pinia
安装
yarn add pinia
# 或者使用 npm
npm install pinia
使用
import { createApp } from 'vue'
import App from './App.vue'
// 导入
import { createPinia } from 'pinia'
const app = createApp(App)
// 创建实例
const pinia = createPinia()
// 安装使用
app.use(pinia)
app.mount('#app')
2、存储数据
通常会在src下建一个store
包,用来专门存储数据,需用共享的某一类数据放在同一个文件下
定义store
Store 是用 defineStore()
定义的,它的第一个参数要求是一个独一无二的名字,第二个参数可接受两类值:Setup 函数或 Option 对象,这里只讲Setup函数方法。
state 都是你的 store 的核心。也就是真正存储数据的地方
import { defineStore } from "pinia"
export const useCounterStore = defineStore("count", {
state() {
return {
// 定义状态
sum: 6,
name: 'zhangsan',
}
}
})
使用store
在需用使用数据的vue页面中
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量
const counterStore = useCounterStore()
console.log(counterStore.sum)
</script>
3、修改数据
修改数据有多种方式
import { defineStore } from "pinia"
export const useCountStore = defineStore("count", {
actions: {
increment(value:number) {
// 不仅可以修改数据,还可以编写一些业务逻辑
if (this.sum<10){
this.sum += value
}
}
},
state() {
return {
// 定义状态
sum: 6,
name: "张三",
age: 18
}
}
})
<template>
<div class="count">
<h2>求和:{{ countStore.sum }}</h2>
<h3>{{ countStore.name }} - {{ countStore.age }}</h3>
<select v-model.number="n">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="add">+</button>
<button @click="minus">-</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { useCountStore } from "@/store/count"
const countStore = useCountStore()
console.log(countStore)
const n = ref(1)
const add = () => {
// 第一种修改方式,直接修改
// countStore.sum = 222
// countStore.name = '徐某'
// countStore.age = 28
// 第二种修改方式:批量修改
// countStore.$patch({
// sum: 666,
// name: '江某',
// age: 8
// })
// 借助action修改
countStore.increment(n.value)
}
const minus = () => {
}
</script>
4、Action
Action 相当于组件中的 method。它们可以通过 defineStore()
中的 actions
属性来定义,并且它们也是定义业务逻辑的完美选择。
action 可以通过 this
访问整个 store 实例,并支持完整的类型标注。
可以看看上文修改数据的例子:
import { defineStore } from "pinia"
export const useCountStore = defineStore("count", {
actions: {
increment(value:number) {
// 不仅可以修改数据,还可以编写一些业务逻辑
if (this.sum<10){
this.sum += value
}
}
},
state() {
return {
// 定义状态
sum: 6,
name: "张三",
age: 18
}
}
})
action
可以是异步的,你可以在它们里面 await
调用任何 API,以及其他 action
5、storeToRefs
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
。它将为每一个响应式属性创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:
<script setup>
import { toRefs } from "vue"
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
// 不推荐
const { name, doubleCount } = toRefs(store)
console.log('toRefs(store)会将所有数据和方法都转为响应式', toRefs(store))
</script>
6、Getter
当state
中的数据,需要经过处理后再使用时,可以使用getters
配置。
Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore()
中的 getters
属性来定义它们。推荐使用箭头函数,并且它将接收 state
作为第一个参数:
// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useCountStore = defineStore('count',{
// 动作
actions:{
/************/
},
// 状态
state: () => ({
count: 0,
}),
// 计算
getters: {
doubleCount: (state) => state.count * 2,
},
})
大多数时候,getter 仅依赖 state,不过,有时它们也可能会使用其他 getter。因此,即使在使用常规函数定义 getter 时,我们也可以通过 this
访问到整个 store 实例
在 TypeScript 中必须定义返回类型。这是为了避免 TypeScript 的已知缺陷,不过这不影响用箭头函数定义的 getter,也不会影响不使用 this
的 getter。
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注
return this.doubleCount + 1
},
},
})
可以直接访问 store 实例上的 getter 了
<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
<template>
<p>Double count is {{ store.doubleCount }}</p>
</template>
7、$subscribe监视数据
类似于 Vuex 的 subscribe 方法,你可以通过 store 的 $subscribe()
方法侦听 state 及其变化。比起普通的 watch()
,使用 $subscribe()
的好处是 subscriptions 在 patch 后只触发一次
利用$subscribe()
监视来将每一次获取到的数据更新到内存中
import { defineStore } from "pinia"
import axios from 'axios'
import { nanoid } from 'nanoid'
export const useGushiStore = defineStore("gushi", {
actions: {
async getPoetry () {
const { data:{ data: { sentence:count } }} = await axios.get('https://open.saintic.com/api/sentence/shuqing.huaigu.json')
console.log(count)
const obj = {id: nanoid(), count}
this.gushiList.push(obj)
}
},
state() {
return {
// 定义状态
gushiList: JSON.parse(localStorage.getItem('gushi') as string) || []
}
}
})
<template>
<div class="gushi">
<button @click="getGushi">获取句子</button>
<ul>
<li v-for="gushi in gushiStore.gushiList" :key="gushi.id">{{ gushi.count }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useGushiStore } from "@/store/gushi"
const gushiStore = useGushiStore()
const { gushiList } = storeToRefs(gushiStore)
gushiStore.$subscribe((mutation, state) => {
console.log(mutation, state)
localStorage.setItem('gushi',JSON.stringify(gushiList.value))
})
const getGushi = async () => {
gushiStore.getPoetry()
}
</script>
8、组合式写法
将上述例子转为组合式写法
import { defineStore } from "pinia"
import axios from 'axios'
import { nanoid } from 'nanoid'
import { reactive } from "vue"
export const useGushiStore = defineStore("gushi",() => {
const gushiList = reactive(JSON.parse(localStorage.getItem('gushi') as string) || [])
async function getPoetry() {
const { data:{ data: { sentence:count } }} = await axios.get('https://open.saintic.com/api/sentence/shuqing.huaigu.json')
console.log(count)
const obj = {id: nanoid(), count}
gushiList.push(obj)
}
return { getPoetry,gushiList }
})
五、组件通信
组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。
这就需要引入组件通信
1、父子互传_props
props
是使用频率最高的父子互传通信方式
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<p>书籍:{{ book }}</p>
<p v-if="game">子组件孝敬父组件的游戏:{{ game }}</p>
<Child :book="book" :sendGame="getGame"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from 'vue'
// 父传子数据
const book = ref('神们自己')
const game = ref('')
// 子接收父数据
const getGame = (value: string) => {
console.log('父组件接收到的游戏是:', value)
game.value=value
}
</script>
子组件:
<template>
<div class="child">
<h3>子组件</h3>
<p>游戏:{{ game }}</p>
<p>父组件给得书籍:{{ book }}</p>
<button @click="sendGame(game)">送游戏给父组件</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from 'vue'
const game = ref('对马岛之魂')
// 接收
defineProps(['book', 'sendGame'])
</script>
从上面可以看出来,父传子是直接传递,而子传父,是先由父传子一个函数,而子在适当时调用这个函数
- 若 父传子:属性值是非函数。
- 若 子传父:属性值是函数。
2、子传父_通过自定义事件
利用自定义事件来实现通信,只能用于子传父
父组件
<template>
<div class="father">
<h3>父组件</h3>
<p v-if="game">子组件孝顺父组件的游戏:{{ game }}</p>
<Child @send-game="saveGame"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
//接收子组件元素
const game = ref('')
const saveGame = (value: string) => {
game.value = value
}
</script>
子组件
<template>
<div class="child">
<h3>子组件</h3>
<p>游戏:{{ game }}</p>
<button @click="handleClick">送游戏给父组件</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
const game = ref('哈迪斯')
const emit = defineEmits(['send-game'])
const handleClick = () => {
emit('send-game', game.value)
}
</script>
3、任意互传_mitt
可以实现任意组件间通信
安装mitt
npm i mitt
新建文件emitter.ts
,通常在放在src\utils\
下
// 引入mitt
import mitt from "mitt";
// 创建emitter
const emitter = mitt()
/*
// 绑定事件
emitter.on('abc',(value)=>{
console.log('abc事件被触发',value)
})
emitter.on('xyz',(value)=>{
console.log('xyz事件被触发',value)
})
setInterval(() => {
// 触发事件
emitter.emit('abc',666)
emitter.emit('xyz',777)
}, 1000);
setTimeout(() => {
// 清理事件
emitter.all.clear()
}, 3000);
*/
// 创建并暴露mitt
export default emitter
接收数据的组件中:绑定事件、同时在销毁前解绑事件:
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";
// 绑定事件
emitter.on('send-book',(value)=>{
console.log('send-book事件被触发',value)
})
//在组件卸载时
onUnmounted(()=>{
// 解绑事件
emitter.off('send-book')
})
提供数据的组件,在合适的时候触发事件
import emitter from "@/utils/emitter";
const book = ref('银河界区')
const sendBook = () => {
// 触发事件
emitter.emit('send-book',book.value)
}
4、父子互传_v-model
通过组件标签样式v-model,来父子通信
v-model底层原理
<!-- 使用v-model指令 -->
<input type="text" v-model="userName">
<!-- v-model的本质是下面这行代码 -->
<input
type="text"
:value="userName"
@input="userName =(<HTMLInputElement>$event.target).value"
//<HTMLInputElement>是ts语法,实质也就是$event.target.value
>
vue3中组件标签
中的v-model底层原理,注意,和vue2中的有区别
<!-- 组件标签上使用v-model指令 -->
<NoManSky v-model="userName"/>
<!-- 组件标签上v-model的本质 -->
<NoManSky :modelValue="userName" @update:model-value="userName = $event"/>
自定义一个组件NoManSky
(子组件)
<template>
<div class="box">
<!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
<!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
<input
type="text"
:value="modelValue"
@input="emit('update:model-value',(<HTMLInputElement>$event.target).value)"
>
</div>
</template>
<script setup lang="ts" name="AtguiguInput">
// 接收props
defineProps(['modelValue'])
// 声明事件
const emit = defineEmits(['update:model-value'])
</script>
当一个子组件需要用到多个v-model来实现传递时,可自定义名字来区分
父组件
<!-- 也可以更换value,例如改成abc-->
<NoManSky v-model:name="userName" v-model:pwd="password"/>
<!-- 上面代码的本质如下 -->
<NoManSky
:name="userName" @update:name="userName = $event"
:pwd="password" @update:pwd="password = $event"
/>
子组件
<template>
<input
type="text"
:value="name"
@input="emit('update:name',(<HTMLInputElement>$event.target).value)"
>
<br>
<input
type="text"
:value="pwd"
@input="emit('update:pwd',(<HTMLInputElement>$event.target).value)"
>
</template>
<script setup lang="ts" name="NoManSky">
defineProps(['name','pwd'])
const emit = defineEmits(['update:name','update:pwd'])
</script>
从 Vue 3.4 开始,v-model组件通信推荐的实现方式是使用 defineModel()
宏
父组件
<!-- Parent.vue -->
<Child v-model="countModel" />
子组件
<!-- Child.vue -->
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>Parent bound v-model is: {{ model }}</div>
</template>
defineModel()
返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:
- 它的
.value
和父组件的v-model
的值同步; - 当它被子组件变更了,会触发父组件绑定的值一起更新。
可以参考:Vue父子组件间双向数据绑定:v-model与dineModel详解
官网:组件 v-model
5、祖孙互传_$attrs
$attrs
用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)
$attrs
是一个对象,包含所有父组件传入的标签属性
$attrs
会自动排除props
中声明的属性,通俗的说,$attrs
会自动排除defineProps
已接收的变量,可自行接着vue插件观察
父组件
<template>
<div class="father">
<h3>父组件</h3>
<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
//v-bind="{x:100,y:200}" 等同于 :x="100" :y="200"
</div>
</template>
<script setup lang="ts">
import Child from './Child.vue'
import { ref } from "vue";
let a = ref(1)
let b = ref(2)
let c = ref(3)
let d = ref(4)
function updateA(value){
a.value = value
}
</script>
子组件
<template>
<div class="child">
<h3>子组件</h3>
<!-- <h4>a: {{ a }}</h4>
<h4>其他:{{ $attrs }}</h4> -->
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
// defineProps(['a'])
</script>
孙组件
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
<h4>d:{{ d }}</h4>
<h4>x:{{ x }}</h4>
<h4>y:{{ y }}</h4>
<button @click="updateA(6)">点我将爷爷那的a更新</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a','b','c','d','x','y','updateA'])
</script>
父传孙:父组件的数据传给子组件,子组件不通过defineProps
接收,所有的数据都在$attrs
中,用于传给孙组件,孙组件通过defineProps
接收
孙传父:孙组件调用父组件里的方法,在方法里面修改变量
祖孙互传用父、子、孙三个组件来表示,不要搞乱了
6、父子互传_$refs
和$parent
首先了解组件上的ref用法,上文已经讲过——ref组件用法
在组件元素上声明ref
接收子组件数据,通过$refs
获取多个子组件数据
子元素通过$parent
获取父组件数据
无论是父组件还是子组件,都需要通过defineExpose
将数据暴露出去才能被获取
属性 | 说明 |
---|---|
$refs | 值为对象,包含所有被ref 属性标识的DOM 元素或组件实例 |
$parent | 值为对象,当前组件的父组件实例对象 |
父组件
<template>
<div class="father">
<h3>父组件</h3>
<h4>房产:{{ house }}</h4>
<button @click="changeToy">修改Child1的玩具</button>
<button @click="changeComputer">修改Child2的电脑</button>
<button @click="getAllChild($refs)">让所有孩子的书变多</button>
<Child1 ref="c1"/>
<Child2 ref="c2"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import { ref,reactive } from "vue";
let c1 = ref()
let c2 = ref()
// 数据
let house = ref(4)
// 方法
function changeToy(){
c1.value.toy = '芭比'
}
function changeComputer(){
c2.value.computer = '华为'
}
function getAllChild(refs:{[key:string]:any}){
console.log(refs)
for (let key in refs){
refs[key].book += 3
}
}
// 向外部提供数据
defineExpose({house})
</script>
子组件1
<template>
<div class="child1">
<h3>子组件1</h3>
<h4>玩具:{{ toy }}</h4>
<h4>书籍:{{ book }} 本</h4>
<button @click="minusHouse($parent)">继承父亲的一套房产</button>
</div>
</template>
<script setup lang="ts" name="Child1">
import { ref } from "vue";
// 数据
let toy = ref('高达')
let book = ref(3)
// 方法
function minusHouse(parent:any){
parent.house -= 1
}
// 把数据交给外部
defineExpose({toy,book})
</script>
子组件2
<template>
<div class="child2">
<h3>子组件2</h3>
<h4>电脑:{{ computer }}</h4>
<h4>书籍:{{ book }} 本</h4>
</div>
</template>
<script setup lang="ts" name="Child2">
import { ref } from "vue";
// 数据
let computer = ref('联想')
let book = ref(6)
// 把数据交给外部
defineExpose({computer,book})
</script>
7、祖孙互传_provide和inject
先来讲解provide
和inject
用法,官网上也叫依赖注入
provide:提供一个值,可以被后代组件注入。
function provide<T>(key: InjectionKey<T> | string, value: T): void
provide()
接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。
与注册生命周期钩子的 API 类似,provide()
必须在组件的 setup()
阶段同步调用。
<script setup>
import { ref, provide } from 'vue'
import { countSymbol } from './injectionSymbols'
// 提供静态值
provide('path', '/project/')
// 提供响应式的值
const count = ref(0)
provide('count', count)
</script>
inject:注入一个由祖先组件或整个应用 (通过 app.provide()
) 提供的值。
// 没有默认值
function inject<T>(key: InjectionKey<T> | string): T | undefined
// 带有默认值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject()
将返回 undefined
,除非提供了一个默认值。
第二个参数是可选的,即在没有匹配到 key 时使用的默认值。
与注册生命周期钩子的 API 类似,inject()
必须在组件的 setup()
阶段同步调用
<script setup>
import { inject } from 'vue'
import { countSymbol } from './injectionSymbols'
// 注入不含默认值的静态值
const path = inject('path')
// 注入响应式的值
const count = inject('count')
// 注入一个值,若为空则使用提供的默认值
const bar = inject('path', '/default-path')
// 注入一个值,若为空则使用提供的函数类型的默认值
const fn = inject('function', () => {})
</script>
利用provide
和inject
来实现实现祖孙组件直接通信,无论间隔多少代
不同于上面的[祖孙互传_KaTeX parse error: Expected 'EOF', got '#' at position 8: attrs](#̲5、祖孙互传_attrs),这里不需要祖孙中间的组件进行任何操作
具体使用:
- 在祖先组件中通过
provide
配置向后代组件提供数据 - 在后代组件中通过
inject
配置来声明接收数据
父组件
<template>
<div class="father">
<h3>父组件</h3>
<h4>票子:{{ money }}万元</h4>
<h4>车子:{{car.brand}},价值{{car.price}}万元</h4>
<Child/>
</div>
</template>
<script setup lang="ts">
import Child from './Child.vue'
import {ref,reactive,provide} from 'vue'
let money = ref(100)
let car = reactive({
brand:'劳斯莱斯',
price:100
})
function updateMoney(value:number){
money.value -= value
}
// 向后代提供数据
provide('moneyContext',{money,updateMoney})
provide('car',car)
</script>
孙组件
<template>
<div class="grand-child">
<h3>我是孙组件</h3>
<h4>票子:{{ money }}</h4>
<h4>车子:{{car.brand}},价值{{car.price}}万元</h4>
<button @click="updateMoney(10)">花老祖宗的钱</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from "vue";
let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(param:number)=>{}})
let car = inject('car',{brand:'未知',price:0})
</script>
六、插槽 slots
插槽 slot 是写在子组件的代码中,供父组件使用的占位符
通俗的理解就是“占坑”,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑
1、默认插槽
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。
父组件
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Category title="热门游戏列表">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Category>
<Category title="今日美食城市">
<img :src="imgUrl" alt="">
</Category>
<Category title="今日影视推荐">
<video :src="videoUrl" controls></video>
</Category>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Category from './Category.vue'
import { ref,reactive } from "vue";
let games = reactive([
{id:'zzzq01',name:'使命呼唤'},
{id:'zzzq02',name:'占地2042'},
{id:'zzzq03',name:'荒野大嫖客'},
{id:'zzzq04',name:'蒸汽朋克2077'}
])
let imgUrl = ref('https://c-ssl.duitang.com/uploads/blog/202009/02/20200902121137_672fc.jpg')
let videoUrl = ref('https://vt1.doubanio.com/202405171600/09419b6b482125d696d4a78dc06ed08f/view/movie/M/703160002.mp4')
</script>
插槽(子组件)
<template>
<div class="category">
<h2>{{title}}</h2>
<slot>默认内容</slot>
</div>
</template>
<script setup lang="ts" name="Category">
defineProps(['title'])
</script>
父组件中,组件标签内的东西,会展示到子组件的<slot></slot>
内,如果父组件不传内容,子组件就会展示默认内容
2、具名插槽
有时在一个组件中包含多个插槽出口,这时候就要用到具名插槽。
<slot>
元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID
<template>
<div class="category">
<slot name="s1">默认内容</slot>
<slot name="s2">默认内容</slot>
</div>
</template>
<script setup lang="ts" name="Category">
</script>
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default”。
在父组件中使用该组件时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:
要为具名插槽传入内容,我们需要使用一个含 v-slot
指令的 <template>
元素,并将目标插槽的名字传给该指令:
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Category v-solt:s2>
<template v-slot:s1>
<h2>热门游戏</h2>
</template>
<template v-slot:s2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
</Category>
<Category title="今日美食城市">
<template v-slot:s1>
<h2>美食</h2>
</template>
<template v-slot:s2>
<img :src="imgUrl" alt="">
</template>
</Category>
<Category>
<template #s1>
<h2>今日影视推荐</h2>
</template>
<template #s2>
<video :src="videoUrl" controls></video>
</template>
</Category>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Category from './Category.vue'
import { ref,reactive } from "vue";
let games = reactive([
{id:'zzzq01',name:'使命呼唤'},
{id:'zzzq02',name:'占地'},
{id:'zzzq03',name:'荒野大嫖客'},
{id:'zzzq04',name:'蒸汽朋克'}
])
let imgUrl = ref('https://c-ssl.duitang.com/uploads/blog/202009/02/20200902121137_672fc.jpg')
let videoUrl = ref('https://vt1.doubanio.com/202405171600/09419b6b482125d696d4a78dc06ed08f/view/movie/M/703160002.mp4')
</script>
v-slot
有对应的简写 #
,因此 <template v-slot:s1>
可以简写为 <template #s1>
。
3、作用域插槽
普通插槽的内容无法访问到子组件的状态,然而在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。
要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽,这个时候就需要作用域插槽
子组件
<template>
<div class="game">
<h2>游戏列表</h2>
<slot :youxi="games" x="其他数据" y="hello" ></slot>
</div>
</template>
<script setup lang="ts" name="Game">
import {reactive} from 'vue'
let games = reactive([
{id:'zzzq01',name:'使命呼唤'},
{id:'zzzq02',name:'占地'},
{id:'zzzq03',name:'荒野大嫖客'},
{id:'zzzq04',name:'蒸汽朋克'}
])
</script>
需要渲染的数据全部在子组件中,可以通过在<slot></slot>
中声明标签,将要父组件需要的数据抛出去,比如:youxi="games"
将游戏game
数据传出去
父组件
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Game>
<template v-slot="params">
<ul>
<li v-for="y in params.youxi" :key="y.id">
{{ y.name }}
</li>
</ul>
<h4> {{ params }}</h4>
</template>
</Game>
<Game>
<template v-slot="params">
<ol>
<li v-for="item in params.youxi" :key="item.id">
{{ item.name }}
</li>
</ol>
</template>
</Game>
<Game>
//从全部数据中解构出youxi
<template v-slot:default="{youxi}"> //#default="{youxi}"
<h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3>
</template>
</Game>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Game from './Game.vue'
</script>
通过子组件标签上的 v-slot
指令,直接接收到了一个插槽 props 对象,注意,接收的数据是子组件传递的全部数据,也就是子组件传递的 :youxi="games" x="其他数据" y="hello"
,可以解构想要的数据
当遇到具名插槽时,比如
<slot name="abc" :youxi="games" x="其他数据" y="hello" ></slot>
对应父组件接收时,也要声明子组件对应的名字
<template v-slot:abc="params"> //#abc="{params}"
</template>
七、Vue3中的重要API
vue3的API参考:https://cn.vuejs.org/api/
-----------------知识内容大多来源于互联网-----------------
标签:vue,const,name,基础知识,语法,Vue3,组件,import,ref From: https://blog.csdn.net/weixin_51473175/article/details/139448433