组件基础
组件允许我们将UI划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织为一个层层嵌套的树状结构:
这和嵌套HTML元素的方式类似,Vue实现了自己的组件模型,使我们可以在每个组件内封装自定义内容和逻辑。 Vue同样也能很好地配合原生Web Componeent。
定义一个组件
当使用构建步骤时,我们一般会将Vue组件定义在一个单独的.vue文件中,这杯叫做单文件组件(SFC):
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
当不使用构建步骤时,一个Vue组件以一个包含Vue特定选项地JavaScript对象来定义:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 也可以针对一个 DOM 内联模板:
// template: '#my-template-element'
}
这里的模板是一个内联的JavaScript字符串,Vue将会在运行时编译它。你也可以使用ID选择器来指向一个元素(通常是原生的<template>元素),Vue将会使用其内容作为模板来源。
使用组件
要使用一个子组件,需要在父组件中导入它。假设把计数器组件放在了一个叫做ButtonCounter.vue的文件中,这个组件将会以默认导出的形式被暴露给外部。
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
(一般情况下,在外部使用一个js文件既需要在本文件导出又需要导入到需求文件,这里如果用了构建步骤就不用导出了,直接导入即可)
通过<script setup>,导入的组件都在模板中直接可用。
当然也可以全局地注册一个组件,使得它在当前应用中地任何组件上都可以使用,而不需要额外再导入。
组件可以被重用任意多次:
<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
你会注意到,每当点击这些按钮时,每一个组件都维护者自己的状态,是不同地count。这是因为每当你使用一个组件,就创建了一个新的实例。
在单文件组件中,推荐为子组件使用PascalCase地标签名,以次来和原生地HTML元素作区分。虽然原生HTML标签名是不区分大小写地,但Vue单文件组件时可以在编译中区分大小写地。也可以使用/>来关闭一个标签。
如果是直接在DOM中书写模板(例如原生<template>元素的内容),模板的编译需要遵从浏览器中HTML地解析行为。在这种情况下,应该需要使用 kebab-case 形式并显式地关闭这些组件的标签。
<!-- 如果是在 DOM 中书写该模板 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
传递props
假设我们正在构建一个博客,可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果必须向组件传递数据,例如每篇文章标题和内容,这就会使用到props。
Props是一种特别的attributes,可以在组件上声明注册。要传递给博客文章组件一个标题,必须在组件的props列表上声明它。这里要用到 defineProps 宏:
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
defineProps 是一个仅<script setup>中可用的编译宏命令,并不需要显式地导入。声明地props会自动暴露给模板。defineProps 会返回一个对象,其中包含了可以传递给组件的所有props:
const props = defineProps(['title'])
console.log(props.title)
如果你没有使用<script setup>, props必须以props选项的方式声明,props对象会作为setup()函数的第一个参数被传入:
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
一个组件可以又任意多的props,默认情况下,所有prop都接受任意类型的值。
当一个prop被注册后,可以像这样以自定以attribtue的形式传递数据给它:
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
在实际应用中,可能在父组件中会有如下的一个博文文章数组:
const posts = ref([
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
])
这种情况下,可以使用v-for来渲染它们:
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
留意这里使用了v-bind语法(:title="post.title")来传递动态prop值。当事先不知道要渲染的确切内容时,这一点特别有用。
监听事件
有时我们会发现它需要与父组件进行交互。例如,要在此处实现无障碍访问的需求,将博客文章的文字能够放大,而页面的其余部分仍然使用默认字号。
在父组件中,可以添加一个postFontSize ref来实现这个效果:
const posts = ref([
/* ... */
])
const postFontSize = ref(1)
在模板中用它来控制所有博客文章的字体大小:
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>
然后,给<BlogPost>组件添加一个按钮:
<!-- BlogPost.vue, 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button>Enlarge text</button>
</div>
</template>
这个按钮目前还没有做任何事情,我们想要点击这个按钮来告诉父组件它应该放大所有博客文章的文字。要解决这个问题,组件实例提供了一个自定义事件系统。父组件可以通过v-on或@来选择性地监听子组件上抛地事件,就像监听原生DOM事件那样:
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>
子组件可以通过调用内置的$emit方法,通过传入事件名称来抛出一个事件:
<!-- BlogPost.vue, 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
因为有了@enlarge-text = "postFontSize +=0.1"的监听,父组件会接收这一事件,从而更新postFontSize的值。
( 上述一连串的内容可以看作一件事,父组件定义了子组件字体的数据和修改数据的方法,子组件button用$emit向父组件抛出修改字体的方法)
我们可以通过defineEmits宏来声明需要抛出的事件:
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
这声明一个组件可能触发的所有事件,还可以对事件的参数惊醒验证。同时,这还可以让Vue避免将它们作为原生事件监听器隐式地应用于子组件的根元素。
和 defineProps 类似,defineEmits仅可用于<script setup>之中,并且不需要导入,它返回一个等同于$emit方法的emit函数。它可以被用于在组件的<script setup>中抛出事件,因为此处无法直接访问$emit:
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>
如果你没有在使用<script setup>,你可以通过emits选项定义组件会抛出的事件。你可以从setup()函数的第二个参数,即setup上下文对象上访问到emit函数:
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
通过插槽来分配内容
一些情况下我们会希望能和HMTL元素一样向组件中传递内容:
<AlertBox>
Something bad happened.
</AlertBox>
这可以通过Vue的自定义<slot>元素来实现:
<!-- AlertBox.vue -->
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>
动态组件
有些场景会需要在两个组件间来回切换,例如Tab界面:
可以通过Vue的<component>元素和特殊的is attribute实现。
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
被传给 :is 的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
也可以使用 is attribute 来创建一般的HTML元素。
当使用<component :is="..."> 来在多个组件间作切换,被切换掉的组件会被卸载。可以通过<KeepAlive>组件强制被切换的组件仍然保持“存活”的状态。
DOM内模板解析注意事项
如果你想在DOM中直接书写Vue模板,Vue则必须从DOM中获取模板字符串。由于浏览器的原生HTML解析行为限制,有一些需要注意的事项。
请注意下面讨论只适用于直接在 DOM 中编写模板的情况。如果你使用来自以下来源的字符串模板,就不需要顾虑这些限制了:
- 单文件组件
- 内联模板字符串 (例如
template: '...'
)<script type="text/x-template">
大小写区分
HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写。这意味着当你使用 DOM 内的模板时,无论是 PascalCase 形式的组件名称、camelCase 形式的 prop 名称还是 v-on 的事件名称,都需要转换为相应等价的 kebab-case (短横线连字符) 形式:
// JavaScript 中的 camelCase
const BlogPost = {
props: ['postTitle'],
emits: ['updatePost'],
template: `
<h3>{{ postTitle }}</h3>
`
}
<!-- HTML 中的 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>
闭合标签
之前已经使用过了闭合标签(self-closing tag):
<MyComponent />
这是因为 Vue 的模板解析器支持任意标签使用 /> 作为标签关闭的标志。
然而在 DOM 内模板中,必须显式地写出关闭标签:
<my-component></my-component>
这是由于 HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是 <input>
和 <img>
。对于其他的元素来说,如果你省略了关闭标签,原生的 HTML 解析器会认为开启的标签永远没有结束。
元素位置限制
某些 HTML 元素对于放在其中的元素类型有限制,例如 <ul>
,<ol>
,<table>
和 <select>
,相应的,某些元素仅在放置于特定元素中时才会显示,例如 <li>
,<tr>
和 <option>
。
这将导致在使用带有此类限制元素的组件时出现问题。例如:
<table>
<blog-post-row></blog-post-row>
</table>
自定义的组件<blog-post-row>将作为无效的内容被忽略,因而在最终呈现的输出中造成错误。可以使用特殊的 is attribute 作为一种解决方案:
<table>
<tr is="vue:blog-post-row"></tr>
</table>
当使用在原生 HTML 元素上时,is的值必须加上前缀vue: 才可以被解析为一个 Vue 组件。这一点是必要的,为了避免和原生的自定义内置元素相混淆。