Vue3 新特性 + TypeScript 小实战
上次将 Composition API
大致梳理了一遍 ,这次主要是想记录一些 vue3
相较 vue2
新增出来的一些特性和一些方法上使用的变动,话不多说,直接开撸。
Teleport
我们日常开发中经常会遇到这样一个场景,比如我们封装一个弹层 msk
组件,但是它包裹在一个 position: relative
定位的父元素中,此时它将被局限在父元素的大小中,我们很难将弹层铺满整个窗口。而 Teleport
提供了一种干净的方法,允许我们控制在 DOM
中哪个父节点下渲染了 HTML
,而不必求助于全局状态或将其拆分为两个组件。举个栗子:
<body>
<div id="root"></div>
<div id="center"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<div class="mask-wrapper">
<msk></msk>
</div>
`
})
app.component('msk', {
template: `
<div class="msk">111</div>
`
})
app.mount('#root')
</script>
浏览器渲染结果如下:
未使用 teleport
这肯定不是我们想要实现的效果,我们希望蒙层是充满整个窗口的,此时我们可以直接将蒙层组件通过 teleport
渲染到 body
下面或者我们指定的 dom
节点下面,teleport
上面有一个 to
的属性,它接受一个 css query selector
作为参数。如下栗子:
<script>
const app = Vue.createApp({
template: `
<div class="mask-wrapper">
// 使用 to 属性将其挂载到 id = center 的 dom 节点下
// 我们也可以直接使用 to = body 将其直接挂载到 body 中
<teleport to="#center">
<msk></msk>
</teleport>
</div>
`
})
</script>
emits
我们知道在 vue2
中父子组件传值会用到 props
和 $emit
,但是在 vue3 中新增了 emits
,它的主要作用是汇总该组件有哪些自定义事件,可以是一个数组写法,也可以是一个对象写法,同时在对象写法中还支持自定义函数,可以在运行时验证参数是否正确。
了解它的基础用法之后我们将 teleport
中写入的小栗子重写,让其组件通信完成最基本的显示和隐藏的动态交互功能。当然我们在子组件通过 $emit
触发的事件要统一写入 emits
数组中进行管理。
const app = Vue.createApp({
template: `
<div class="mask-wrapper">
<button @click="openMsk">打开弹层</button>
<teleport to="#center">
<msk :isOpen="isOpen" @closeMsk="closeMsk"></msk>
</teleport>
</div>
`,
setup() {
const { ref } = Vue
const isOpen = ref(false)
const openMsk = () => {
isOpen.value = true
}
const closeMsk = () => {
isOpen.value = false
}
return { openMsk, isOpen, closeMsk }
}
})
app.component('msk', {
props: ['isOpen'],
// 子组件中我们会向父组件触发 `closeMsk` 事件,所以将其统一写入 `emits` 中方便管理维护
emits: ['closeMsk'],
template: `
<div class="msk" v-show="isOpen">
<button @click="closeMsk">关闭弹层</button>
</div>
`,
setup(props, context) {
const closeMsk = () => {
context.emit('closeMsk')
}
return { closeMsk }
}
})
app.mount('#root')
当然我们也可以在 emits
中使用对象写法,并且传入验证的自定义函数:
app.component('msk', {
props: ['isOpen'],
emits: {
// 'closeMsk': null, 无需验证
'closeMsk': (payload) => {
return payload === 111 // 事件触发时验证传入的值是否为 111
// 验证失败,因为我传入的是 222
// 无效的事件参数:事件“closeMsk”的事件验证失败。
}
},
setup(props, context) {
const closeMsk = () => {
context.emit('closeMsk', 222)
}
return { closeMsk }
}
}
小伙伴们可以试一试,当然即使我传入的值和验证时的值不匹配但是并不会影响这个事件的正常执行,只是会在浏览器中给出警告提示。
Suspense
和 teleport
组件一样,这也是 vue3.0
新推出来的一个全新的组件,它的主要作用是和异步组件一起使用,我们可以现在这里回忆一下 vue2.0
中我们是如何使用动态组件和异步组件的。
动态组件
vue 2.0
和 vue3.0
动态组件的使用方式基本差不多,都是根据数据的变化,结合 component
这个标签,来随时动态切换组件的实现。这里简单做个小回顾:
const app = Vue.createApp({
setup() {
const { ref, keepAlive } = Vue
const currentItem = ref('first-item')
const handleClick = () => {
if (currentItem.value === 'first-item') {
currentItem.value = 'second-item'
} else {
currentItem.value = 'first-item'
}
}
return { currentItem, handleClick }
},
template: `
<keep-alive>
<component :is="currentItem"></component>
</keep-alive>
<button @click="handleClick">组件切换</button>
`
})
app.component('first-item', {
template: `
<div>hello world</div>
`
})
app.component('second-item', {
template: `
<input type="text" />
`
})
app.mount('#root')
异步组件
以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,这里可以直接查看 vue2.0 中如何定义异步组件。但是在 vue3.0
中现在,由于函数式组件被定义为纯函数,因此异步组件的定义需要通过将其包装在新的 defineAsyncComponent 助手方法中来显式地定义,其实也很简单,看栗子就知道了:
const { defineAsyncComponent } = Vue
const app = Vue.createApp({
template: `
<div>
<async-show></async-show>
<async-common-item></async-common-item>
</div>
`
})
app.component('asyncShow', defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve({
template: `<div>我将在 1s 之后被渲染出来</div>`
})
}, 1000)
})
}))
app.component('async-common-item', defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve({
template: `<div>我将在 3s 之后被渲染出来</div>`
})
}, 3000)
})
}))
defineAsyncComponent
接受返回 Promise
的工厂函数。从服务器检索组件定义后,应调用 Promise
的 resolve
回调。你也可以调用 reject(reason)
,来表示加载失败。
接下来就可以引入我们的主角 Suspense
组件,他可以用来接收一个或多个异步组件,它本身支持两个具名插槽,一个承载异步插件返回等待状态的插槽,一个承载异步插件返回成功状态的插槽。举个小栗子:
const { defineAsyncComponent } = Vue
const app = Vue.createApp({
template: `
<Suspense >
<template #default> // 异步组件成功内容包裹在 default 插槽中
<async-show></async-show>
</template>
<template #fallback> // 异步组件未加载时显示 fallback里的内容
<h1>loading !!!!</h1>
</template>
</Suspense>
`
})
当然 Suspense
组件也支持多个异步组件的插入,并且它会等待所有异步组件都返回才将其显示出来,不过此时我们需要在其根上包一层,如下栗子:
const app = Vue.createApp({
template: `
<Suspense >
<template #default>
<div>
<async-show></async-show>
<async-common-item></async-common-item>
</div>
</template>
<template #fallback>
<h1>loading !!!!</h1>
</template>
</Suspense>
`
})
Provide / Inject
通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop
沿着组件链逐级传递下去,可能会很麻烦。
对于这种情况,我们可以使用一对 provide
和 inject
。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这些数据。
上面两段话摘自官网,说的很明白,基础的用法其实和 vue2
中差不多,但是我们知道 vue2
中无法实现数据的响应式监听,但是 vue3
中我们使用 composition API
就可以完成对应响应式变化的监听。我们先来回顾一下 vue2
中的基础用法:
父组件像孙子组件传递固定值
const app = Vue.createApp({
provide: {
count: 1
},
template: `
<child />
`
})
app.component('child', {
template: `
<child-child></child-child>
`
})
app.component('child-child', {
inject: ['count'],
template: `
<div>{{count}}</div>
`
})
如果我们想使用 provide
传递数据 data
中的值时,我们就不能用上面这种写法,我们需要将 provide
转换为返回对象的函数。栗子如下:
const app = Vue.createApp({
data() {
return {
count: 1
}
},
provide() {
return {
count: this.count
}
}
})
父组件像孙子组件动态传值
如果此时我们新增一个按钮改变父组件中 count
的值,子组件是无法继续监听到改变后的值的。此时如果我们想对祖先组件中的更改做出响应,我们需要为 provide
的 count
分配一个组合式API computed
const app = Vue.createApp({
data() {
return {
count: 1
}
},
methods: {
handleClick() {
this.count++
}
},
provide() {
return {
count: Vue.computed(() => this.count)
}
},
template: `
<child />
<button @click="handleClick">增加</button>
`
})
app.component('child', {
template: `
<child-child></child-child>
`
})
app.component('child-child', {
inject: ['count'],
template: `
<div>{{count.value}}</div>
`
})
当然此时我们还没有用到 Composition API
来实现,我们在使用 Composiiton API
来重构下上面的代码:
const { ref, provide, inject } = Vue
const app = Vue.createApp({
setup() {
let count = ref(1)
const handleClick = () => {
count.value++
}
provide('count', count)
return { handleClick }
},
template: `
<child />
<button @click="handleClick">增加</button>
`
})
app.component('child', {
template: `
<child-child></child-child>
`
})
app.component('child-child', {
setup() {
let count = inject('count')
return { count }
},
template: `
<div>{{count}}</div>
`
})
是不是感觉非常简单,那么问题来了,刚刚我们使用了 provide / inject
实现了父组件和子孙组件中的数据传递,如果两个毫无关联的组件,那么我们应该如何建立数据通讯呢?除了 vuex
你最先能想到什么,在 2.x
中,Vue
实例可用于触发通过事件触发 API
强制附加的处理程序 ($on
,$off
和 $once
),这用于创建 event hub
,以创建在整个应用程序中使用的全局事件侦听器,因为我前面写过相关文章,关于 vue2
的知识就不在这里过多赘述了,详情可以点击 Vue 常见 API 及问题。
但是在 vue3
中废弃了 $on, $off
,为什么会废弃呢,可以参考文章解读Vue3中废弃组件事件进行解读。官方推荐我们使用第三方库 mitt
进行全局事件的绑定和监听。大体用法其实和原来差不多,这里就不过多赘述了。
Mixin
Mixin
应该算 vue2
中用的比较多的,用法其实和以前大体相差不大,我在 Vue 常见 API 及问题 中记录过 Mixin
的基础用法,官网写的也挺详细的,当然现在关于组件公共逻辑的抽离其实更推荐使用 组合式 API 。这里还是简单总结下 Mixin
的几个特点:
1、混入过程中,组件
data
、methods
、优先级高于mixin data
,methods
优先级。
2、生命周期函数,先执行mixin
里面的,在执行组件里面的。
3、自定义的属性,组件中的属性优先级高于mixin
属性优先级。
什么叫自定义属性呢?我们来看个小栗子:
const app = Vue.createApp({
number: 3,
template: `
<div>{{number}}</div>
`
})
我们直接定义了一个属性 number
,它既不在 data
中,也不在 setup
中,而是直接挂载在 app
上,那么它就是 app
上的自定义属性。此时我们无法在模板中直接使用 this
访问到这个 number
属性,必须要通过 this.$options
才能访问到它:
const app = Vue.createApp({
number: 3,
template: `
<div>{{this.$options.number}}</div>
`
})
如果我们此时在 mixin
中也定义一个 number
属性:
const myMixin = {
number: 1
}
const app = Vue.createApp({
mixins: [myMixin],
number: 3,
template: `
<div>{{this.$options.number}}</div>
`
})
前面我们说过,mixin
的优先级低于组件优先级,所以此时肯定输出的是 3
,但是如果我们希望 mixin
的优先级高于组件优先级,我们就可以使用 app.config.optionMergeStrategies
自定义选项合并策略:
const myMixin = {
number: 1
}
const app = Vue.createApp({
mixins: [myMixin],
number: 3,
template: `
<div>{{this.$options.number}}</div>
`
})
// 接收两个参数,配置优先返回第一个参数,如找不到在返回第二个参数
app.config.optionMergeStrategies.number = (mixinValue, appValue) => {
return mixinValue || appValue
}
自定义指令
我们先假想一个使用场景,如果我们希望在页面加载的时候自动获取 input
框的焦点事件,我们一般会这样写:
const app = Vue.createApp({
template: `
<input ref="input" />
`,
mounted() {
this.$refs.input.focus()
}
})
假如另一个组件中又有一个 input
,那么我们就又需要在那个组件的 dom
元素节点处定义 ref
,然后在组件的生命周期中调用一遍 this.$refs.input.focus()
。如果我们可以定义一个全局的 autofocus
事件,只要遇到 input
我们就通过给定的指令直接触发那应该怎么办呢?此时我们就可以用到自定义指令了:
const app = Vue.createApp({
template: `
<input ref="input" v-focus />
`
})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
通过过 app.directive
我们定义了一个全局的 focus
指令,指令的使用只需要在前面加上 v-
即可;当然指令也和组件一样,有着生命周期,我们在 mounted
的时候可以拿到使用指令的 dom
元素节点,然后操作这个节点完成对应的功能。当然上面我们使用 app.directive
将指令定义到了全局,日常开发中我们可能更多的是使用局部指令:
// 定义指令集合,因为可以是多个指令,所以是复数
const directives = {
focus: {
mounted(el) {
el.focus()
}
}
}
const app = Vue.createApp({
directives,
template: `
<input ref="input" v-focus />
`
})
动态指令参数
例如我们想通过一个指令实时改变 input
框的位置,那么此时我们写下代码:
const app = Vue.createApp({
template: `
<input class="input" ref="input" v-pos />
`
})
app.directive('pos', {
mounted(el) {
el.style.top = '200px'
}
})
上面这个栗子虽然我们每次使用 v-pos
都会改变输入框的 top
值,但是如果我们希望这个值不是固定的 200
,而是指令中传给我们的数字,那该如何进行改造呢?
官网的文档资料告诉了我们,指令中的参数可以是动态的,例如,在 v-mydirective:[argument]="value"
中,argument
参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。光看文字可能有点糊,我们来使用实际的栗子:
// 场景一:指令接收传值
const app = Vue.createApp({
template: `
<input class="input" ref="input" v-pos="100" />
`
})
app.directive('pos', {
mounted(el, binding) {
// binding.value 是我们传递给指令的值——在这里是 200
el.style.top = binding.value + 'px'
}
})
其实上面的小栗子还有个缺点,就是我们将 v-pos
的值定义在 data
中,但是我们实时改变 data
中的值,页面并不会产生对应的响应式变化。那是因为我们指令注册的过程中 mounted
生命周期只会执行一遍,所以如果我们希望对应变化的产生就可以使用 updated
生命周期:
const app = Vue.createApp({
data() {
return {
top: 100
}
},
template: `
<input class="input" v-pos="top" />
`
})
app.directive('pos', {
mounted(el, binding) {
el.style.top = binding.value + 'px'
},
// 通过 updated 监听指令值的实时变化
updated(el, binding) {
el.style.top = binding.value + 'px'
}
})
const vm = app.mount('#root')
当然如果我们在 mounted
和 updated
时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个回调函数传递给指令来实现:
app.directive('pos', (el, binding) => {
el.style.top = binding.value + 'px'
})
如果应用场景升级,我们不仅希望它只是是在 top
上的偏移,而是通过我们指定传入的方向值进行偏移,那么应该如何实现呢?这时使用动态参数就可以非常方便地根据每个组件实例来进行更新。
// 场景二:动态指令参数
const app = Vue.createApp({
template: `
<input class="input" ref="input" v-pos:[direction]="100" />
`,
data() {
return {
direction: 'bottom'
}
}
})
app.directive('pos', {
mounted(el, binding) {
// binding.arg 是我们传递给指令的参数
const direction = binding.arg || 'top'
el.style[direction] = binding.value + 'px'
}
})
你可以试着使用自定义组件完成一个这样的功能?
插件
我们在 vue
项目中经常会使用别人写好的插件,例如 vue-router
、vue-touch
等,那么我们如何自己编写一个插件呢?看官网的介绍:插件是自包含的代码,通常向 Vue
添加全局级功能。它可以是公开 install()
方法的 object
,也可以是 function
。光看这句话可能有点懵,其实就传达给了我们两点讯息:
1、编写插件可以是一个对象写法,也可以是一个函数写法
2、插件有一个公开的install()
默认方法,它接收 vue 实例和你自定义的属性两个形参。
举个栗子:
// 对象写法:
const myPlugin = {
install(app, options) {
console.log(app, options) // vue 实例,{name: "cc"}
}
}
app.use(myPlugin, { name: 'cc' })
//函数写法:
const myPlugin = (app, options) => {
console.log(app, options)
}
app.use(myPlugin, { name: 'cc' })
插件一般怎么写呢?我们使用插件的时候额外的参数会放到 options
中,而 app
是使用这个插件的时候 vue
对应的实例。我们既然能得到实例,我们就可以对其做很多拓展,例如:
const app = Vue.createApp({
template: `
<child></child>
`
})
// 子组件就可以通过 inject 接收到我们写的插件里的 `name`
app.component('child', {
inject: ['name'],
template: `<div>{{name}}</div>`
})
// 自己写插件,在上面通过 `provide` 拓展一个 name 属性
const myPlugin = (app, options) => {
app.provide('name', 'cc')
}
app.use(myPlugin, { name: 'cc' })
官网栗子中给出了 app.config.globalProperties
这个语法,其实就是对 vue
全局的属性做一些拓展,比如我们想在全局上加入 sayHello
这样一个属性,我们一般会使用 app.config.globalProperties.$sayHello
这样去写,在属性名前加入 $
符号代表这是我们自己在 vue
全局添加的一个私有属性,更方便我们管理。此时我们就可以在组件中直接访问到这个全局私有属性:
app.config.globalProperties.$sayHello = 'hello cc'
// 子组件直接通过 this 使用 $sayHello 属性
app.component('child', {
inject: ['name'],
template: `<div>{{name}}</div>`,
mounted() {
console.log(this.$sayHello)
}
})
结合官网,我们是否可以简单的写一个小插件,例如表单中的 input
框输入检测,对输入的值进行一些基础的校验,如下栗子:
当然,这个简单的小栗子肯定难不倒聪明的我们,其实我们可以使用一个全局 mixin
就可以完成这个功能:
const app = Vue.createApp({
data() {
return {
name: 'cc',
age: '18'
}
},
template: `
<div>
姓名: <input type="text" v-model="name" />
<span class="hint" v-if="this.$options.rules.name.error">
{{this.$options.rules.name.message}}
</span>
</div>
<div>
年龄: <input type="number" v-model="age" />
<span class="hint" v-if="this.$options.rules.age.error">
{{this.$options.rules.age.message}}
</span>
</div>
`,
rules: {
name: {
validate: name => name.length > 3,
error: false,
message: '用户名最少为4个字符'
},
age: {
validate: age => age > 20,
error: false,
message: '年龄不能小于 20 岁'
}
}
})
// 校验插件
const validatorPlugin = (app, options) => {
app.mixin({
created() {
const rules = this.$options.rules
for (let key in rules) {
let item = rules[key]
this.$watch(key, (value) => {
if (!item.validate(value)) {
item.error = true
} else {
item.error = false
}
}, {
immediate: true
})
}
}
})
}
app.use(validatorPlugin)
我们在组件中定义了 rules
属性,所以我们可以通过 this.$options.rules
直接访问到这个属性,然后我们通过 watch
监听 name
和 age
的变化,通过回调函数来校验值是否满足条件,当然判断的过程中我们知道 watch
是有惰性的,所以我们在 watch
的配置中要加上 immediate: true
,这样就可以在页面加载完成时立即执行。这样我们就完成了一个迷你版的 input
校验功能。
自定义 v-model
vue2
中自定义 v-model
的实现及双向绑定的原理我已经写过对应的文章了,Vue 中如何自定义 v-model 及双向绑定的实现原理 ,老版本的 v-model
有几个痛点:
1、比较繁琐,要添加一个 model 属性
2、组件上只能有一个 v-model,如果组件上出现多个 v-model,实现就比较困难
3、对初学者比较不友好,看的云里雾里
所以在 vue3
中对 v-model
也是进行了大刀阔斧的改革,在 vue3
中实现 v-model
不需要再给组件添加一个 model
属性,只需要:
1、在组件的
props
中添加一个modelValue
的属性
2、更新值的时候组件中的emit
时有一个update:modelValue
的方法
我们直接通过一个栗子来认识 vue3
中的自定义 v-model
:
const app = Vue.createApp({
data() {
return {
count: 1
}
},
template: `
<div>{{count}}</div>
<child v-model="count"></child>
`
})
app.component('child', {
// props 中默认的 modelValue 接收父组件中 count 的值
props: {
modelValue: String
},
template: `
<div @click="handleClick">{{modelValue}}</div>
`,
methods: {
handleClick() {
// 组件更新值的时候使用规定的 `update: modelValue` 方法
this.$emit('update:modelValue', this.modelValue + 3)
}
}
})
前面说过,vue3
中可以使用多个 v-model
,那么我们在来看看多个 v-model
的应用:
const app = Vue.createApp({
data() {
return {
count: 1,
age: 18
}
},
template: `
<div>父组件的值</div>
<div>{{count}}</div>
<div>{{age}}</div>
<div>子组件 v-model 绑定的值</div>
<child v-model="count" v-model:age="age"></child>
`
})
app.component('child', {
props: {
modelValue: Number,
age: Number
},
template: `
<div @click="handleClick">{{modelValue}}</div>
<input :value="age" @input="handleInput" type="number" />
`,
methods: {
handleClick() {
this.$emit('update:modelValue', this.modelValue + 3)
},
handleInput(event) {
this.$emit('update:age', +(event.target.value))
}
}
})
当我们在 v-model
后面不接入任何参数时,就可以直接在子组件中使用默认的 modelValue
与父组件中 v-model
的值进行绑定,而当我们在 v-model:age
传入 age
参数之后,对应的子组件的 props
中也需要改成 age
,而更新值的时候组件中的 emit
中的方法也要改成对应的 update: age
。其实新版本中的 v-model
使用更简单更方便,同时可以绑定多个互不干扰。
非 Prop 的 Attribute
官网给出的定义为一个非 prop
的 attribute
是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute
。常见的示例包括 class
、style
和 id
属性。咋看这段解释可能有点懵,其实我们可以通过一些栗子来看问题
Attribute 继承
当组件返回单个根节点时,非 prop attribute
将自动添加到根节点的 attribute
中。例如下列栗子:
const app = Vue.createApp({
template: `
<child type="number" class="parent"></child>
`
})
app.component('child', {
template: `
<div class="child">
<input />
</div>
`
})
被渲染的 child
组件实际代码结构如下:
// class 和 type 都被渲染到根节点上去了
<div class="child parent" type="number">
<input>
</div>
禁用 Attribute 继承
如果你不希望组件的根元素继承 attribute
,你可以在组件的选项中设置 inheritAttrs: false
。例如:禁用 attribute
继承的常见情况是需要将 attribute
应用于根节点之外的其他元素。
通过将 inheritAttrs
选项设置为 false
,你可以访问组件的 $attrs property
,该 property
包括组件 props
和 emits property
中未包含的所有属性 (例如,class
、style
、v-on
监听器等)。还是上面的栗子,我们需要 child
组件中的 input
去渲染对应的 class
和 type
,我们就可以将代码改写一下:
app.component('child', {
inheritAttrs: false,
template: `
<div class="child">
<input v-bind="$attrs" />
</div>
`
})
此时我们再从浏览器中查看 DOM
元素节点就可以看到如下结构:
<div class="child">
<input type="number" class="parent">
</div>
多个根节点上的 Attribute 继承
如果我们的子组件存在多个根节点怎么办,例如:
const app = Vue.createApp({
template: `
<child class="child"></child>
`
})
app.component('child', {
template: `
<div class="header" >
<input />
</div>
<div class="main" v-bind="$attrs">main</div>
<div class="footer">footer</div>
`
})
如果我们不在其中一个根组件使用 v-bind = "$attrs"
控制台就会给我们报错,我们在其中一个根节点上使用之后父组件上对应的 attribute
就会被继承到这个根组件上。
当然我们也可以禁止掉根节点上的继承,直接在 header
结构下的 input
框加入 v-bind = "$attrs"
即可。如下栗子:
app.component('child', {
inheritAttrs: false,
template: `
<div class="header" >
<input v-bind="$attrs" />
</div>
<div class="main">main</div>
<div class="footer">footer</div>
`
})
查漏补缺
我们知道在 vue2
模板中可以通过在 DOM
结构中指定 ref
属性,然后在逻辑代码中通过 this.$refs.
去操作 DOM
,那么在 vue3
中我们应该如何操作 DOM
元素呢?
我们先来一个简单的场景,判断点击的 dom
元素是否在 id = 'index'
的 dom
结构中,场景代码如下:
// 判断点击的 dom 元素节点是不是在 index 中
const app = Vue.createApp({
template: `
<div id="index">
<div id="index-list">555</div>
</div>
<div id="demo">666</div>
`,
})
此时我们就要进行 DOM
元素节点判断,结合 setup
我们应该如何去使用 ref
来获取 dom
元素节点呢?代码比较简单就直接上结果了:
const app = Vue.createApp({
template: `
<div id="index" ref="index">
<div id="index-list">555</div>
</div>
<div id="demo">666</div>
`,
setup() {
const { ref, onMounted } = Vue
const index = ref(null)
onMounted(() => {
document.addEventListener('click', function (e) {
if (index.value.contains(e.target)) {
console.log('点击的是 index 里面的元素')
} else {
console.log('点击的不是 index 里面的元素')
}
})
})
return { index }
}
})
因为 setup
的执行是在 beforeCreate
和 created
之间,所以我们如果想拿到对应的 dom
元素节点,最好在其内部的生命周期中进行获取。
vue3
中相较 vue2
大体的改动和日常开发中经常会遇到的问题基本都已经整理的差不多了,由于 vue3
代码基本都是 ts
写的,所以学习 ts
其实已经迫在眉睫的。结尾综合做个小栗子吧:
其实很简单,就是一个 form 表单提交验证,不过封装基本用的是 vue3 + ts
,小伙伴可以自己独立实现一个类似 element-ui
中的表单效验插件,其实组件开发更多的是学习思路以及代码的扩展性,优雅性。最近 github
经常打不开,源代码就放在 gitee 上了。本文多为自己学习笔记记录,如有错误,欢迎指正!!!