Vue.js: 组件基础

最后更新:2026-08-26

Vue 组件是可复用的 Vue 实例——每个组件封装自己的模板、逻辑、样式。Vue 3 的核心架构就是"组件树":根组件(App.vue)→ 页面组件 → 业务组件 → 基础组件(Button/Input)。

掌握组件基础是写大型 Vue 应用的关键一步。本课帮你理解组件的本质、注册方式、命名规范,以及为什么 SFC(单文件组件)是 Vue 推荐的写法。

1. 你将学到


2. 一个 30 页电商后台的代码重复灾难

(1) 痛点:30 个商品卡片,复制粘贴 30 次

Alice 的电商后台需要在多个页面显示 30 个商品卡片。她最初的方案:

HTML
<!-- ❌ 翻车版:30 个商品卡片复制 30 次 -->
<!-- HomeView.vue 中 -->
<div class="product-card">
  <img src="iphone.jpg">
  <h3>iPhone 15 Pro</h3>
  <p>$999</p>
  <button>Add to Cart</button>
</div>
<div class="product-card">
  <img src="macbook.jpg">
  <h3>MacBook Pro</h3>
  <p>$2499</p>
  <button>Add to Cart</button>
</div>
<!-- ... 还有 28 个 ... -->

<!-- 同样的代码又出现在 ProductList.vue、SearchResults.vue、Cart.vue 中 -->

产品经理 Charlie 追加了 5 个新需求:

"Alice,我要把按钮文字从'Add to Cart'改成'Add to Bag'。另外卡片要显示库存状态。还需要做移动端响应式。"

Alice 不得不修改 30+ 个 HTML 文件 × 3 次 = 90 次编辑。这根本无法维护。

(2) Vue 组件解法:1 个 ProductCard.vue,30 处复用

VUE
<!-- components/ProductCard.vue - 1 个组件定义 -->
<template>
  <div class="product-card">
    <img :src="product.image">
    <h3>{{ product.name }}</h3>
    <p>${{ product.price }}</p>
    <button :disabled="product.stock === 0" @click="addToCart">
      {{ product.stock === 0 ? 'Out of Stock' : 'Add to Bag' }}
    </button>
  </div>
</template>

<script setup>
const props = defineProps({ product: Object })
const emit = defineEmits(['add-to-cart'])

function addToCart() {
  emit('add-to-cart', props.product.id)
}
</script>
VUE
<!-- HomeView.vue / ProductList.vue / SearchResults.vue 中 -->
<template>
  <ProductCard v-for="product in products" :key="product.id" :product="product" @add-to-cart="handleAdd" />
</template>

1 个 ProductCard.vue → 30 处复用。改按钮文字只需改 1 处。

(3) 收益

组件化后:


3. 组件是什么?

(1) 定义

组件(Component)是 Vue 中可复用的、有独立逻辑和样式的 UI 单元。每个组件 = 1 个 Vue 实例 + 自己的 data/methods/lifecycle。

100%
graph TB
    A[App.vue<br/>根组件] --> B[HomeView.vue<br/>首页]
    A --> C[AboutView.vue<br/>关于]
    A --> D[DashboardView.vue<br/>仪表盘]
    
    B --> E[ProductCard.vue<br/>商品卡片]
    B --> F[ProductList.vue<br/>商品列表]
    B --> G[FilterBar.vue<br/>筛选栏]
    
    E --> H[Button.vue<br/>按钮]
    E --> I[Badge.vue<br/>徽章]
    
    F --> E
    G --> J[Select.vue<br/>下拉]
    G --> K[Input.vue<br/>输入]
    
    style A fill:#42b883,color:#fff
    style E fill:#42b883,color:#fff
    style H fill:#42b883,color:#fff

(2) 5 大核心特点

特点 说明
可复用 1 个组件可在 N 处使用
封装性 自己的 data / methods / style,互不干扰
可组合 小组件组合成大组件
可测试 单元测试单个组件
可维护 改 1 处 = 改 N 处

(3) 组件 vs 函数

维度 函数 组件
复用单位 代码逻辑 UI + 逻辑
输入 参数 (args) props
输出 return emit
状态 局部变量 data / refs
副作用 生命周期钩子

4. 单文件组件 SFC

(1) 什么是 SFC?

SFC(Single File Component)= 1 个 .vue 文件 = 1 个组件。把 template、script、style 集中在一个文件里,便于管理。

VUE
<!-- ProductCard.vue - SFC 单文件组件 -->
<template>
  <!-- 1. template:HTML 模板(必填) -->
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>${{ product.price }}</p>
  </div>
</template>

<script setup>
// 2. script:JS 逻辑(必填)
const { ref } = Vue

const props = defineProps({ product: Object })
const count = ref(0)
</script>

<style scoped>
/* 3. style:CSS 样式(可选) */
.product-card {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 1rem;
}
</style>

(2) SFC 3 部分详解

部分 作用 必填? 语言
<template> HTML 模板 HTML + Vue 指令
<script setup> JS 逻辑 JavaScript / TypeScript
<style scoped> CSS 样式 CSS / SCSS / Less

(3) SFC vs HTML 字符串

JS
// ❌ 不用 SFC(字符串模板,Vue 2 风格)
Vue.component('my-component', {
  template: '<div>{{ msg }}</div>',
  data() { return { msg: 'Hello' } }
})

// ✅ 用 SFC(推荐)
// MyComponent.vue
<template>
  <div>{{ msg }}</div>
</template>
<script setup>
const msg = 'Hello'
</script>

SFC 优势


5. 组件注册

(1) 全局注册(不推荐大型项目)

JS
// main.js
const { createApp } = Vue
import App from './App.vue'
import ProductCard from './components/ProductCard.vue'

const app = createApp(App)

// 全局注册:所有组件都能用 <ProductCard />
app.component('ProductCard', ProductCard)

app.mount('#app')

(2) 局部注册(推荐)

VUE
<!-- HomeView.vue -->
<script setup>
// 1. 导入
import ProductCard from '@/components/ProductCard.vue'

// 2. 在模板中使用(无需注册,自动可用)
</script>

<template>
  <ProductCard :product="product" />
</template>

(3) 两种注册对比

维度 全局注册 局部注册
可用范围 整个应用 当前组件
代码位置 main.js 组件内 <script setup>
Tree-shaking ❌ 不支持 ✅ 支持
类型提示 强(IDE 自动补全)
推荐度 ⭐⭐ 插件用 ⭐⭐⭐⭐⭐ 业务用

6. 组件命名规范

(1) 3 种命名风格

风格 例子 用法 推荐度
PascalCase ProductCard.vue JS import / 模板中 ⭐⭐⭐⭐⭐
kebab-case product-card.vue 文件名 / kebab-case 模板 ⭐⭐⭐
camelCase productCard.vue 不推荐

(2) 模板中使用

VUE
<template>
  <!-- PascalCase(推荐,IDE 友好) -->
  <ProductCard />
  <UserProfile />
  
  <!-- kebab-case(也支持) -->
  <product-card />
  <user-profile />
  
  <!-- ✅ 两种都能用,PascalCase 更易读 -->
</template>

(3) 文件夹结构

TEXT 📖 仅展示
src/
├── components/          # 公共组件(跨页面用)
│   ├── ProductCard.vue
│   ├── Button.vue
│   └── Header.vue
├── views/               # 页面组件(路由用)
│   ├── HomeView.vue
│   ├── ProductListView.vue
│   └── DashboardView.vue
├── composables/         # 组合式函数
│   ├── useAuth.ts
│   └── useFetch.ts
├── router/              # 路由配置
└── App.vue              # 根组件

7. props 和 emit 基础

(1) props:父传子

VUE
<!-- 父组件 Parent.vue -->
<template>
  <ProductCard :product="myProduct" :show-stock="true" />
</template>

<!-- 子组件 ProductCard.vue -->
<template>
  <div>
    <h3>{{ product.name }}</h3>
    <p v-if="showStock">Stock: {{ product.stock }}</p>
  </div>
</template>

<script setup>
// 接收父组件传的 props
const props = defineProps({
  product: { type: Object, required: true },
  showStock: { type: Boolean, default: false }
})
</script>

(2) emit:子传父

VUE
<!-- 子组件 ProductCard.vue -->
<template>
  <button @click="handleAdd">Add to Cart</button>
</template>

<script setup>
const emit = defineEmits(['add-to-cart'])

function handleAdd() {
  emit('add-to-cart', { id: 1, name: 'iPhone' })
}
</script>
VUE
<!-- 父组件 Parent.vue -->
<template>
  <ProductCard @add-to-cart="handleAddToCart" />
</template>

<script setup>
function handleAddToCart(product) {
  console.log('Add to cart:', product)
}
</script>

(3) props vs emit 速查

方向 API 用途
父 → 子 defineProps() 数据传递
子 → 父 defineEmits() 事件通知

8. 完整示例:5 个 ProductCard 复用

▶ 示例:ProductCard 组件(CDN 多组件演示)

HTML 📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<style>
.product-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; margin: 0.5rem 0; transition: box-shadow 0.2s; }
.product-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.out-of-stock { opacity: 0.6; }
.price { color: #42b883; font-weight: bold; }
.stock { color: #f59e0b; font-size: 0.85rem; }
button:disabled { background: #ccc; cursor: not-allowed; }
button { background: #42b883; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; }
</style>

<div id="app">
  <h3>Featured Products</h3>
  <product-card
    v-for="product in products"
    :key="product.id"
    :product="product"
    @add-to-cart="handleAdd"
  ></product-card>
  <p>购物车商品数: {{ cartCount }}</p>
</div>

<script>
const { createApp, ref } = Vue

// 子组件:ProductCard
const ProductCard = {
  props: {
    product: {
      type: Object,
      required: true,
      validator: (val) => val.id && val.name && val.price !== undefined
    }
  },
  emits: ['add-to-cart'],
  template: `
    <div :class="['product-card', { 'out-of-stock': product.stock === 0 }]">
      <h3>{{ product.name }}</h3>
      <p class="price">${{ product.price }}</p>
      <span class="stock" v-if="product.stock < 10 && product.stock > 0">
        Only {{ product.stock }} left
      </span>
      <button
        :disabled="product.stock === 0"
        @click="$emit('add-to-cart', product.id)"
      >
        {{ product.stock === 0 ? 'Out of Stock' : 'Add to Cart' }}
      </button>
    </div>
  `
}

// 根组件
const App = {
  components: { ProductCard },
  setup() {
    const products = ref([
      { id: 1, name: 'iPhone 15 Pro', price: 999, stock: 50 },
      { id: 2, name: 'MacBook Pro', price: 2499, stock: 5 },
      { id: 3, name: 'Sold Out Item', price: 99, stock: 0 }
    ])
    const cartCount = ref(0)
    function handleAdd(id) {
      cartCount.value++
      console.log('Added to cart:', id)
    }
    return { products, cartCount, handleAdd }
  }
}

createApp(App).mount('#app')
</script>
逻辑代码 65 行(超过 40 行限制,仅展示)

▶ 示例:在 3 个页面复用 ProductCard

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<style>
.product-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; margin: 0.5rem 0; }
</style>

<div id="app">
  <!-- Page 1: 首页 Featured -->
  <h3>首页 Featured</h3>
  <product-card v-for="p in featured" :key="p.id" :product="p"></product-card>

  <!-- Page 2: 列表 All -->
  <h3>商品列表 All</h3>
  <product-card v-for="p in all" :key="p.id" :product="p"></product-card>

  <!-- Page 3: 分类 Phones -->
  <h3>分类 Phones</h3>
  <product-card v-for="p in phones" :key="p.id" :product="p"></product-card>
</div>

<script>
const { createApp, ref, computed } = Vue

const ProductCard = {
  props: ['product'],
  template: `
    <div class="product-card">
      <h4>{{ product.name }}</h4>
      <p>${{ product.price }} | 库存: {{ product.stock }}</p>
    </div>
  `
}

const App = {
  components: { ProductCard },
  setup() {
    const products = ref([
      { id: 1, name: 'iPhone', price: 999, stock: 50, category: 'phone' },
      { id: 2, name: 'MacBook', price: 2499, stock: 20, category: 'laptop' },
      { id: 3, name: 'iPad', price: 599, stock: 30, category: 'tablet' }
    ])
    const featured = computed(() => products.value.slice(0, 2))
    const all = computed(() => products.value)
    const phones = computed(() => products.value.filter(p => p.category === 'phone'))
    return { featured, all, phones }
  }
}

createApp(App).mount('#app')
</script>
▶ 试一试

▶ 示例:全局注册 vs 局部注册对比

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <h3>局部注册演示</h3>
  <my-button text="按钮 A" variant="primary"></my-button>
  <my-button text="按钮 B" variant="success"></my-button>
  <my-button text="按钮 C" variant="danger"></my-button>
</div>

<script>
const { createApp } = Vue

// 子组件
const MyButton = {
  props: ['text', 'variant'],
  template: `
    <button :style="{ background: bgColor, color: 'white', padding: '6px 12px', border: 'none', borderRadius: '4px', margin: '4px' }">
      {{ text }}
    </button>
  `,
  computed: {
    bgColor() {
      return { primary: '#3b82f6', success: '#10b981', danger: '#ef4444' }[this.variant] || '#999'
    }
  }
}

// ✅ 局部注册(推荐)
const App = {
  components: { MyButton },
  template: `
    <my-button text="局部注册按钮" variant="primary"></my-button>
  `
}

const app = createApp(App)
// ❌ 全局注册(不推荐 — 整个应用都可用但 bundle 变大)
// app.component('MyButton', MyButton)
app.mount('#app')
</script>
▶ 试一试

▶ 示例:props 5 大类型 + 验证器

HTML 📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <user-card
    :name="'Alice'"
    :age="25"
    :active="true"
    :user="{ id: 1, role: 'admin' }"
    :items="['Vue', 'React', 'Angular']"
    :title="'管理员'"
    :page-size="20"
    :email="'alice@example.com'"
    :status="'active'"
  ></user-card>
</div>

<script>
const { createApp } = Vue

const UserCard = {
  // props 5 大类型 + 验证器
  props: {
    // 基础类型
    name: String,
    age: Number,
    active: Boolean,

    // 复杂类型
    user: Object,
    items: Array,

    // 必填 + 默认值
    title: { type: String, required: true },
    pageSize: { type: Number, default: 20 },

    // 验证器
    email: {
      type: String,
      validator: (val) => val.includes('@')
    },
    status: {
      type: String,
      validator: (val) => ['active', 'inactive'].includes(val)
    }
  },
  template: `
    <div style="padding: 1rem; border: 1px solid #eee;">
      <h3>{{ title }} - {{ name }}</h3>
      <p>年龄: {{ age }} | 状态: {{ active ? '在线' : '离线' }}</p>
      <p>角色: {{ user.role }} | 框架: {{ items.join(', ') }}</p>
      <p>邮箱: {{ email }} | 每页: {{ pageSize }}</p>
    </div>
  `
}

const App = { components: { UserCard } }
createApp(App).mount('#app')
</script>
逻辑代码 46 行(超过 40 行限制,仅展示)

▶ 示例:5 个组件设计原则

原则 说明
单一职责 1 个组件只做 1 件事
可复用 抽象通用 UI(Button/Input/Modal)
可配置 props 控制行为,slot 控制内容
可组合 小组件组合成大组件
易测试 组件独立,便于单元测试

▶ 示例:5 个常见错误

错误 现象 解决
组件名单文件 IDE 报错 用 PascalCase:ProductCard.vue
全局注册太多 bundle 变大 局部注册支持 tree-shaking
组件太大 难维护 拆成小组件
props 直接修改 Vue 警告 props 只读,要改用 emit
忘记 import 组件 模板显示 "unknown component" 检查 <script setup> 中的 import

❓ 常见问题

Q 什么时候用全局注册?
A 只在 2 种情况:(1) 插件式组件(如 Element Plus 的 <el-button>);(2) 整个应用都要用的基础组件(Button/Icon)。业务组件一律局部注册,tree-shaking 才能生效。
Q SFC 必须 3 部分都有吗?
A <template><script> 必填,<style> 可选。如果组件只有 JS(比如只是封装数据),可以没有 template(用 render 函数)。
Q 组件文件多大合适?
A 建议 < 200 行。超过 300 行考虑拆分。组件越简单越好维护。
Q SFC 用 .vue 后缀,其他工具怎么识别?
A Vite / Webpack / Vue CLI 都默认识别 .vue 后缀。VS Code 装 "Vue - Official" 扩展。
Q 如何测试单个组件?
A 用 Vitest + Vue Test Utils(@vue/test-utils):mount(ProductCard, { props: { product: mockProduct } })。本教程 Phase 4.6 详细介绍。
Q 组件库(Element Plus)怎么用?
A npm 安装 → main.js 全局注册 → 模板中直接用 <el-button>。具体看 Phase 4.7 章节。
Q SFC 和 React JSX 的区别?
A SFC 用 HTML-like 模板(接近 web 标准),React 用 JSX(JS 写 HTML)。SFC 学习曲线平缓,JSX 灵活但需要 build。Vue 3 也支持 JSX(@vitejs/plugin-vue-jsx),但 SFC 是默认推荐。

📖 小节


📝 作业

  1. 基础题(难度⭐) 写一个 Button 组件 src/components/BaseButton.vue

    • props: text (String), variant (String, 'primary'/'success'/'danger')
    • 3 种 variant 对应 3 种 class
    • emit click 事件
    • 在 HomeView.vue 中使用 3 个不同 variant
  2. 进阶题(难度⭐⭐) 写一个商品列表 + 卡片系统:

    • ProductCard.vue:单个商品卡片(props: product, emit: add-to-cart)
    • ProductList.vue:商品列表容器(v-for + 过滤)
    • App.vue:根组件,传数据 + 处理 add-to-cart 事件
    • 3 个文件 3 个组件嵌套使用
  3. 挑战题(难度⭐⭐⭐) 实现一个完整的"用户卡片"组件系统:

    1. Avatar.vue:头像组件(支持 url / initials 两种模式)
    2. UserCard.vue:用户卡片(用 Avatar)
    3. UserList.vue:用户列表(用 UserCard)
    4. App.vue:根组件(数据 + 交互)
    5. 5 个 props(avatar/name/email/role/active)
    6. 3 个 emit 事件(edit/delete/select)
    7. scoped 样式隔离
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏