Skip to content

VueJS - 组件

组件 (Components) 是构建 Vue(以及许多其他现代框架)应用的基础。它们允许你将 用户界面 (UI) 拆分成独立、可重用的部分,每个部分管理自己的 逻辑 (logic) 和 标记 (markup)。可以把它们想象成你可以在整个应用程序中定义和复用的自定义 HTML 元素。

在 Vue 3 中,组件通常定义为包含选项的对象 (Options API) 或使用 setup 函数 (Composition API)。为了更好的组织性,尤其是在 构建环境 (build environments) 中, 单文件组件 (Single-File Components, .vue 文件) 是定义组件的标准方式,它将 模板 (template)、脚本 (script) 和 样式 (style) 放在一起。

让我们在一个假设的 CounterButton.vue 文件中创建一个简单的计数器按钮组件:

CounterButton.vue
<script setup>
import { ref } from 'vue';
// Reactive state specific to this component instance
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">
You clicked me {{ count }} times
</button>
</template>
<style scoped>
/* Styles specific to this component */
button {
background-color: lightgreen;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
button:hover {
background-color: darkgreen;
color: white;
}
</style>

<script setup> 是在 单文件组件 (SFC) 中使用 Composition API 的 编译时语法糖 (compile-time syntactic sugar)。在这里声明的变量和函数会自动暴露给模板。<style> 标签上的 scoped 属性确保 CSS 只应用于此组件内部的元素。

在使用组件之前,需要先注册它。

全局注册的组件可以在该 应用实例 (application instance) 内的任何组件模板中使用,无需单独导入。使用 app.component():

// main.js (Your application entry point)
import { createApp } from 'vue';
import App from './App.vue';
import GlobalComponent from './components/GlobalComponent.vue';
const app = createApp(App);
// Register 'GlobalComponent' globally
app.component('GlobalComponent', GlobalComponent);
// Now <GlobalComponent /> can be used anywhere in App or its children
app.mount('#app');

全局注册很方便,但如果注册了许多未使用的组件,可能会导致 打包体积 (bundle sizes) 较大。它通常更适合用于通用、广泛使用的组件(如基础按钮、输入框、模态框等)。

局部注册的组件仅在其注册的组件内部可用。对于大多数应用程序组件来说,这是首选方法,因为它使得依赖关系更加明确,并且更好地支持 摇树优化 (tree-shaking)(移除未使用的代码)。

在 <script setup> 中,只需导入组件即可使其在模板中可用:

ParentComponent.vue
<script setup>
// Import the component
import LocalButton from './LocalButton.vue';
import AnotherComponent from './AnotherComponent.vue';
</script>
<template>
<div>
<p>Using a locally registered component:</p>
<!-- Use the imported component directly (PascalCase or kebab-case) -->
<LocalButton />
<another-component></another-component> <!-- kebab-case also works -->
</div>
</template>

如果未使用 <script setup>(例如,使用 Options API),则需要使用 components 选项:

// ParentComponent.vue (Options API example)
import LocalButton from './LocalButton.vue';
export default {
components: {
// Register the component locally
LocalButton
// You could also rename it: 'MyButton': LocalButton
}
// ... other options like data, methods
}

组件通常需要接收来自父组件的数据。这是通过 props 来完成的。Props 是你可以在组件上注册的自定义属性/特性。父组件可以通过绑定到这些属性来将数据传递给子组件。

Greeting.vue
<script setup>
// Define the props the component accepts
// 'defineProps' is a compiler macro, no import needed
const props = defineProps({
name: {
type: String, // Type validation
required: true, // Make the prop required
default: 'World' // Default value if not provided (though 'required' makes this less relevant)
}
});
// Props are accessible directly in the template,
// or via the 'props' object in script setup
console.log('Greeting received name:', props.name);
</script>
<template>
<h2>Hello, {{ name }}!</h2>
</template>

defineProps 是一个 编译器宏 (compiler macro),无需导入。Props 在模板中可以直接访问,或者在 <script setup> 中通过 props 对象访问。

在父组件中使用 Greeting 组件:

App.vue
<script setup>
import { ref } from 'vue';
import Greeting from './Greeting.vue';
const userName = ref('Alice');
</script>
<template>
<div>
<!-- Pass static value -->
<Greeting name="Bob" />
<!-- Bind to dynamic value using v-bind (or :) -->
<Greeting :name="userName" />
<input v-model="userName" placeholder="Change name"/>
</div>
</template>

Props 允许 单向数据流 (one-way data flow):父组件将数据传递给子组件。子组件不应该直接修改 props。

触发事件 (Emitting Events):子组件到父组件通信

Section titled “触发事件 (Emitting Events):子组件到父组件通信”

由于子组件不应该通过 props 直接修改父组件的状态,它们通过 触发自定义事件 来与父组件通信。父组件使用 v-on(或 @)监听这些事件。

示例:触发 ‘update’ 事件的自定义输入组件

Section titled “示例:触发 ‘update’ 事件的自定义输入组件”
CustomInput.vue
<script setup>
// Define props (e.g., modelValue for v-model compatibility)
const props = defineProps(['modelValue']);
// Define the events the component can emit
// 'defineEmits' is a compiler macro
const emit = defineEmits(['update:modelValue', 'focused']);
function handleInput(event) {
// Emit the 'update:modelValue' event with the new value
// This pattern allows the component to work with v-model
emit('update:modelValue', event.target.value);
}
function handleFocus() {
emit('focused'); // Emit a custom 'focused' event
}
</script>
<template>
<input
type="text"
:value="modelValue"
@input="handleInput"
@focus="handleFocus"
placeholder="Custom Input"
/>
</template>

defineProps 和 defineEmits 都是 编译器宏 (compiler macro),无需导入。

在父组件中使用 CustomInput:

App.vue
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const textValue = ref('');
function onInputFocus() {
console.log('Custom input was focused!');
}
</script>
<template>
<div>
<p>Current Value: {{ textValue }}</p>
<!-- Use v-model (which listens for 'update:modelValue' event) -->
<CustomInput v-model="textValue" @focused="onInputFocus" />
<!-- Explicitly listening (equivalent to v-model): -->
<!-- <CustomInput
:modelValue="textValue"
@update:modelValue="newValue => textValue = newValue"
@focused="onInputFocus" /> -->
</div>
</template>

子组件触发事件(update:modelValue、focused),父组件监听(v-model、@focused)以进行相应的响应。

有时你需要动态切换不同的组件。Vue 提供了带有 is 特殊属性的 <component> 元素来实现这一目的。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Dynamic Components</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
.tab-button { padding: 6px 10px; border-bottom: 2px solid #ccc; cursor: pointer; background: #f0f0f0; margin-right: -1px;}
.tab-button:hover { background: #e0e0e0; }
.tab-button.active { background: #e0e0e0; border-bottom-color: green; }
.tab-content { border: 1px solid #ccc; padding: 10px; }
</style>
</head>
<body>
<div id="app">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-button', { active: currentTab === tab.component }]"
@click="currentTab = tab.component"
>
{{ tab.name }}
</button>
<!-- Dynamic component is rendered here -->
<div class="tab-content">
<!-- Use keep-alive to preserve state when switching -->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
</div>
</div>
<script>
const { createApp, ref, shallowRef, computed } = Vue;
// Define some simple components (could be imported from .vue files)
const TabHome = { template: '<p>Home component content</p>' };
const TabPosts = { template: '<p>Posts component content</p>' };
const TabArchive = { template: '<p>Archive component content</p>' };
const app = createApp({
setup() {
const tabs = shallowRef([ // Use shallowRef for non-deep reactivity on the array itself
{ id: 1, name: 'Home', component: 'TabHome' },
{ id: 2, name: 'Posts', component: 'TabPosts' },
{ id: 3, name: 'Archive', component: 'TabArchive' }
]);
const currentTab = ref('TabHome'); // Holds the *name* of the current component
// Computed property to resolve the actual component object
const currentTabComponent = computed(() => {
switch (currentTab.value) {
case 'TabHome': return TabHome;
case 'TabPosts': return TabPosts;
case 'TabArchive': return TabArchive;
default: return TabHome;
}
});
return { tabs, currentTab, currentTabComponent };
}
});
// Register components (globally for this simple example)
app.component('TabHome', TabHome);
app.component('TabPosts', TabPosts);
app.component('TabArchive', TabArchive);
app.mount('#app');
</script>
</body>
</html>

在这个选项卡式界面的例子中:

  • currentTab ref 存储要显示的组件的名称。
  • 计算属性 currentTabComponent 将组件名称解析为实际的组件对象。
  • <component :is="currentTabComponent"> 渲染由计算属性决定的组件。
  • 点击按钮改变 currentTab,从而更新 currentTabComponent,导致 <component> 元素渲染新的组件。
  • <keep-alive> 用于缓存不活动的组件,以便在来回切换时保留它们的状态。

组件是构建可扩展和可维护的 Vue 应用程序的基础。掌握 props、事件和 组件组合 (component composition) 是精通 Vue 的关键。

进一步阅读:Vue 官方文档中关于组件基础的章节:https://vuejs.org/guide/essentials/component-basics.html 和组件深入的章节:https://vuejs.org/guide/components/registration.html