Vue3 初探(二) 新特性

这篇博客我们继续来看Vue3的一些新特性,包括Teleport,片段,触发组件选项。

大部分内容来自官方文档,对部分难理解的部分进行了解释与总结。

Teleport

Vue 鼓励我们通过将 UI 和相关行为封装到组件中来构建 UI。我们可以将它们嵌套在另一个内部,以构建一个组成应用程序 UI 的树。

然而,有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分移动到 DOM 中 Vue app 之外的其他位置。

也就是说我们希望有一种组件,它在逻辑上属于A组件的子组件,可以与A进行正常的父子组件可以进行的操作,但是实际上他又渲染为B组件的子组件

一个常见的场景是创建一个包含全屏模式的组件。在大多数情况下,你希望模态的逻辑存在于组件中,但是模态的定位很快就很难通过 CSS 来解决,或者需要更改组件组合。

考虑下面的 HTML 结构。

1
2
3
4
5
6
7
8
<body>
<div style="position: relative;">
<h3>Tooltips with Vue 3 Teleport</h3>
<div>
<modal-button></modal-button>
</div>
</div>
</body>

让我们来看看 modal-button 组件:

该组件将有一个 button 元素来触发模态的打开,以及一个具有类 .modaldiv 元素,它将包含模态的内容和一个用于自关闭的按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const app = Vue.createApp({});

app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal!
</button>

<div v-if="modalOpen" class="modal">
<div>
I'm a modal!
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
`,
data() {
return {
modalOpen: false
}
}
})

当在初始的 HTML 结构中使用这个组件时,我们可以看到一个问题——模态是在深度嵌套的 div 中渲染的,而模态的 position:absolute 以父级相对定位的 div 作为引用。

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。

让我们修改 modal-button 以使用 <teleport>,并告诉 Vue “Teleport 这个 HTML 该‘body’标签”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>

<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})

因此,一旦我们单击按钮打开模式,Vue 将正确地将模态内容渲染为 body 标签的子级。

与 Vue components 一起使用

如果 <teleport> 包含 Vue 组件,则它仍将是 <teleport> 父组件的逻辑子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const app = Vue.createApp({
template: `
<h1>Root instance</h1>
<parent-component />
`
})

app.component('parent-component', {
template: `
<h2>This is a parent component</h2>
<teleport to="#endofbody">
<child-component name="John" />
</teleport>
`
})

app.component('child-component', {
props: ['name'],
template: `
<div>Hello, {{ name }}</div>
`
})

在这种情况下,即使在不同的地方渲染 child-component,它仍将是 parent-component 的子级,并将从中接收 name prop。

这也意味着来自父组件的注入按预期工作,并且子组件将嵌套在 Vue Devtools 中的父组件之下,而不是放在实际内容移动到的位置。

片段

在 Vue 3 中,组件现在正式支持多根节点组件,即片段!

2.x 语法

在 2.x 中,不支持多根组件,当用户意外创建多根组件时会发出警告,因此,为了修复此错误,许多组件被包装在一个 <div> 中。

1
2
3
4
5
6
7
8
<!-- Layout.vue -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>

3.x 语法

在 3.x 中,组件现在可以有多个根节点!但是,这确实要求开发者明确定义属性应该分布在哪里。

1
2
3
4
5
6
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>

非Prop的Attribute的继承

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 propsemits 定义的 attribute。常见的示例包括 classstyleid 属性。

Attribute 继承

当组件返回单个根节点时,非 prop attribute 将自动添加到根节点的 attribute 中。例如,在 <date-picker> 组件的实例中:

1
2
3
4
5
6
7
app.component('date-picker', {
template: `
<div class="date-picker">
<input type="datetime" />
</div>
`
})

如果我们需要通过 data status property 定义 <date-picker> 组件的状态,它将应用于根节点 (即 div.date-picker)。

1
2
3
4
5
6
7
<!-- 具有非prop attribute的Date-picker组件-->
<date-picker data-status="activated"></date-picker>

<!-- 渲染 date-picker 组件 -->
<div class="date-picker" data-status="activated">
<input type="datetime" />
</div>

同样的规则适用于事件监听器

多个根节点上的 Attribute 继承

与单个根节点组件不同,具有多个根节点的组件不具有自动 attribute 回退行为。如果未显式绑定 $attrs,将发出运行时警告。

1
<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 这将发出警告
app.component('custom-layout', {
template: `
<header>...</header>
<main>...</main>
<footer>...</footer>
`
})

// 没有警告,$attrs被传递到<main>元素
app.component('custom-layout', {
template: `
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
`
})

自定义事件

事件名

不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件:

1
this.$emit('myEvent')

则监听这个名字的 kebab-case 版本是不会有任何效果的:

1
2
<!-- 没有效果 -->
<my-component @my-event="doSomething"></my-component>

不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 @myEvent 将会变成 @myevent——导致 myEvent 不可能被监听到。

因此,我们推荐你始终使用 kebab-case 的事件名

定义自定义事件

可以通过 emits 选项在组件上定义已发出的事件。

1
2
3
app.component('custom-form', {
emits: ['in-focus', 'submit']
})

当在 emits 选项中定义了原生事件 (如 click) 时,将使用组件中的事件替代原生事件侦听器。

验证抛出的事件

与 prop 类型验证类似,如果使用对象语法而不是数组语法定义发出的事件,则可以验证它。

要添加验证,将为事件分配一个函数,该函数接收传递给 $emit 调用的参数,并返回一个布尔值以指示事件是否有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.component('custom-form', {
emits: {
// 没有验证
click: null,

// 验证submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm() {
this.$emit('submit', { email, password })
}
}
})

多个 v-model 绑定

通过利用以特定 prop 和事件为目标的能力,正如我们之前在 v-model 参数中所学的那样,我们现在可以在单个组件实例上创建多个 v-model 绑定。

每个 v-model 将同步到不同的 prop,而不需要在组件中添加额外的选项:

1
2
3
4
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const app = Vue.createApp({})

app.component('user-name', {
props: {
firstName: String,
lastName: String
},
template: `
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)">

<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)">
`
})

自定义v-model修饰符

在 2.x 中,我们对组件 v-model 上的 .trim 等修饰符提供了硬编码支持。但是,如果组件可以支持自定义修饰符,则会更有用。在 3.x 中,添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件:

让我们创建一个示例自定义修饰符 capitalize,它将 v-model 绑定提供的字符串的第一个字母大写。

添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers prop。

请注意,当组件的 created 生命周期钩子触发时,modelModifiers prop 包含 capitalize,其值为 true——因为它被设置在 v-model 绑定 v-model.capitalize="bar"

现在我们已经设置了 prop,我们可以检查 modelModifiers 对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 <input/> 元素触发 input 事件时,我们都将字符串大写。

1
2
3
4
<div id="app">
<my-component v-model.capitalize="myText"></my-component>
{{ myText }}
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const app = Vue.createApp({
data() {
return {
myText: ''
}
}
})

app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
template: `<input
type="text"
:value="modelValue"
@input="emitValue">`
})

app.mount('#app')

对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + "Modifiers"

1
<my-component v-model:foo.capitalize="bar"></my-component>
1
2
3
4
5
6
7
8
9
10
11
app.component('my-component', {
props: ['foo', 'fooModifiers'],
template: `
<input type="text"
:value="foo"
@input="$emit('update:foo', $event.target.value)">
`,
created() {
console.log(this.fooModifiers) // { capitalize: true }
}
})