组件的组织

实际应用中,组件不是孤立存在的。它们相互嵌套,形成组件树。理解组件的组织方式,对于构建大型应用至关重要。

组件树

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 提供了多种通信方式。

父传子:Props

<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>

子传父:Events

<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')
  }
})

跨层级:provide/inject

祖先组件向所有子孙组件注入依赖:

// 祖先组件提供数据
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

复杂应用使用 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>

组件通信优先级

  1. 父子通信:props / events
  2. 兄弟通信:事件总线 / Vuex
  3. 跨层级:provide/inject
  4. 全局状态:Vuex

小结

组件组织要点:

  1. 理解组件树:父子关系、组件层级
  2. 选择通信方式:props、events、事件总线、Vuex
  3. 善用插槽:提高组件灵活性
  4. 合理组织目录:按功能或模块划分

掌握组件组织,你就能构建出结构清晰、易于维护的大型应用。