3. Vue3核心语法
3.1. 【OptionsAPI 与 CompositionAPI】
Vue2
的API
设计是Options
(配置)风格的。Vue3
的API
设计是Composition
(组合)风格的。
Options API 的弊端
Options
类型的 API
,数据、方法、计算属性等,是分散在:data
、methods
、computed
中的,若想新增或者修改一个需求,就需要分别修改:data
、methods
、computed
,不便于维护和复用。
Composition API 的优势
可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
说明:以上四张动图原创作者:大帅老猿
3.2. 【拉开序幕的 setup】
setup 概述
setup
是Vue3
中一个新的配置项,值是一个函数,它是 Composition API
“表演的舞台”,组件中所用到的:数据、方法、计算属性、监视......等等,均配置在setup
中。
特点如下:
setup
函数返回的对象中的内容,可直接在模板中使用。setup
中访问this
是undefined
。setup
函数会在beforeCreate
之前调用,它是“领先”所有钩子执行的。
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name:'Person',
setup(){
// 数据,原来写在data中(注意:此时的name、age、tel数据都不是响应式数据)
let name = '张三'
let age = 18
let tel = '13888888888'
// 方法,原来写在methods中
function changeName(){
name = 'zhang-san' //注意:此时这么修改name页面是不变化的
console.log(name)
}
function changeAge(){
age += 1 //注意:此时这么修改age页面是不变化的
console.log(age)
}
function showTel(){
alert(tel)
}
// 返回一个对象,对象中的内容,模板中可以直接使用
return {name,age,tel,changeName,changeAge,showTel}
}
}
</script>
setup 的返回值
- 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用**(重点关注)。**
- 若返回一个函数:则可以自定义渲染内容,代码如下:
setup(){
return ()=> '你好啊!'
}
setup 与 Options API 的关系
Vue2
的配置(data
、methos
......)中可以访问到setup
中的属性、方法。- 但在
setup
中不能访问到Vue2
的配置(data
、methos
......)。 - 如果与
Vue2
冲突,则setup
优先。
setup 语法糖
setup
函数有一个语法糖,这个语法糖,可以让我们把setup
独立出去,代码如下:
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changName">修改名字</button>
<button @click="changAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name:'Person',
}
</script>
<!-- 下面的写法是setup语法糖 -->
<script setup lang="ts">
console.log(this) //undefined
// 数据(注意:此时的name、age、tel都不是响应式数据)
let name = '张三'
let age = 18
let tel = '13888888888'
// 方法
function changName(){
name = '李四'//注意:此时这么修改name页面是不变化的
}
function changAge(){
console.log(age)
age += 1 //注意:此时这么修改age页面是不变化的
}
function showTel(){
alert(tel)
}
</script>
扩展:上述代码,还需要编写一个不写setup
的script
标签,去指定组件名字,比较麻烦,我们可以借助vite
中的插件简化
- 第一步:
npm i vite-plugin-vue-setup-extend -D
- 第二步:
vite.config.ts
import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [ VueSetupExtend() ]
})
- 第三步:
<script setup lang="ts" name="Person">
3.3. 【ref 创建:基本类型的响应式数据】
- **作用:**定义响应式变量。
- 语法:
let xxx = ref(初始值)
。 - **返回值:**一个
RefImpl
的实例对象,简称ref对象
或ref
,ref
对象的value
属性是响应式的。 - 注意点:
JS
中操作数据需要:xxx.value
,但模板中不需要.value
,直接使用即可。- 对于
let name = ref('张三')
来说,name
不是响应式的,name.value
是响应式的。
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script setup lang="ts" name="Person">
import {ref} from 'vue'
// name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。
let name = ref('张三')
let age = ref(18)
// tel就是一个普通的字符串,不是响应式的
let tel = '13888888888'
function changeName(){
// JS中操作ref对象时候需要.value
name.value = '李四'
console.log(name.value)
// 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。
// name = ref('zhang-san')
}
function changeAge(){
// JS中操作ref对象时候需要.value
age.value += 1
console.log(age.value)
}
function showTel(){
alert(tel)
}
</script>
3.4. 【reactive 创建:对象类型的响应式数据】
- 作用:定义一个响应式对象(基本类型不要用它,要用
ref
,否则报错) - 语法:
let 响应式对象= reactive(源对象)
。 - **返回值:**一个
Proxy
的实例对象,简称:响应式对象。 - 注意点:
reactive
定义的响应式数据是“深层次”的。
<template>
<div class="person">
<h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
<h2>游戏列表:</h2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<h2>测试:{{obj.a.b.c.d}}</h2>
<button @click="changeCarPrice">修改汽车价格</button>
<button @click="changeFirstGame">修改第一游戏</button>
<button @click="test">测试</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { reactive } from 'vue'
// 数据
let car = reactive({ brand: '奔驰', price: 100 })
let games = reactive([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = reactive({
a:{
b:{
c:{
d:666
}
}
}
})
function changeCarPrice() {
car.price += 10
}
function changeFirstGame() {
games[0].name = '流星蝴蝶剑'
}
function test(){
obj.a.b.c.d = 999
}
</script>
3.5. 【ref 创建:对象类型的响应式数据】
- 其实
ref
接收的数据可以是:基本类型、对象类型。 - 若
ref
接收的是对象类型,内部其实也是调用了reactive
函数。
<template>
<div class="person">
<h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
<h2>游戏列表:</h2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<h2>测试:{{obj.a.b.c.d}}</h2>
<button @click="changeCarPrice">修改汽车价格</button>
<button @click="changeFirstGame">修改第一游戏</button>
<button @click="test">测试</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref } from 'vue'
// 数据
let car = ref({ brand: '奔驰', price: 100 })
let games = ref([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = ref({
a:{
b:{
c:{
d:666
}
}
}
})
console.log(car)
function changeCarPrice() {
car.value.price += 10
}
function changeFirstGame() {
games.value[0].name = '流星蝴蝶剑'
}
function test(){
obj.value.a.b.c.d = 999
}
</script>
3.6. 【ref 对比 reactive】
宏观角度看:
ref
用来定义:基本类型数据、对象类型数据;
reactive
用来定义:对象类型数据。
- 区别:
ref
创建的变量必须使用.value
(可以使用volar
插件自动添加.value
)。
reactive
重新分配一个新对象,会失去响应式(可以使用Object.assign
去整体替换)。
- 使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref
。- 若需要一个响应式对象,层级不深,
ref
、reactive
都可以。- 若需要一个响应式对象,且层级较深,推荐使用
reactive
。
3.7. 【toRefs 与 toRef】
- 作用:将一个响应式对象中的每一个属性,转换为
ref
对象。 - 备注:
toRefs
与toRef
功能一致,但toRefs
可以批量转换。 - 语法如下:
<template>
<div class="person">
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>性别:{{person.gender}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,reactive,toRefs,toRef} from 'vue'
// 数据
let person = reactive({name:'张三', age:18, gender:'男'})
// 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
let {name,gender} = toRefs(person)
// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
let age = toRef(person,'age')
// 方法
function changeName(){
name.value += '~'
}
function changeAge(){
age.value += 1
}
function changeGender(){
gender.value = '女'
}
</script>
3.8. 【computed】
作用:根据已有数据计算出新数据(和Vue2
中的computed
作用一致)。

<template>
<div class="person">
姓:<input type="text" v-model="firstName"> <br>
名:<input type="text" v-model="lastName"> <br>
全名:<span>{{fullName}}</span> <br>
<button @click="changeFullName">全名改为:li-si</button>
</div>
</template>
<script setup lang="ts" name="App">
import {ref,computed} from 'vue'
let firstName = ref('zhang')
let lastName = ref('san')
// 计算属性——只读取,不修改
/* let fullName = computed(()=>{
return firstName.value + '-' + lastName.value
}) */
// 计算属性——既读取又修改
let fullName = computed({
// 读取
get(){
return firstName.value + '-' + lastName.value
},
// 修改
set(val){
console.log('有人修改了fullName',val)
firstName.value = val.split('-')[0]
lastName.value = val.split('-')[1]
}
})
function changeFullName(){
fullName.value = 'li-si'
}
</script>
3.9.【watch】
- 作用:监视数据的变化(和
Vue2
中的watch
作用一致) - 特点:
Vue3
中的watch
只能监视以下四种数据:
ref
定义的数据。reactive
定义的数据。- 函数返回一个值(
getter
函数)。- 一个包含上述内容的数组。
我们在Vue3
中使用watch
的时候,通常会遇到以下几种情况:
* 情况一
监视ref
定义的【基本类型】数据:直接写数据名即可,监视的是其value
值的改变。
<template>
<div class="person">
<h1>情况一:监视【ref】定义的【基本类型】数据</h1>
<h2>当前求和为:{{sum}}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,watch} from 'vue'
// 数据
let sum = ref(0)
// 方法
function changeSum(){
sum.value += 1
}
// 监视,情况一:监视【ref】定义的【基本类型】数据
const stopWatch = watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
if(newValue >= 10){
stopWatch()
}
})
</script>
* 情况二
监视ref
定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
注意:
若修改的是
ref
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。若修改整个
ref
定义的对象,newValue
是新值,oldValue
是旧值,因为不是同一个对象了。
<template>
<div class="person">
<h1>情况二:监视【ref】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
</div>
</template>
<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})
</script>
* 情况三
监视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>
* 情况四
监视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:'爱玛'}
}
// 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
/* watch(()=> person.name,(newValue,oldValue)=>{
console.log('person.name变化了',newValue,oldValue)
}) */
// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
watch(()=>person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
* 情况五
监视上述的多个数据
<template>
<div class="person">
<h1>情况五:监视上述的多个数据</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:'爱玛'}
}
// 监视,情况五:监视上述的多个数据
watch([()=>person.name,person.car],(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
3.10. 【watchEffect】
官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
watch
对比watchEffect
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
:要明确指出监视的数据watchEffect
:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
示例代码:
vue<template> <div class="person"> <h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1> <h2 id="demo">水温:{{temp}}</h2> <h2>水位:{{height}}</h2> <button @click="changePrice">水温+1</button> <button @click="changeSum">水位+10</button> </div> </template> <script lang="ts" setup name="Person"> import {ref,watch,watchEffect} from 'vue' // 数据 let temp = ref(0) let height = ref(0) // 方法 function changePrice(){ temp.value += 10 } function changeSum(){ height.value += 1 } // 用watch实现,需要明确的指出要监视:temp、height watch([temp,height],(value)=>{ // 从value中获取最新的temp值、height值 const [newTemp,newHeight] = value // 室温达到50℃,或水位达到20cm,立刻联系服务器 if(newTemp >= 50 || newHeight >= 20){ console.log('联系服务器') } }) // 用watchEffect实现,不用 const stopWtach = watchEffect(()=>{ // 室温达到50℃,或水位达到20cm,立刻联系服务器 if(temp.value >= 50 || height.value >= 20){ console.log(document.getElementById('demo')?.innerText) console.log('联系服务器') } // 水温达到100,或水位达到50,取消监视 if(temp.value === 100 || height.value === 50){ console.log('清理了') stopWtach() } }) </script>
3.11. 【标签的 ref 属性】
作用:用于注册模板引用。
用在普通
DOM
标签上,获取的是DOM
节点。用在组件标签上,获取的是组件实例对象。
用在普通DOM
标签上:
<template>
<div class="person">
<h1 ref="title1">尚硅谷</h1>
<h2 ref="title2">前端</h2>
<h3 ref="title3">Vue</h3>
<input type="text" ref="inpt"> <br><br>
<button @click="showLog">点我打印内容</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref} from 'vue'
let title1 = ref()
let title2 = ref()
let title3 = ref()
function showLog(){
// 通过id获取元素
const t1 = document.getElementById('title1')
// 打印内容
console.log((t1 as HTMLElement).innerText)
console.log((<HTMLElement>t1).innerText)
console.log(t1?.innerText)
/************************************/
// 通过ref获取元素
console.log(title1.value)
console.log(title2.value)
console.log(title3.value)
}
</script>
用在组件标签上:
<!-- 父组件App.vue -->
<template>
<Person ref="ren"/>
<button @click="test">测试</button>
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {ref} from 'vue'
let ren = ref()
function test(){
console.log(ren.value.name)
console.log(ren.value.age)
}
</script>
<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
import {ref,defineExpose} from 'vue'
// 数据
let name = ref('张三')
let age = ref(18)
/****************************/
/****************************/
// 使用defineExpose将组件中的数据交给外部
defineExpose({name,age})
</script>
3.12. 【props】
参考官网关于props的使用 和在Typescript中为组件的 props 标注类型
在父组件中import的时候注意需要写上扩展名(.vue)
,具体原因详见 vite配置
简单用法
<!-- 1 定义组件模版 -->
<template>
<div>
<div>propA: {{ propA }}</div>
<div>propB: {{ propB }}</div>
<div>propC: {{ propC }}</div>
<div>propD: {{ propD }}</div>
</div>
</template>
<!-- 2 定义组件逻辑 -->
<script setup name="ChildOne">
import { ref } from 'vue'
defineProps({
propA:String,
propB:Number,
propC:Boolean,
propD:Date,
})
</script>
<!-- 3 定义样式 -->
<style scoped>
</style>
<script setup lang="ts">
import ChildOne from './components/ChildOne.vue'
</script>
<template>
<ChildOne :prop-a="'abcdef'" :prop-b="123" :prop-c="true" :prop-d="new Date()"></ChildOne>
</template>
<style scoped>
</style>
Prop 名字格式
组件
<script setup lang="ts" name="MyComponent">
defineProps({
greetingMessage: string
})
</script>
对于组件名我们推荐使用 PascalCase
PascalCase 形式 :
<MyComponent></MyComponent>
kebab-case形式:
<my-component></my-component>
在js中属性名可以使用 camelCase 形式,
camelCase形式 :
<MyComponent greetingMessage="hello" />
在html标签中【推荐】使用 kebab-case 形式
<MyComponent greeting-message="hello" />
动态属性VS静态属性
参见官网
Prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。
要声明对 props 的校验,你可以向 defineProps()
宏提供一个带有 props 校验选项的对象,例如:
<!-- 1 定义组件模版 -->
<template>
<div>
<div>propA: {{ propA }}</div>
<div>propB: {{ propB }}</div>
<div>propC: {{ propC }}</div>
<div>propD: {{ propD }}</div>
<div>propE: {{ propE }}</div>
<div>propF: {{ propF }}</div>
<div>propG: {{ propG }}</div>
</div>
</template>
<!-- 2 定义组件逻辑 -->
<script setup name="ChildOne">
import { ref } from 'vue'
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
// 在 3.4+ 中完整的 props 作为第二个参数传入
propF: {
validator(value, props) {
// The value must match one of these strings
let valid = ['success', 'warning', 'danger'].includes(value)
if(!valid){
console.warn("propF属性 必须包含在 "+['success', 'warning', 'danger']+"中");
}
return valid
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
</script>
<!-- 3 定义样式 -->
<style scoped></style>
在父组件中引用ChildTwo组件
<ChildTwo ></ChildTwo>
[Vue warn]: Missing required prop: "propC"
at <ChildTwo>
at <App>
此时"propC" 是有required说明,未必填项
通过观察页面数据可以看到 propD
、propE
,propG
分别都有默认值
当propF
属性填写的是 'success', 'warning', 'danger'
以外的值时,控制台提示异常
<ChildTwo :propF="'success1'"></ChildTwo>
注意
这个时候虽然控制台中提示异常,但是浏览器端依然可以正常渲染出 propF:success1
的值
在Typescript中使用属性
在上述代码中数据校验虽然可以在控制台中进行提示,但是并不是强制性的,编译后依然可以通过,此时可以使用Typescript的类型检查功能
此部分用到vue的 defineProps、withDefaults函数 和Typescript的接口(定义属性、可选属性)
export interface PersonInter{
id:string,
name:string,
age?:number
}
<!-- 1 定义组件模版 -->
<template>
<div class="container">
<div>字符串类型: {{ strProp }}</div>
<div>数字类型: {{ numberProp }}</div>
<div>boolean类型: {{ boolProp }}</div>
<div>自定义类型 : {{ typeProp }}</div>
<div>自定义类型[]数组 :
<ol>
<li v-for="item in arrayList">
{{item}}
</li>
</ol>
</div>
<div>非必须属性: {{ unMustProp }}</div>
</div>
</template>
<!-- 2 定义组件逻辑 -->
<script setup lang="ts" name="Person">
import { ref } from 'vue'
import type { PersonInter } from '@/types';
//仅仅定义属性
// let props = defineProps(["strProp","numberProp","boolProp","typeProp","arrayList"])
//仅仅定义属性 + 限定类型
// let props = defineProps<{
// strProp:string,
// numberProp:number,
// boolProp:boolean,
// typeProp:PersonInter,
// // arrayList:PersonInter[]
// arrayList:Array<PersonInter>
// }>()
//定义属性 + 限定类型 + 限定是否必要(?)
// let props = defineProps<{
// strProp:string,
// numberProp:number,
// boolProp:boolean,
// typeProp:PersonInter,
// arrayList:PersonInter[]
// unMustProp?:string
// }>()
//定义属性 + 限定类型 + 限定是否必要(?)
let props = withDefaults(defineProps<{
strProp:string,
numberProp:number,
boolProp:boolean,
typeProp:PersonInter,
arrayList:PersonInter[]
unMustProp?:string
}>(),{
unMustProp:()=>"未设置该属性"
})
</script>
<!-- 3 定义样式 -->
<style scoped>
.container >div{
margin: 10px;
}
</style>
<template>
<h1>基于Typescript的组件属性传递</h1>
<TsChild :number-prop="3.14" :str-prop="'字符串参数'" :bool-prop="true" :type-prop="person" :array-list="personList" :unMustProp="'主动传递该属性'"></TsChild>
</template>
<script setup lang="ts" >
import { reactive } from 'vue';
import TsChild from './components/TsChild.vue'
import type { PersonInter } from '@/types'
//非响应式
// let person: PersonInter = {
// id: '',
// name: '',
// age: 0
// }
//响应式数据
let person = reactive<PersonInter>({
id: '',
name: '',
age: 0
})
//响应式数据 数组
let personList = reactive<PersonInter[]>([
{id: '1', name: '张飞', age: 18},
{id: '2', name: '刘备', age: 28},
{id: '3', name: '李逵'},
])
</script>
<style scoped></style>
使用一个对象绑定多个 prop
如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind
,即只使用 v-bind
而非 :prop-name
。例如,这里有一个 post
对象:
const post = {
id: 1,
title: 'My Journey with Vue'
}
以及下面的模板:
template
<BlogPost v-bind="post" />
而这实际上等价于:
template
<BlogPost :id="post.id" :title="post.title" />
单向数据流
官网原文:
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:
更改对象 / 数组类型的 props
当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。
3.13. 【表单绑定】
参考官方文档
3.14【生命周期】
概念:
Vue
组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue
会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子规律:
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
Vue2
的生命周期创建阶段:
beforeCreate
、created
挂载阶段:
beforeMount
、mounted
更新阶段:
beforeUpdate
、updated
销毁阶段:
beforeDestroy
、destroyed
Vue3
的生命周期创建阶段:
setup
挂载阶段:
onBeforeMount
、onMounted
更新阶段:
onBeforeUpdate
、onUpdated
卸载阶段:
onBeforeUnmount
、onUnmounted
常用的钩子:
onMounted
(挂载完毕)、onUpdated
(更新完毕)、onBeforeUnmount
(卸载之前)示例代码:
vue<template> <div class="person"> <h2>当前求和为:{{ sum }}</h2> <button @click="changeSum">点我sum+1</button> </div> </template> <!-- vue3写法 --> <script lang="ts" setup name="Person"> import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' // 数据 let sum = ref(0) // 方法 function changeSum() { sum.value += 1 } console.log('setup') // 生命周期钩子 onBeforeMount(()=>{ console.log('挂载之前') }) onMounted(()=>{ console.log('挂载完毕') }) onBeforeUpdate(()=>{ console.log('更新之前') }) onUpdated(()=>{ console.log('更新完毕') }) onBeforeUnmount(()=>{ console.log('卸载之前') }) onUnmounted(()=>{ console.log('卸载完毕') }) </script>
3.15.引入ElementPlus
第三方组件
在项目中我们会使用一些常规的 一些通用组件,这时候我们需要自己定义组件,或者使用第三方开源组件如ElementPlus、Ant Design等
此处介绍一下ElementPlus的简单使用
安装
ElementPlus【官网】是支持vue3语法的组件库,可以通过在vue3项目的基础上使用npm的方式安装
cnpm install element-plus --save
在Vue3项目中引用
下面是一个利用ElementPlus中的Container布局组件
、菜单组件Menu
、表格组件table
、 上传图片组件Upload
实现的上传图片,
除了使用ElementPlus之外还使用了axios库,请提前安装
npm install axios
安装完成后按照如下结构组织前端代码(Vue项目) ps:此处仅展示主要代码
Vue
│ env.d.ts
│ index.html
│ package.json
│ tsconfig.json
│ vite.config.ts
└─src
│ App.vue
│ main.ts
├─components
│ NavMenu.vue
|
└─views
└─upload
index.vue
个文件内容如下
// ui/vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 9999,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
// src/main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
<script setup lang="ts">
//自定义组件,包含了导航菜单
import NavMenu from "./components/NavMenu.vue";
//自定义组件包含了上传图片的功能
import Upload from "@/views/upload/index.vue";
</script>
<template>
<el-container class="root_container">
<el-header class="header"> For mhy 文件上传 </el-header>
<el-container>
<el-aside width="200px" class="aside"><NavMenu></NavMenu></el-aside>
<el-main class="main" >
<Upload></Upload>
</el-main>
</el-container>
</el-container>
</template>
<style scoped>
.root_container{
height: 100vh;
}
.header{
background-color: #999;
line-height: 60px;
}
.aside{
background-color: #333;
}
</style>
<template>
<div>上传文件列表:</div>
<el-table :data="tableData" style="width: 100%">
<el-table-column type="index" label="序号" width="100" />
<el-table-column prop="name" label="文件名称" />
<el-table-column prop="time" label="上传时间(time)" width="180"/>
<el-table-column prop="size" label="大小(byte)" width="180"/>
</el-table>
<div>列表展示图片</div>
<el-upload v-model:file-list="fileList" action="/api/upload" list-type="picture-card" >
<el-icon><Plus /></el-icon>
</el-upload>
</template>
<script setup lang="ts" name="Upload">
import {ref,reactive,onMounted,computed} from 'vue'
import axios from 'axios'
import { Plus } from '@element-plus/icons-vue'
import type { UploadUserFile } from 'element-plus'
//数据
let tableData = reactive([])
//方法
function getData(){
axios.get('/api/list').then(res=>{
tableData.push(...res.data)
})
}
//计算属性
const fileList = computed(()=>{
const arr:Array<UploadUserFile> = []
tableData.forEach(item=>{
arr.push( {
name: 'food.jpeg',
url: '/api/get/'+item.name,
})
})
return arr
})
//组件挂在时加载数据
onMounted(()=>{
getData()
})
</script>
<style scoped>
</style>
<template>
<el-menu
active-text-color="#ffd04b"
background-color="#545c64"
class="el-menu-vertical-demo"
default-active="2"
text-color="#fff"
router
>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>系统管理</span>
</el-menu-item>
<el-menu-item index="/upload" >
<el-icon><document /></el-icon>
<span>上传文件管理</span>
</el-menu-item>
</el-menu>
</template>
<script setup lang="ts">
//使用ElementPlus系统图标
import { Document } from '@element-plus/icons-vue'
</script>
<style scoped>
</style>
后端接口
上述代码如果想要完成上传实验还需要配合后端接口开发,此处使用springboot+commons-fileupload组件完成上传
创建一个spirngboot的quickstart项目
添加响应的依赖(具体详见下述pom.xml)
编写Controller(用于实现具体的上传、列目录文件、获取单个文件即下载等功能)
编写App启动类
具体代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<name>server</name>
<packaging>jar</packaging>
<groupId>org.springframework.boot</groupId>
<artifactId>server</artifactId>
<version>1.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
</dependencies>
</project>
package org.mhy.upload.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
@RestController
public class UploadController {
//定义上传文件目录
public static final String UPLOAD_DIR = "D:\\upload";
/**
* 用于测试 API类是否可以使用
* @return
*/
@GetMapping("/upload")
public String upload() {
return "文件上传测试,当前时间: "+new Date().getTime();
}
/**
* 上传文件
* @param file
* @return
* @throws IOException
*/
@PostMapping("/upload")
Map upload(MultipartFile file) throws IOException {
//1 将临时空间的文件,转储到 D:\\upload
String originalFileName = file.getOriginalFilename();
//扩展名
String ext = originalFileName.substring(originalFileName.lastIndexOf("."));
//转储的新文件名
String newFileName = UUID.randomUUID().toString() + ext;
//转储到 D:\\upload 下的 uuid.png/uuid.jpg
file.transferTo(new File(UPLOAD_DIR, newFileName));
//2 进行业务处理,比如将上传路径存储到数据库中, 或者进行opencv等图片识别
Map result = new HashMap();
result.put("status", 200);
result.put("file",newFileName);
return result;
}
/**
* 列文件目录内容,用于列表展示
* @return
* @throws IOException
*/
@GetMapping("/list")
List list() throws IOException {
File[] fls = new File(UPLOAD_DIR).listFiles();
List list = new ArrayList();
for (File file : fls) {
Map fl = new HashMap();
fl.put("name",file.getName());
fl.put("size",file.length());
fl.put("time",file.lastModified());
list.add(fl);
}
return list;
}
/**
* 下载文件,展示图片
* @param pic 闻不见名
* @return
* @throws IOException
*/
@GetMapping("/get/{pic}")
void get(@PathVariable String pic, HttpServletResponse response) throws IOException {
File picFile = new File(UPLOAD_DIR, pic);
InputStream is = new FileInputStream(picFile);
OutputStream os = response.getOutputStream();
int len = -1;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
os.close();
is.close();
}
}
package org.mhy.upload;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App
{
public static void main( String[] args ){
SpringApplication.run(App.class,args);
System.out.println("系统启动成功,请访问 : http://localhost:8080");
}
}
最终的效果如下
3.16. 【自定义hook】
什么是
hook
?—— 本质是一个函数,把setup
函数中使用的Composition API
进行了封装,类似于vue2.x
中的mixin
。自定义
hook
的优势:复用代码, 让setup
中的逻辑更清楚易懂。参考组合函数
示例代码:
useSum.ts
中内容如下:jsimport {ref,onMounted} from 'vue' export default function(){ let sum = ref(0) const increment = ()=>{ sum.value += 1 } const decrement = ()=>{ sum.value -= 1 } onMounted(()=>{ increment() }) //向外部暴露数据 return {sum,increment,decrement} }
useDog.ts
中内容如下:jsimport {reactive,onMounted} from 'vue' import axios,{AxiosError} from 'axios' export default function(){ let dogList = reactive<string[]>([]) // 方法 async function getDog(){ try { // 发请求 let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/../../imgs/vuejs3/random') // 维护数据 dogList.push(data.message) } catch (error) { // 处理错误 const err = <AxiosError>error console.log(err.message) } } // 挂载钩子 onMounted(()=>{ getDog() }) //向外部暴露数据 return {dogList,getDog} }
组件中具体使用:
vue<template> <h2>当前求和为:{{sum}}</h2> <button @click="increment">点我+1</button> <button @click="decrement">点我-1</button> <hr> <img v-for="(u,index) in dogList.urlList" :key="index" :src="(u as string)"> <span v-show="dogList.isLoading">加载中......</span><br> <button @click="getDog">再来一只狗</button> </template> <script lang="ts"> import {defineComponent} from 'vue' export default defineComponent({ name:'App', }) </script> <script setup lang="ts"> import useSum from './hooks/useSum' import useDog from './hooks/useDog' let {sum,increment,decrement} = useSum() let {dogList,getDog} = useDog() </script>
改造上传图片功能
在引入ElementPlus一章中我们使用了axios 以及相关的一系列功能完成的文件上传,功能中我们可以将index.vue的功能改造成hooks组合函数的方式,在index.vue同目录下创建useUploadPic.ts
内容如下
import {ref, reactive, onMounted, computed} from 'vue'
import axios from 'axios'
import type {UploadUserFile} from 'element-plus'
export default function useUpload() {
//数据
let tableData = reactive<Array<any>>([])
const fileList = computed(() => {
const arr: Array<UploadUserFile> = []
tableData.forEach(item => {
arr.push({
name: 'food.jpeg',
url: '/api/get/' + item.name,
})
})
return arr
})
//方法
function getData() {
tableData.splice(0);
axios.get('/api/list').then(res => {
tableData.push(...res.data)
})
}
onMounted(() => {
getData()
})
return {tableData, fileList,getData}
}
修改index.vue,使用组合函数
<template>
<div>上传文件列表:</div>
<el-table :data="tableData" style="width: 100%">
<el-table-column type="index" label="序号" width="100" />
<el-table-column prop="name" label="文件名称" />
<el-table-column prop="time" label="上传时间(time)" width="180"/>
<el-table-column prop="size" label="大小(byte)" width="180"/>
</el-table>
<div>列表展示图片-------:</div>
<el-upload v-model:file-list="fileList" action="/api/upload" list-type="picture-card" :on-success="getData" >
<el-icon><Plus /></el-icon>
</el-upload>
</template>
<script setup lang="ts" name="Upload">
import { Plus } from '@element-plus/icons-vue'
import useUpload from "@/views/upload/useUploadPic";
let {tableData,fileList,getData } = useUpload()
</script>
<style scoped>
</style>
VueUSE
