组件是Vue应用的基本构建块,允许我们将UI拆分成独立、可复用的部分。每个组件都有自己的模板、逻辑和样式。
Vue的单文件组件将模板、脚本和样式封装在一个.vue文件中,包含三个主要部分:
<!-- MyButton.vue -->
<template>
<button @click="count++" class="my-button">
点击了 {{ count }} 次
</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<style scoped>
.my-button {
background: #42b883;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.my-button:hover {
background: #369870;
}
</style>
<!-- MyButton.vue -->
<template>
<button @click="count++" class="my-button">
点击了 {{ count }} 次
</button>
</template>
<script>
export default {
name: 'MyButton',
data() {
return {
count: 0
}
}
}
</script>
<style scoped>
.my-button {
background: #42b883;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
<!-- App.vue -->
<template>
<div class="app">
<h1>我的应用</h1>
<div class="button-group">
<MyButton />
<MyButton />
<MyButton />
</div>
</div>
</template>
<script setup>
import MyButton from './components/MyButton.vue'
</script>
<style>
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
</style>
每个组件实例都维护自己的状态,互不影响。这是组件化开发的核心特性之一。
Props用于父组件向子组件传递数据。
<!-- BlogPost.vue -->
<template>
<div class="blog-post" :class="{ featured: isFeatured }">
<h3>{{ title }}</h3>
<p class="content">{{ content }}</p>
<div class="meta">
<span class="author">作者: {{ author }}</span>
<span class="date">发布时间: {{ publishDate }}</span>
</div>
<div v-if="tags.length" class="tags">
<span v-for="tag in tags" :key="tag" class="tag">{{ tag }}</span>
</div>
</div>
</template>
<script setup>
// 定义props
const props = defineProps({
title: {
type: String,
required: true
},
content: {
type: String,
default: ''
},
author: {
type: String,
default: '匿名'
},
publishDate: {
type: String,
default: '未知'
},
tags: {
type: Array,
default: () => []
},
isFeatured: {
type: Boolean,
default: false
}
})
// 在模板中可以直接使用props,无需props.前缀
</script>
<style scoped>
.blog-post {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.blog-post.featured {
border-color: #42b883;
background-color: #f8fff8;
}
.content {
line-height: 1.6;
color: #666;
}
.meta {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 0.9em;
color: #999;
}
.tags {
margin-top: 10px;
}
.tag {
display: inline-block;
background: #f0f0f0;
padding: 2px 8px;
border-radius: 4px;
margin-right: 5px;
font-size: 0.8em;
}
</style>
<!-- App.vue -->
<template>
<div class="app">
<h1>博客文章列表</h1>
<!-- 静态props传递 -->
<BlogPost
title="Vue3学习笔记"
content="Vue3是一个很棒的框架,提供了更好的性能和开发体验。"
author="张三"
publishDate="2024-01-15"
:tags="['Vue3', '前端', 'JavaScript']"
:isFeatured="true"
/>
<!-- 动态props传递 -->
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
:content="post.content"
:author="post.author"
:publishDate="post.publishDate"
:tags="post.tags"
:isFeatured="post.isFeatured"
/>
<!-- 使用对象展开语法 -->
<BlogPost v-bind="newPost" />
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import BlogPost from './components/BlogPost.vue'
const posts = ref([
{
id: 1,
title: '文章1',
content: '这是第一篇文章的内容',
author: '作者1',
publishDate: '2024-01-10',
tags: ['技术', '分享'],
isFeatured: false
},
{
id: 2,
title: '文章2',
content: '这是第二篇文章的内容',
author: '作者2',
publishDate: '2024-01-12',
tags: ['Vue', '教程'],
isFeatured: true
}
])
const newPost = reactive({
title: '新文章',
content: '这是通过v-bind传递的新文章',
author: '系统',
publishDate: '2024-01-20',
tags: ['新功能'],
isFeatured: false
})
</script>
<style>
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
</style>
Props验证是Vue组件开发中的重要环节,它能够:
<script setup>
// 完整的props验证示例
defineProps({
// 基础类型检查
title: String,
likes: Number,
isPublished: Boolean,
tags: Array,
author: Object,
callback: Function,
// 多种可能的类型
propA: [String, Number],
// 必填的字符串
propB: {
type: String,
required: true,
validator: (value) => value.length > 0
},
// 带有默认值的数字
propC: {
type: Number,
default: 100,
validator: (value) => value >= 0 && value <= 100
},
// 带有默认值的对象(必须使用工厂函数)
propD: {
type: Object,
default: () => ({
message: 'hello',
count: 0
})
},
// 带有默认值的数组(必须使用工厂函数)
propE: {
type: Array,
default: () => []
},
// 自定义验证函数
propF: {
validator: (value) => {
return ['success', 'warning', 'danger'].includes(value)
}
},
// 复杂对象验证
user: {
type: Object,
required: true,
validator: (value) => {
return value && typeof value === 'object' &&
'name' in value && 'email' in value
}
}
})
</script>
子组件通过emit向父组件发送事件。
<!-- Counter.vue -->
<template>
<div class="counter">
<h3>计数器组件</h3>
<div class="controls">
<button @click="decrement" class="btn btn-decrement">-</button>
<span class="count">{{ count }}</span>
<button @click="increment" class="btn btn-increment">+</button>
</div>
<div class="actions">
<button @click="reset" class="btn btn-reset">重置</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
// 定义emits(推荐使用对象语法进行验证)
const emit = defineEmits({
// 无参数的简单事件
increment: null,
// 带参数的事件
change: (payload) => {
// 验证payload结构
return typeof payload === 'object' &&
'count' in payload &&
'timestamp' in payload
},
// 带验证的事件
reset: (value) => {
return value === 0
}
})
function increment() {
count.value++
emit('increment')
emit('change', {
count: count.value,
timestamp: Date.now(),
action: 'increment'
})
}
function decrement() {
if (count.value > 0) {
count.value--
emit('change', {
count: count.value,
timestamp: Date.now(),
action: 'decrement'
})
}
}
function reset() {
const oldValue = count.value
count.value = 0
emit('reset', oldValue)
emit('change', {
count: count.value,
timestamp: Date.now(),
action: 'reset',
oldValue: oldValue
})
}
</script>
<style scoped>
.counter {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
max-width: 300px;
margin: 0 auto;
}
.controls {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin: 20px 0;
}
.count {
font-size: 24px;
font-weight: bold;
min-width: 50px;
}
.btn {
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
}
.btn-increment {
background: #42b883;
color: white;
}
.btn-decrement {
background: #f44336;
color: white;
}
.btn-reset {
background: #ff9800;
color: white;
}
.actions {
margin-top: 15px;
}
</style>
<!-- App.vue -->
<template>
<div class="app">
<h1>事件通信示例</h1>
<Counter
@increment="handleIncrement"
@change="handleChange"
@reset="handleReset"
/>
<div class="status">
<h3>父组件状态</h3>
<p>当前计数: {{ parentCount }}</p>
<p>最后操作: {{ lastAction }}</p>
<p>操作时间: {{ lastTimestamp ? new Date(lastTimestamp).toLocaleString() : '暂无' }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Counter from './components/Counter.vue'
const parentCount = ref(0)
const lastAction = ref('')
const lastTimestamp = ref(null)
function handleIncrement() {
console.log('子组件触发了增加事件')
}
function handleChange(payload) {
parentCount.value = payload.count
lastAction.value = payload.action
lastTimestamp.value = payload.timestamp
console.log('计数变化:', payload)
}
function handleReset(oldValue) {
console.log('计数器已重置,原值:', oldValue)
lastAction.value = 'reset'
lastTimestamp.value = Date.now()
}
</script>
<style>
.app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.status {
margin-top: 30px;
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
}
h1 {
text-align: center;
color: #333;
}
</style>
v-model是Vue中实现双向数据绑定的语法糖,它实际上是以下代码的简写:
<CustomInput
:modelValue="message"
@update:modelValue="newValue => message = newValue"
/>
在Vue 3中,v-model的默认prop名称是modelValue,默认事件名称是update:modelValue。
<!-- CustomInput.vue -->
<template>
<div class="custom-input">
<label v-if="label">{{ label }}</label>
<input
:value="modelValue"
@input="handleInput"
:placeholder="placeholder"
:type="type"
class="input-field"
>
<span v-if="error" class="error-text">{{ error }}</span>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: [String, Number],
default: ''
},
label: String,
placeholder: String,
type: {
type: String,
default: 'text'
},
error: String
})
const emit = defineEmits(['update:modelValue'])
function handleInput(event) {
emit('update:modelValue', event.target.value)
}
</script>
<style scoped>
.custom-input {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.input-field {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.input-field:focus {
outline: none;
border-color: #42b883;
}
.error-text {
color: #f44336;
font-size: 14px;
margin-top: 5px;
}
</style>
<!-- App.vue -->
<template>
<div class="app">
<h1>自定义输入框组件</h1>
<CustomInput
v-model="username"
label="用户名"
placeholder="请输入用户名"
:error="usernameError"
/>
<CustomInput
v-model="email"
label="邮箱"
placeholder="请输入邮箱地址"
type="email"
:error="emailError"
/>
<CustomInput
v-model="password"
label="密码"
placeholder="请输入密码"
type="password"
:error="passwordError"
/>
<div class="preview">
<h3>预览数据</h3>
<p>用户名: {{ username }}</p>
<p>邮箱: {{ email }}</p>
<p>密码: {{ password }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import CustomInput from './components/CustomInput.vue'
const username = ref('')
const email = ref('')
const password = ref('')
const usernameError = computed(() => {
if (!username.value) return '用户名不能为空'
if (username.value.length < 3) return '用户名至少3个字符'
return ''
})
const emailError = computed(() => {
if (!email.value) return '邮箱不能为空'
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email.value)) return '请输入有效的邮箱地址'
return ''
})
const passwordError = computed(() => {
if (!password.value) return '密码不能为空'
if (password.value.length < 6) return '密码至少6个字符'
return ''
})
</script>
<style>
.app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.preview {
margin-top: 30px;
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
}
h1 {
text-align: center;
color: #333;
}
</style>
v-model:title和v-model:content| 注册方式 | 作用域 | 使用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 全局注册 | 整个应用 | 常用基础组件 | 随处可用,无需重复导入 | 增加打包体积,无法tree-shaking |
| 局部注册 | 当前组件 | 特定功能组件 | 按需加载,tree-shaking友好 | 需要手动导入 |
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './components/MyButton.vue'
import MyInput from './components/MyInput.vue'
import MyModal from './components/MyModal.vue'
const app = createApp(App)
// 全局注册常用组件
app.component('MyButton', MyButton)
app.component('MyInput', MyInput)
app.component('MyModal', MyModal)
// 也可以批量注册
const components = {
MyButton,
MyInput,
MyModal
}
Object.entries(components).forEach(([name, component]) => {
app.component(name, component)
})
app.mount('#app')
<!-- App.vue -->
<template>
<div>
<Header />
<MainContent />
<Sidebar />
<Footer />
</div>
</template>
<script setup>
// 局部注册 - Composition API
import Header from './components/Header.vue'
import MainContent from './components/MainContent.vue'
import Sidebar from './components/Sidebar.vue'
import Footer from './components/Footer.vue'
// 导入后自动注册,可直接在模板中使用
</script>
<!-- Options API写法 -->
<script>
import Header from './components/Header.vue'
import MainContent from './components/MainContent.vue'
import Sidebar from './components/Sidebar.vue'
import Footer from './components/Footer.vue'
export default {
name: 'App',
components: {
Header,
MainContent,
Sidebar,
Footer
}
}
</script>
创建一个完整的用户管理系统,包含用户列表展示、用户卡片、搜索过滤等功能,展示组件化开发的实际应用。
<!-- UserCard.vue -->
<template>
<div class="user-card" :class="{ featured: isFeatured }">
<div class="avatar-section">
<img :src="avatar" :alt="name" class="avatar">
<span v-if="isOnline" class="online-indicator"></span>
</div>
<div class="info-section">
<h3 class="name">{{ name }}</h3>
<p class="bio">{{ bio }}</p>
<div class="meta">
<span class="role" :class="role">{{ roleText }}</span>
<span class="join-date">加入时间: {{ joinDate }}</span>
</div>
</div>
<div class="actions-section">
<button
@click="handleFollow"
class="follow-btn"
:class="{ following: isFollowing }"
>
{{ isFollowing ? '已关注' : '关注' }}
</button>
<button
@click="handleMessage"
class="message-btn"
>
发消息
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
avatar: {
type: String,
required: true
},
bio: {
type: String,
default: ''
},
role: {
type: String,
default: 'user',
validator: (value) => ['user', 'admin', 'moderator'].includes(value)
},
joinDate: {
type: String,
default: '未知'
},
isOnline: {
type: Boolean,
default: false
},
isFeatured: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['follow', 'unfollow', 'message'])
const isFollowing = ref(false)
const roleText = computed(() => {
const roleMap = {
user: '普通用户',
admin: '管理员',
moderator: '版主'
}
return roleMap[props.role] || '用户'
})
function handleFollow() {
isFollowing.value = !isFollowing.value
emit(isFollowing.value ? 'follow' : 'unfollow', {
name: props.name,
timestamp: Date.now()
})
}
function handleMessage() {
emit('message', {
to: props.name,
timestamp: Date.now()
})
}
</script>
<style scoped>
.user-card {
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
transition: all 0.3s ease;
background: white;
}
.user-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.user-card.featured {
border-color: #42b883;
background: linear-gradient(135deg, #f8fff8 0%, #e8f5e8 100%);
}
.avatar-section {
position: relative;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.online-indicator {
position: absolute;
bottom: 5px;
right: 5px;
width: 12px;
height: 12px;
background: #4caf50;
border-radius: 50%;
border: 2px solid white;
}
.info-section {
flex: 1;
}
.name {
margin: 0 0 8px 0;
color: #333;
font-size: 18px;
}
.bio {
margin: 0 0 10px 0;
color: #666;
line-height: 1.4;
}
.meta {
display: flex;
gap: 10px;
align-items: center;
}
.role {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.role.user {
background: #e3f2fd;
color: #1976d2;
}
.role.admin {
background: #ffebee;
color: #d32f2f;
}
.role.moderator {
background: #f3e5f5;
color: #7b1fa2;
}
.join-date {
font-size: 12px;
color: #999;
}
.actions-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.follow-btn, .message-btn {
padding: 8px 16px;
border: 1px solid #42b883;
border-radius: 6px;
background: white;
color: #42b883;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.follow-btn.following {
background: #42b883;
color: white;
}
.follow-btn:hover {
background: #42b883;
color: white;
}
.message-btn {
border-color: #666;
color: #666;
}
.message-btn:hover {
background: #666;
color: white;
}
</style>
<!-- UserList.vue -->
<template>
<div class="user-list">
<div class="filters">
<input
v-model="searchQuery"
placeholder="搜索用户..."
class="search-input"
>
<select v-model="roleFilter" class="role-select">
<option value="">所有角色</option>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
<option value="moderator">版主</option>
</select>
<label class="online-filter">
<input type="checkbox" v-model="onlineOnly">
仅显示在线用户
</label>
</div>
<div class="users-grid">
<UserCard
v-for="user in filteredUsers"
:key="user.id"
v-bind="user"
@follow="handleFollow"
@unfollow="handleUnfollow"
@message="handleMessage"
/>
</div>
<div v-if="filteredUsers.length === 0" class="empty-state">
没有找到匹配的用户
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import UserCard from './UserCard.vue'
const props = defineProps({
users: {
type: Array,
required: true,
default: () => []
}
})
const emit = defineEmits(['userAction'])
const searchQuery = ref('')
const roleFilter = ref('')
const onlineOnly = ref(false)
const filteredUsers = computed(() => {
return props.users.filter(user => {
const matchesSearch = user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.bio.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesRole = !roleFilter.value || user.role === roleFilter.value
const matchesOnline = !onlineOnly.value || user.isOnline
return matchesSearch && matchesRole && matchesOnline
})
})
function handleFollow(payload) {
emit('userAction', { type: 'follow', ...payload })
}
function handleUnfollow(payload) {
emit('userAction', { type: 'unfollow', ...payload })
}
function handleMessage(payload) {
emit('userAction', { type: 'message', ...payload })
}
</script>
<style scoped>
.user-list {
max-width: 800px;
margin: 0 auto;
}
.filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
align-items: center;
flex-wrap: wrap;
}
.search-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
flex: 1;
min-width: 200px;
}
.role-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
}
.online-filter {
display: flex;
align-items: center;
gap: 5px;
font-size: 14px;
}
.users-grid {
display: grid;
gap: 20px;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
font-size: 16px;
}
</style>
创建一个按钮组件,支持以下功能:
创建一个模态框组件,包含以下功能:
创建一组表单组件: