实际应用中,组件不是孤立存在的。它们相互嵌套,形成组件树。理解组件的组织方式,对于构建大型应用至关重要。
Vue 应用由嵌套的组件组成,形成树形结构:
App(根组件)
├── Header
│ ├── Logo
│ └── Navigation
│ └── NavItem
├── Main
│ ├── Sidebar
│ │ └── Menu
│ └── Content
│ ├── Article
│ │ └── Comment
│ └── Pagination
└── Footer
└── Links
组件之间存在父子关系:
// 父组件
Vue.component('parent-component', {
template: `
<div>
<h2>父组件</h2>
<child-component></child-component>
</div>
`,
components: {
'child-component': {
template: '<p>子组件</p>'
}
}
})
通过 this.$children 访问子组件实例:
Vue.component('parent', {
template: `
<div>
<child></child>
<child></child>
<button @click="getChildren">获取子组件</button>
</div>
`,
methods: {
getChildren() {
console.log(this.$children)
// 返回所有子组件实例数组
}
}
})
通过 this.$parent 访问父组件实例:
Vue.component('child', {
template: '<button @click="getParent">访问父组件</button>',
methods: {
getParent() {
console.log(this.$parent)
// 返回父组件实例
}
}
})
通过 this.$root 访问根 Vue 实例:
Vue.component('deep-child', {
template: '<button @click="getRoot">访问根组件</button>',
methods: {
getRoot() {
console.log(this.$root)
// 返回根 Vue 实例
}
}
})
避免直接访问组件实例
直接访问父组件或子组件实例会破坏组件的封装性,增加耦合。应该优先使用 props 和事件进行通信。
组件树中,不同层级的组件需要通信。Vue 提供了多种通信方式。
<div id="app">
<user-card :user="currentUser"></user-card>
</div>
<script>
Vue.component('user-card', {
props: ['user'],
template: `
<div class="card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
`
})
new Vue({
el: '#app',
data: {
currentUser: {
name: '张三',
email: 'zhangsan@example.com'
}
}
})
</script>
<div id="app">
<counter @increment="handleIncrement"></counter>
<p>总数:{{ total }}</p>
</div>
<script>
Vue.component('counter', {
data() {
return {
count: 0
}
},
template: '<button @click="increment">{{ count }}</button>',
methods: {
increment() {
this.count++
this.$emit('increment', this.count)
}
}
})
new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleIncrement(count) {
this.total++
}
}
})
</script>
没有直接关系的组件,可以使用事件总线:
// 创建事件总线
var bus = new Vue()
// 组件 A 发送事件
Vue.component('component-a', {
template: '<button @click="send">发送消息</button>',
methods: {
send() {
bus.$emit('message', '来自 A 的消息')
}
}
})
// 组件 B 接收事件
Vue.component('component-b', {
template: '<p>{{ message }}</p>',
data() {
return {
message: ''
}
},
created() {
bus.$on('message', (msg) => {
this.message = msg
})
},
beforeDestroy() {
bus.$off('message')
}
})
祖先组件向所有子孙组件注入依赖:
// 祖先组件提供数据
Vue.component('ancestor', {
provide: {
theme: 'dark',
user: {
name: '张三'
}
},
template: '<div><descendant></descendant></div>'
})
// 后代组件注入数据
Vue.component('descendant', {
inject: ['theme', 'user'],
template: `
<div>
<p>主题:{{ theme }}</p>
<p>用户:{{ user.name }}</p>
</div>
`
})
provide/inject 特点
复杂应用使用 Vuex 管理状态:
// store.js
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment({ commit }) {
commit('increment')
}
},
getters: {
doubleCount: state => state.count * 2
}
})
// 组件中使用
Vue.component('my-component', {
computed: {
count() {
return this.$store.state.count
},
doubleCount() {
return this.$store.getters.doubleCount
}
},
methods: {
increment() {
this.$store.dispatch('increment')
}
}
})
插槽让组件内容更灵活,是组件组织的重要方式。
Vue.component('alert-box', {
template: `
<div class="alert">
<strong>提示:</strong>
<slot></slot>
</div>
`
})
// 使用
<alert-box>这是一条消息</alert-box>
Vue.component('layout', {
template: `
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`
})
// 使用
<layout>
<template v-slot:header>
<h1>页面标题</h1>
</template>
<p>主要内容</p>
<template v-slot:footer>
<p>页脚信息</p>
</template>
</layout>
Vue.component('user-list', {
props: ['users'],
template: `
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user">
{{ user.name }}
</slot>
</li>
</ul>
`
})
// 使用
<user-list :users="users">
<template v-slot="{ user }">
<span class="name">{{ user.name }}</span>
<span class="email">{{ user.email }}</span>
</template>
</user-list>
components/
├── base/ # 基础组件
│ ├── Button.vue
│ ├── Input.vue
│ └── Icon.vue
├── layout/ # 布局组件
│ ├── Header.vue
│ ├── Footer.vue
│ └── Sidebar.vue
├── common/ # 通用组件
│ ├── Modal.vue
│ ├── Toast.vue
│ └── Loading.vue
└── business/ # 业务组件
├── UserCard.vue
├── ProductList.vue
└── OrderItem.vue
components/
├── user/
│ ├── UserCard.vue
│ ├── UserList.vue
│ └── UserForm.vue
├── product/
│ ├── ProductCard.vue
│ ├── ProductList.vue
│ └── ProductFilter.vue
└── order/
├── OrderItem.vue
├── OrderList.vue
└── OrderDetail.vue
// 基础组件:Base 前缀
BaseButton.vue
BaseInput.vue
// 单例组件:The 前缀
TheHeader.vue
TheFooter.vue
// 业务组件:功能前缀
UserCard.vue
ProductList.vue
<div id="app">
<app-layout>
<template v-slot:header>
<app-header>
<app-logo></app-logo>
<app-nav>
<nav-item to="/">首页</nav-item>
<nav-item to="/about">关于</nav-item>
</app-nav>
</app-header>
</template>
<app-main>
<app-sidebar>
<sidebar-menu :items="menuItems"></sidebar-menu>
</app-sidebar>
<app-content>
<article-list :articles="articles">
<template v-slot:item="{ article }">
<article-card :article="article">
<template v-slot:footer>
<article-meta :article="article"></article-meta>
</template>
</article-card>
</template>
</article-list>
</app-content>
</app-main>
<template v-slot:footer>
<app-footer>
<footer-links :links="links"></footer-links>
</app-footer>
</template>
</app-layout>
</div>
Vue.component('tree-node', {
name: 'TreeNode',
props: {
node: Object
},
template: `
<div class="tree-node">
<div class="node-content">
<span @click="toggle">{{ node.name }}</span>
</div>
<div v-if="expanded" class="node-children">
<tree-node
v-for="child in node.children"
:key="child.id"
:node="child"
></tree-node>
</div>
</div>
`,
data() {
return {
expanded: false
}
},
methods: {
toggle() {
this.expanded = !this.expanded
}
}
})
<div id="app">
<button
v-for="tab in tabs"
:key="tab.name"
@click="currentTab = tab.name"
>
{{ tab.label }}
</button>
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
</div>
<script>
Vue.component('tab-home', {
template: '<div>首页内容</div>'
})
Vue.component('tab-posts', {
template: '<div>文章列表</div>'
})
Vue.component('tab-archive', {
template: '<div>归档内容</div>'
})
new Vue({
el: '#app',
data: {
currentTab: 'home',
tabs: [
{ name: 'home', label: '首页' },
{ name: 'posts', label: '文章' },
{ name: 'archive', label: '归档' }
]
},
computed: {
currentComponent() {
return 'tab-' + this.currentTab
}
}
})
</script>
避免过深的组件嵌套
// ❌ 过深嵌套
<app>
<layout>
<main>
<section>
<article>
<content>
<paragraph>
<text>内容</text>
</paragraph>
</content>
</article>
</section>
</main>
</layout>
</app>
// ✅ 合理嵌套
<app>
<article-content>
<paragraph>内容</paragraph>
</article-content>
</app>
组件通信优先级
组件组织要点:
掌握组件组织,你就能构建出结构清晰、易于维护的大型应用。