正文
1. 组件基础
组件是 Vue 的核心功能,允许我们将 UI 拆分为独立可复用的代码片段。
1.1 组件注册
// 全局组件注册
const app = Vue.createApp({})
// 全局注册组件
app.component('my-component', {
// 组件选项
template: '<div>全局组件</div>',
data() {
return {
count: 0
}
}
})
// 局部注册组件
const ComponentA = {
template: '<div>局部组件 A</div>'
}
const ComponentB = {
template: '<div>局部组件 B</div>'
}
const app = Vue.createApp({
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
1.2 组件基本用法
<div id="app">
<!-- 使用组件 -->
<button-counter></button-counter>
<!-- 可以重复使用组件,每个实例都是独立的 -->
<button-counter></button-counter>
<button-counter></button-counter>
</div>
<script>
const app = Vue.createApp({})
app.component('button-counter', {
// 组件的数据必须是函数,以确保每个实例都有独立的数据副本
data() {
return {
count: 0
}
},
template: `
<button @click="count++">
你点击了我 {{ count }} 次
</button>
`
})
app.mount('#app')
</script>
2. 组件通信
2.1 Props 向下传递数据
<div id="app">
<!-- 传递静态 prop -->
<blog-post title="Vue 组件入门"></blog-post>
<!-- 动态绑定 prop -->
<blog-post :title="post.title"></blog-post>
<blog-post :title="post.title" :author="post.author" :publish-date="post.publishDate"></blog-post>
<!-- 传递对象的所有属性 -->
<blog-post v-bind="post"></blog-post>
</div>
<script>
const app = Vue.createApp({
data() {
return {
post: {
id: 1,
title: 'Vue 组件化开发指南',
author: '张三',
publishDate: '2023-05-15'
}
}
}
})
app.component('blog-post', {
// 声明 props
props: {
// 基础类型检查
title: String,
// 多种类型
author: [String, Object],
// 必填项
publishDate: {
type: String,
required: true
},
// 带默认值
comments: {
type: Array,
default: () => []
},
// 自定义验证函数
likes: {
validator(value) {
return value >= 0
}
}
},
template: `
<div class="blog-post">
<h3>{{ title }}</h3>
<p>作者: {{ author }}</p>
<p>发布日期: {{ publishDate }}</p>
</div>
`
})
app.mount('#app')
</script>
2.2 自定义事件向上传递数据
<div id="app">
<!-- 监听自定义事件 -->
<blog-post
:title="post.title"
@enlarge-text="fontSizeIncrease"
></blog-post>
<p :style="{ fontSize: postFontSize + 'px' }">
文章内容示例,当前字体大小: {{ postFontSize }}px
</p>
</div>
<script>
const app = Vue.createApp({
data() {
return {
post: {
title: 'Vue 组件通信'
},
postFontSize: 16
}
},
methods: {
fontSizeIncrease(enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
})
app.component('blog-post', {
props: ['title'],
emits: ['enlarge-text'], // 声明组件发出的事件
template: `
<div class="blog-post">
<h3>{{ title }}</h3>
<button @click="$emit('enlarge-text', 2)">
放大文字
</button>
</div>
`
})
app.mount('#app')
</script>
2.3 插槽分发内容
<div id="app">
<!-- 使用插槽 -->
<alert-box>
发生了一个错误,请检查您的输入。
</alert-box>
<!-- 具名插槽 -->
<base-layout>
<template v-slot:header>
<h1>页面标题</h1>
</template>
<template v-slot:default>
<p>页面主体内容</p>
</template>
<template v-slot:footer>
<p>页面底部版权信息</p>
</template>
</base-layout>
<!-- 作用域插槽 -->
<user-list :users="users">
<template v-slot:user="{ user, index }">
<div class="user-item">
<span>{{ index + 1 }}. {{ user.name }}</span>
<span v-if="user.online" class="online">在线</span>
</div>
</template>
</user-list>
</div>
<script>
const app = Vue.createApp({
data() {
return {
users: [
{ id: 1, name: '张三', online: true },
{ id: 2, name: '李四', online: false },
{ id: 3, name: '王五', online: true }
]
}
}
})
// 基本插槽
app.component('alert-box', {
template: `
<div class="alert-box">
<strong>提示!</strong>
<slot></slot>
</div>
`
})
// 具名插槽
app.component('base-layout', {
template: `
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`
})
// 作用域插槽
app.component('user-list', {
props: ['users'],
template: `
<ul>
<li v-for="(user, index) in users" :key="user.id">
<slot name="user" :user="user" :index="index"></slot>
</li>
</ul>
`
})
app.mount('#app')
</script>
3. 组件注册与组织
3.1 组件命名约定
// 组件命名约定
// 1. kebab-case (短横线分隔命名)
app.component('my-component', { /* ... */ })
// 使用: <my-component></my-component>
// 2. PascalCase (大驼峰命名)
app.component('MyComponent', { /* ... */ })
// 使用: <MyComponent></MyComponent> 或 <my-component></my-component>
3.2 模块化组件系统
// 文件: components/ButtonCounter.js
export default {
name: 'ButtonCounter',
data() {
return {
count: 0
}
},
template: `
<button @click="count++">
你点击了我 {{ count }} 次
</button>
`
}
// 文件: components/BlogPost.js
export default {
name: 'BlogPost',
props: ['title'],
template: `
<div class="blog-post">
<h3>{{ title }}</h3>
<slot></slot>
</div>
`
}
// 文件: main.js
import { createApp } from 'vue'
import ButtonCounter from './components/ButtonCounter.js'
import BlogPost from './components/BlogPost.js'
const app = createApp({
components: {
ButtonCounter,
BlogPost
}
})
app.mount('#app')
3.3 动态组件
<div id="app">
<!-- 使用 component 元素和 is 特性实现动态组件 -->
<button
v-for="tab in tabs"
:key="tab"
@click="currentTab = tab"
:class="{ active: currentTab === tab }"
>
{{ tab }}
</button>
<component :is="currentTabComponent" class="tab"></component>
<!-- 使用 keep-alive 保持组件状态 -->
<keep-alive>
<component :is="currentTabComponent" class="tab"></component>
</keep-alive>
</div>
<script>
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
app.component('tab-home', {
template: `<div>首页内容</div>`
})
app.component('tab-posts', {
data() {
return {
posts: [
{ id: 1, title: '文章一' },
{ id: 2, title: '文章二' }
]
}
},
template: `
<div>
<h3>文章列表</h3>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
`
})
app.component('tab-archive', {
template: `<div>归档内容</div>`
})
app.mount('#app')
</script>
4. 组件高级模式
4.1 组件递归
<div id="app">
<tree-folder :folder="rootFolder"></tree-folder>
</div>
<script>
const app = Vue.createApp({
data() {
return {
rootFolder: {
name: '项目文件',
children: [
{
name: 'src',
children: [
{ name: 'components', children: [
{ name: 'App.vue' },
{ name: 'Button.vue' }
] },
{ name: 'main.js' }
]
},
{ name: 'public', children: [
{ name: 'index.html' },
{ name: 'favicon.ico' }
] },
{ name: 'package.json' }
]
}
}
}
})
app.component('tree-folder', {
props: ['folder'],
template: `
<div class="folder">
<div class="folder-name" @click="toggle">
<span class="icon">{{ isOpen ? '📂' : '📁' }}</span>
{{ folder.name }}
</div>
<div class="folder-contents" v-if="isOpen && folder.children">
<tree-folder
v-for="child in folder.children"
:key="child.name"
:folder="child"
v-if="child.children"
></tree-folder>
<div class="file" v-else>
<span class="icon">📄</span>
{{ child.name }}
</div>
</div>
</div>
`,
data() {
return {
isOpen: false
}
},
methods: {
toggle() {
this.isOpen = !this.isOpen
}
}
})
app.mount('#app')
</script>
4.2 依赖注入
<div id="app">
<parent-component></parent-component>
</div>
<script>
const app = Vue.createApp({})
app.component('parent-component', {
// 提供数据给后代组件
provide() {
return {
theme: 'dark',
user: this.user
}
},
data() {
return {
user: { name: '张三', role: 'admin' }
}
},
template: `
<div class="parent">
<h2>父组件</h2>
<child-component></child-component>
</div>
`
})
app.component('child-component', {
template: `
<div class="child">
<h3>子组件</h3>
<grand-child-component></grand-child-component>
</div>
`
})
app.component('grand-child-component', {
// 注入祖先组件提供的数据
inject: ['theme', 'user'],
template: `
<div class="grand-child" :class="theme">
<h4>孙组件</h4>
<p>主题: {{ theme }}</p>
<p>用户: {{ user.name }} ({{ user.role }})</p>
</div>
`
})
app.mount('#app')
</script>
4.3 异步组件
// 基本异步组件
const AsyncComponent = Vue.defineAsyncComponent(() => {
return new Promise((resolve) => {
// 模拟异步加载
setTimeout(() => {
resolve({
template: '<div>异步加载的组件</div>'
})
}, 1000)
})
})
// 带选项的异步组件
const AsyncComponentWithOptions = Vue.defineAsyncComponent({
loader: () => import('./components/HeavyComponent.js'),
loadingComponent: {
template: '<div>加载中...</div>'
},
errorComponent: {
template: '<div>加载失败!</div>'
},
delay: 200,
timeout: 3000
})
const app = Vue.createApp({
components: {
AsyncComponent,
AsyncComponentWithOptions
},
template: `
<div>
<h2>异步组件示例</h2>
<AsyncComponent />
<AsyncComponentWithOptions />
</div>
`
})
5. 组件通信进阶
5.1 Provide/Inject 响应式
<div id="app">
<theme-provider></theme-provider>
</div>
<script>
const { ref, provide, inject } = Vue
const app = Vue.createApp({})
app.component('theme-provider', {
setup() {
// 创建响应式数据
const theme = ref('light')
// 提供响应式数据
provide('theme', theme)
// 提供切换主题的方法
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('toggleTheme', toggleTheme)
return {
theme,
toggleTheme
}
},
template: `
<div :class="theme">
<h2>当前主题: {{ theme }}</h2>
<button @click="toggleTheme">切换主题</button>
<theme-consumer></theme-consumer>
</div>
`
})
app.component('theme-consumer', {
setup() {
// 注入响应式数据
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
return {
theme,
toggleTheme
}
},
template: `
<div class="consumer">
<h3>消费组件</h3>
<p>当前主题: {{ theme }}</p>
<button @click="toggleTheme">从子组件切换主题</button>
<deep-consumer></deep-consumer>
</div>
`
})
app.component('deep-consumer', {
setup() {
const theme = inject('theme')
return { theme }
},
template: `
<div class="deep-consumer">
<h4>深层消费组件</h4>
<p>当前主题: {{ theme }}</p>
</div>
`
})
app.mount('#app')
</script>
5.2 事件总线
// Vue 3 中的事件总线实现
import { createApp } from 'vue'
import mitt from 'mitt'
const app = createApp({})
// 创建事件总线
const emitter = mitt()
// 将事件总线添加到全局属性
app.config.globalProperties.$bus = emitter
// 组件 A
app.component('component-a', {
template: `
<div>
<h3>组件 A</h3>
<button @click="sendMessage">发送消息</button>
</div>
`,
methods: {
sendMessage() {
// 发送事件
this.$bus.emit('message', {
text: '来自组件A的消息',
time: new Date()
})
}
}
})
// 组件 B
app.component('component-b', {
template: `
<div>
<h3>组件 B</h3>
<p v-if="message">收到消息: {{ message.text }} ({{ formatTime }})</p>
</div>
`,
data() {
return {
message: null
}
},
computed: {
formatTime() {
return this.message ? this.message.time.toLocaleTimeString() : ''
}
},
mounted() {
// 监听事件
this.$bus.on('message', (data) => {
this.message = data
})
},
beforeUnmount() {
// 移除事件监听
this.$bus.off('message')
}
})
app.mount('#app')
6. 组件复用技术
6.1 混入 (Mixins)
// 定义混入对象
const shareMixin = {
data() {
return {
sharedData: '共享数据'
}
},
created() {
console.log('混入对象的钩子被调用')
},
methods: {
sharedMethod() {
console.log('这是一个共享方法')
}
}
}
// 使用混入
const app = Vue.createApp({
mixins: [shareMixin],
created() {
console.log('组件钩子被调用')
console.log('共享数据:', this.sharedData)
}
})
// 全局混入
app.mixin({
created() {
console.log('全局混入的钩子被调用')
}
})
// 组件中使用混入
app.component('custom-component', {
mixins: [shareMixin],
template: `
<div>
<h3>使用混入的组件</h3>
<p>{{ sharedData }}</p>
<button @click="sharedMethod">调用共享方法</button>
</div>
`
})
6.2 组合式 API (Composition API)
<div id="app">
<user-profile></user-profile>
</div>
<script>
const { ref, reactive, computed, onMounted, watch } = Vue
// 可复用的组合函数
function useUserData() {
const user = reactive({
name: '',
email: '',
role: ''
})
const isAdmin = computed(() => user.role === 'admin')
const fetchUserData = async (id) => {
// 模拟API调用
return new Promise((resolve) => {
setTimeout(() => {
Object.assign(user, {
name: '张三',
email: 'zhangsan@example.com',
role: 'admin'
})
resolve(user)
}, 1000)
})
}
return {
user,
isAdmin,
fetchUserData
}
}
// 另一个可复用的组合函数
function useWindowSize() {
const windowSize = reactive({
width: window.innerWidth,
height: window.innerHeight
})
const updateSize = () => {
windowSize.width = window.innerWidth
windowSize.height = window.innerHeight
}
onMounted(() => {
window.addEventListener('resize', updateSize)
})
// 在组件卸载时移除事件监听
onUnmounted(() => {
window.removeEventListener('resize', updateSize)
})
return windowSize
}
const app = Vue.createApp({})
app.component('user-profile', {
setup() {
// 使用组合函数
const { user, isAdmin, fetchUserData } = useUserData()
const windowSize = useWindowSize()
// 组件特有的状态
const loading = ref(true)
// 生命周期钩子
onMounted(async () => {
await fetchUserData()
loading.value = false
})
// 监听属性变化
watch(() => user.role, (newRole) => {
console.log(`用户角色变更为: ${newRole}`)
})
// 返回模板需要的内容
return {
user,
isAdmin,
loading,
windowSize
}
},
template: `
<div class="user-profile">
<div v-if="loading">加载中...</div>
<div v-else>
<h2>用户资料</h2>
<p>姓名: {{ user.name }}</p>
<p>邮箱: {{ user.email }}</p>
<p>角色: {{ user.role }}</p>
<p v-if="isAdmin">管理员特权已启用</p>
<p>窗口尺寸: {{ windowSize.width }} x {{ windowSize.height }}</p>
</div>
</div>
`
})
app.mount('#app')
</script>
7. 组件性能优化
7.1 组件缓存
<div id="app">
<button
v-for="tab in tabs"
:key="tab.id"
@click="currentTab = tab.id"
:class="{ active: currentTab === tab.id }"
>
{{ tab.name }}
</button>
<!-- 使用 keep-alive 缓存组件状态 -->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
<!-- 带有包含/排除规则的 keep-alive -->
<keep-alive :include="['a', 'b']" :exclude="['c']" :max="10">
<component :is="currentTabComponent"></component>
</keep-alive>
</div>
<script>
const app = Vue.createApp({
data() {
return {
tabs: [
{ id: 'home', name: '首页' },
{ id: 'posts', name: '文章' },
{ id: 'profile', name: '个人资料' }
],
currentTab: 'home'
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab
}
}
})
app.component('tab-home', {
template: `<div>首页内容</div>`,
// 当组件在 keep-alive 内被切换时调用
activated() {
console.log('Home 组件被激活')
},
// 当组件在 keep-alive 内被切换出去时调用
deactivated() {
console.log('Home 组件被停用')
}
})
app.component('tab-posts', {
data() {
return {
searchQuery: '',
posts: []
}
},
template: `
<div>
<input v-model="searchQuery" placeholder="搜索文章...">
<p>当前有 {{ posts.length }} 篇文章</p>
</div>
`,
// 模拟数据加载
created() {
console.log('Posts 组件被创建,加载数据')
// 这里的数据加载只会执行一次,因为组件被缓存
setTimeout(() => {
this.posts = Array(20).fill().map((_, i) => ({ id: i, title: `文章 ${i}` }))
}, 1000)
},
activated() {
console.log('Posts 组件被激活')
}
})
app.component('tab-profile', {
template: `<div>个人资料内容</div>`
})
app.mount('#app')
</script>
7.2 动态组件与异步组件结合
import { defineAsyncComponent } from 'vue'
const app = Vue.createApp({
data() {
return {
currentTab: 'Home'
}
},
computed: {
currentTabComponent() {
return `tab-${this.currentTab.toLowerCase()}`
}
}
})
// 注册异步组件
app.component('tab-home', {
template: `<div>首页内容 (立即加载)</div>`
})
// 较重的组件使用异步加载
app.component('tab-posts', defineAsyncComponent({
loader: () => {
return new Promise((resolve) => {
// 模拟网络请求延迟
setTimeout(() => {
resolve({
template: `
<div>
<h3>文章列表</h3>
<ul>
<li v-for="i in 100" :key="i">文章 {{ i }}</li>
</ul>
</div>
`,
created() {
console.log('Posts 组件被创建')
}
})
}, 1000)
})
},
loadingComponent: {
template: `<div class="loading">加载中...</div>`
},
errorComponent: {
template: `<div class="error">加载失败!</div>`
},
delay: 200,
timeout: 5000
}))
app.component('tab-settings', defineAsyncComponent(() => {
return import('./components/Settings.js')
}))
// 在模板中使用
// <keep-alive>
// <component :is="currentTabComponent"></component>
// </keep-alive>
7.3 函数式组件
// Vue 3 中的函数式组件
const FunctionalButton = (props, { slots, emit, attrs }) => {
return Vue.h('button', {
...attrs,
class: ['btn', props.type ? `btn-${props.type}` : ''],
onClick: () => emit('click')
}, slots.default ? slots.default() : '按钮')
}
FunctionalButton.props = {
type: String
}
// 使用函数式组件
const app = Vue.createApp({
components: {
FunctionalButton
},
template: `
<div>
<functional-button @click="handleClick">
点击我
</functional-button>
<functional-button type="primary" @click="handleClick">
主要按钮
</functional-button>
</div>
`,
methods: {
handleClick() {
console.log('按钮被点击')
}
}
})
8. 组件设计模式
8.1 容器/展示组件模式
// 展示组件 - 只负责渲染,不含业务逻辑
app.component('user-card', {
props: {
user: {
type: Object,
required: true
}
},
template: `
<div class="user-card">
<img :src="user.avatar" :alt="user.name">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="$emit('edit')">编辑</button>
</div>
`
})
// 容器组件 - 负责数据获取和业务逻辑
app.component('user-container', {
data() {
return {
user: null,
loading: true,
error: null
}
},
created() {
this.fetchUser()
},
methods: {
async fetchUser() {
this.loading = true
try {
8.2 渲染函数与JSX
// 使用渲染函数
app.component('anchored-heading', {
props: {
level: {
type: Number,
required: true
}
},
render() {
// 创建标题元素
return Vue.h(
'h' + this.level, // 标签名
{}, // props/attributes
this.$slots.default() // 子节点
)
}
})
// 使用JSX (需要配置JSX插件)
app.component('jsx-example', {
data() {
return {
items: ['苹果', '香蕉', '橙子']
}
},
render() {
return (
<div>
<h2>水果列表</h2>
<ul>
{this.items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
{this.items.length === 0 ? (
<p>没有水果</p>
) : (
<p>共有 {this.items.length} 种水果</p>
)}
</div>
)
}
})
8.3 高阶组件模式
// 高阶组件工厂函数
function withLoading(Component) {
return {
props: {
loading: {
type: Boolean,
default: false
},
...Component.props
},
render(ctx) {
return ctx.loading
? Vue.h('div', { class: 'loading-indicator' }, '加载中...')
: Vue.h(Component, ctx.$props, ctx.$slots)
}
}
}
// 基础组件
const UserList = {
props: {
users: Array
},
template: `
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
`
}
// 创建增强版组件
const UserListWithLoading = withLoading(UserList)
// 使用增强版组件
const app = Vue.createApp({
components: {
UserListWithLoading
},
data() {
return {
users: [],
loading: true
}
},
created() {
// 模拟数据加载
setTimeout(() => {
this.users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
]
this.loading = false
}, 2000)
},
template: `
<div>
<h2>用户列表</h2>
<user-list-with-loading
:loading="loading"
:users="users"
></user-list-with-loading>
</div>
`
})
结语
感谢您的阅读!期待您的一键三连!欢迎指正!