1. 組件註冊
<!-- UserCard.vue -->
<script setup lang="ts">
interface Props {
name: string
email: string
avatar?: string
role?: 'admin' | 'user' | 'editor'
}
const props = withDefaults(defineProps<Props>(), {
role: 'user',
})
</script>
<template>
<div class="user-card">
<img :src="props.avatar || '/default-avatar.png'" :alt="props.name" />
<h3>{{ props.name }}</h3>
<p>{{ props.email }}</p>
<span class="badge">{{ props.role }}</span>
</div>
</template>
<!-- Parent.vue — sử dụng component -->
<script setup lang="ts">
import UserCard from './UserCard.vue'
</script>
<template>
<UserCard name="Duy Tran" email="[email protected]" role="admin" />
</template>
2. v-model & 自訂事件
<!-- SearchInput.vue -->
<script setup lang="ts">
const model = defineModel<string>()
// Vue 3.4+ — defineModel replaces props + emit pattern
const emit = defineEmits<{
(e: 'search', query: string): void
}>()
function handleSearch() {
if (model.value) {
emit('search', model.value)
}
}
</script>
<template>
<div class="search">
<input v-model="model" placeholder="Tìm kiếm..." @keydown.enter="handleSearch" />
<button @click="handleSearch">🔍</button>
</div>
</template>
<!-- Parent -->
<script setup lang="ts">
import { ref } from 'vue'
const query = ref('')
</script>
<template>
<SearchInput v-model="query" @search="handleSearch" />
</template>
3. 老虎機
<!-- Card.vue -->
<template>
<div class="card">
<!-- Named slots -->
<div class="card-header">
<slot name="header">Default Header</slot>
</div>
<div class="card-body">
<slot>Default content</slot>
</div>
<div class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
<!-- Usage -->
<Card>
<template #header>
<h2>Tiêu đề</h2>
</template>
<p>Nội dung chính</p>
<template #footer>
<button>Submit</button>
</template>
</Card>
4. 作用域槽
<!-- DataTable.vue -->
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
columns: { key: keyof T; label: string }[]
}>()
</script>
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="String(col.key)">{{ col.label }}</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in items" :key="index">
<td v-for="col in columns" :key="String(col.key)">
<slot :name="String(col.key)" :value="item[col.key]" :item="item">
{{ item[col.key] }}
</slot>
</td>
<td>
<slot name="actions" :item="item" />
</td>
</tr>
</tbody>
</table>
</template>
5. 提供/注入
<!-- Parent — provide -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const theme = ref<'light' | 'dark'>('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('theme', { theme, toggleTheme })
</script>
<!-- Deep child — inject -->
<script setup lang="ts">
import { inject } from 'vue'
const { theme, toggleTheme } = inject('theme')!
</script>
<template>
<button @click="toggleTheme">
Current: {{ theme }}
</button>
</template>
6. 生命週期掛鉤
<script setup lang="ts">
import { onMounted, onUnmounted, onUpdated } from 'vue'
onMounted(() => {
console.log('Component mounted — DOM ready')
window.addEventListener('resize', handleResize)
})
onUpdated(() => {
console.log('Component updated')
})
onUnmounted(() => {
console.log('Component unmounted — cleanup')
window.removeEventListener('resize', handleResize)
})
</script>
下一篇: 指令、渲染和模板參考 — 自訂指令、渲染函數、傳送。