组件的定义

创建组件是 Vue 开发的基础技能。看似简单的 Vue.component() 调用,背后有很多值得注意的细节。理解这些细节,能帮你避免很多常见问题。

基本语法

使用 Vue.component() 定义全局组件:

Vue.component('组件名称', {
  // 组件选项
  template: '...',
  data: function() { return { ... } },
  methods: { ... }
})

第一个参数是组件名称,第二个参数是组件选项对象。

data 必须是函数

这是 Vue 组件最重要的规则之一:组件的 data 必须是一个函数

为什么必须是函数

如果 data 是对象,所有组件实例会共享同一个数据引用:

// ❌ 错误示例
Vue.component('counter', {
  data: {
    count: 0
  },
  template: '<button @click="count++">{{ count }}</button>'
})

// 结果:三个按钮共享同一个 count,点击任意一个都会影响其他

使用函数返回数据,每个组件实例都有独立的数据副本:

// ✅ 正确示例
Vue.component('counter', {
  data: function() {
    return {
      count: 0
    }
  },
  template: '<button @click="count++">{{ count }}</button>'
})

// 结果:每个按钮维护自己的 count

Vue 会报错

如果 data 不是函数,Vue 会在控制台警告:

[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.

data 函数的写法

// 标准写法
data: function() {
  return {
    message: 'Hello'
  }
}

// ES6 简写
data() {
  return {
    message: 'Hello'
  }
}

// 使用箭头函数(不推荐,this 指向有问题)
data: () => ({
  message: 'Hello'
})

模板定义方式

组件模板有多种定义方式,各有优缺点。

字符串模板

Vue.component('my-component', {
  template: '<div class="my-component">{{ message }}</div>',
  data() {
    return {
      message: 'Hello'
    }
  }
})

优点:简单直接 缺点:复杂模板难以维护,没有语法高亮

模板字符串

Vue.component('my-component', {
  template: `
    <div class="my-component">
      <h2>{{ title }}</h2>
      <p>{{ content }}</p>
      <button @click="handleClick">点击</button>
    </div>
  `,
  data() {
    return {
      title: '标题',
      content: '内容'
    }
  },
  methods: {
    handleClick() {
      console.log('clicked')
    }
  }
})

优点:支持多行,可读性好 缺点:仍然没有语法高亮和智能提示

x-template

<script type="text/x-template" id="my-component-template">
  <div class="my-component">
    <h2>{{ title }}</h2>
    <p>{{ content }}</p>
  </div>
</script>

<script>
Vue.component('my-component', {
  template: '#my-component-template',
  data() {
    return {
      title: '标题',
      content: '内容'
    }
  }
})
</script>

优点:模板分离,支持语法高亮 缺点:模板和组件定义分离,不利于维护

单文件组件

<template>
  <div class="my-component">
    <h2>{{ title }}</h2>
    <p>{{ content }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: '标题',
      content: '内容'
    }
  }
}
</script>

<style scoped>
.my-component {
  padding: 20px;
}
</style>

优点:模板、脚本、样式封装在一起,最佳实践 缺点:需要构建工具支持

组件选项

组件支持大部分 Vue 实例的选项:

Vue.component('my-component', {
  // 模板
  template: '...',
  
  // 数据
  data() {
    return { ... }
  },
  
  // 接收外部数据
  props: {
    title: String,
    count: {
      type: Number,
      default: 0
    }
  },
  
  // 计算属性
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  
  // 方法
  methods: {
    doSomething() { ... }
  },
  
  // 监听器
  watch: {
    count(newVal, oldVal) {
      console.log('count changed')
    }
  },
  
  // 生命周期钩子
  created() { ... },
  mounted() { ... },
  destroyed() { ... },
  
  // 局部指令
  directives: { ... },
  
  // 局部过滤器
  filters: { ... },
  
  // 局部组件
  components: { ... }
})

不支持的选项

组件不支持 el 选项,因为组件通过标签使用,不需要挂载点:

// ❌ 组件不支持 el
Vue.component('my-component', {
  el: '#app'  // 无效
})

实战示例

计数器组件

<div id="app">
  <counter></counter>
  <counter></counter>
  <counter></counter>
  <p>总点击次数:{{ total }}</p>
</div>

<script>
Vue.component('counter', {
  template: `
    <div>
      <button @click="increment">+1</button>
      <span>{{ count }}</span>
      <button @click="decrement">-1</button>
    </div>
  `,
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
      this.$emit('change', this.count)
    },
    decrement() {
      this.count--
      this.$emit('change', this.count)
    }
  }
})

new Vue({
  el: '#app',
  data: {
    total: 0
  },
  methods: {
    updateTotal(count) {
      // 这里需要更复杂的逻辑来计算总数
    }
  }
})
</script>

用户卡片组件

Vue.component('user-card', {
  template: `
    <div class="user-card">
      <img :src="user.avatar" :alt="user.name">
      <div class="info">
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
        <button @click="viewProfile">查看详情</button>
      </div>
    </div>
  `,
  props: {
    user: {
      type: Object,
      required: true
    }
  },
  methods: {
    viewProfile() {
      this.$emit('view', this.user.id)
    }
  }
})

标签页组件

Vue.component('tab-item', {
  template: `
    <div v-show="active">
      <slot></slot>
    </div>
  `,
  props: {
    title: String,
    active: Boolean
  }
})

Vue.component('tabs', {
  template: `
    <div class="tabs">
      <ul class="tab-header">
        <li 
          v-for="(tab, index) in tabs" 
          :key="index"
          :class="{ active: index === currentIndex }"
          @click="currentIndex = index"
        >
          {{ tab.title }}
        </li>
      </ul>
      <div class="tab-content">
        <slot></slot>
      </div>
    </div>
  `,
  data() {
    return {
      currentIndex: 0,
      tabs: []
    }
  },
  mounted() {
    this.tabs = this.$children
  }
})

注意事项

模板必须有根元素

组件模板必须有一个根元素包裹:

// ❌ 错误:多个根元素
template: `
  <h2>标题</h2>
  <p>内容</p>
`

// ✅ 正确:单一根元素
template: `
  <div>
    <h2>标题</h2>
    <p>内容</p>
  </div>
`

组件定义顺序

建议在创建 Vue 实例之前定义所有组件:

// ✅ 正确顺序
Vue.component('component-a', { ... })
Vue.component('component-b', { ... })

new Vue({ el: '#app' })

// ❌ 错误顺序:实例创建后再定义组件
new Vue({ el: '#app' })
Vue.component('component-c', { ... })  // 可能无法使用

小结

组件定义要点:

  1. data 必须是函数:确保每个实例有独立数据
  2. 模板单一根元素:模板必须有且只有一个根元素
  3. 选择合适的模板方式:单文件组件是最佳实践
  4. 命名规范:使用短横线分隔,语义化命名