组件基础
有了组件,我们就可以将ui变成独立且可重用的部分。
定义组件
当使用构建步骤时,一般会把vue定义在一个单独的.vue文件,也就是单文件组件
当不使用构建步骤时,一个vue组件以一个包含vue特定选项的JavaScript对象来定义(不常用)
使用组件
要使用子组件,需要先在父组件中导入它。导入后的组件在模板中都直接可用
js
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
也可以通过全局注册组件,使它不需要注册就可以在任何组件上使用
- 每个组件都维护着自己的状态,每当使用一个新组件,等于创建了一个新的实例,互不影响
传递props
如果我们在开发一个blog,希望每篇文章的视觉布局一样,但内容不一样,也就是需要向组件传递数据,这时就会用到props
props 是一种特别的attributes。作为要传递给组件的数据,我们必须在组件的props列表上声明它,defineProps()表示这个组件可以接收哪些数据:
js
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
声明的props会自动暴露给模板,defineProps会返回一个对象,其中包含了可以传递给组件的所有props:
js
const props = defineProps(['title'])
console.log(props.title)
如果没有使用<script setup>,props必须以props选项的方式声明,props会作为setup()函数的第一个参数被传入
js
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。
js
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:
js
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传入子组件:
js
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
子组件接收并渲染:
js
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
- 注意这里
:title="post.title"是v-bind = "post.title"的缩写,v-bind可以把数据动态绑定到HTML属性或组件props上 ,使html属性 = JS变量
监听事件
让我们回到<BlogPost>组件,有时候它也需要与父组件进行交互,例如要实现无障碍访问的需求,将文章文字放大,而页面其他部分仍使用默认字号
在父组件中,我们可以添加一个postFontSize的ref来实现这个效果:
js
const posts = ref([
/* ... */
])
const postFontSize = ref(1)
在模板中用它来控制所有博客文章的字体大小:
js
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>
然后,给<BlogPost>组件添加一个按钮:
js
<!-- 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button>Enlarge text</button>
</div>
</template>
我们想要通过点击这个按钮,告诉父组件它应该放大所有博客文章的文字。
要实现这个问题,可以通过组件实例提供的自定义事件系统。父组件可以通过v-on或@来选择性监听子组件上抛的事件,就像监听原生dom事件一样:
js
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>
- 此处
@enlarge-text="postFontSize += 0.1"为v-on:enlarge-text = "postFontSize += 0.1"的缩写,即监听enlarge-text事件,当事件发生后会执行postFontSize += 0.1
子组件则通过调用内置的$emit方法,通过传入事件名称来抛出一个事件:
js
<!-- 省略了 <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 的值。
要抛出的事件需要通过defineEmits()来声明:
js
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
和defineProps类似,defineEmits 仅可用于 <script setup> 之中,并且不需要导入
通过插槽来分配内容
有时我们会希望能和 HTML 元素一样向组件中传递内容:
html
<AlertBox>
Something bad happened.
</AlertBox>
我们期望能渲染成这样:

这可以通过vue的自定义<slot>元素来实现:
js
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>
也就是使用<slot />作为占位符,父组件传递来的内容会渲染在这里
app.vue
js
<script setup>
import AlertBox from './AlertBox.vue'
</script>
<template>
<AlertBox>
Something bad happened.
</AlertBox>
</template>
AlertBox.vue
js
<template>
<div class="alert-box">
<strong>Error!</strong>
<br/>
<slot />
</div>
</template>
动态组件
有些场景会需要在两个组件之间来回切换,比如Tab界面,通常是通过vue的<component>元素和特殊的is的attribute
js
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
在上面的例子中,被传给 :is 的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive> 组件强制被切换掉的组件仍然保持"存活"的状态。
App.vue
js
<script setup>
import Home from './Home.vue'
import Posts from './Posts.vue'
import Archive from './Archive.vue'
const currentTab = ref('Home')
const tabs = {
Home,
Posts,
Archive
}
</script>
<div class="demo">
<button
v-for="(_, tab) in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab"
>
{{ tab }}
</button>
<component :is="tabs[currentTab]" class="tab"></component>
</div>