文章目录
1. Vue组件基础概念
1.1 什么是Vue组件?
Vue组件是可复用的Vue实例,具有独立的功能和界面。组件化是Vue.js的核心思想之一,它允许我们将复杂的应用拆分成独立、可复用的小组件。
1.2 组件的核心特点
1.2.1 可复用性
- 一次编写,多处使用
- 减少代码重复
- 提高开发效率
1.2.2 封装性
- 内部逻辑独立
- 数据和方法封装
- 样式作用域隔离
1.2.3 组合性
- 组件可以嵌套使用
- 父子组件通信
- 构建复杂应用
1.2.4 维护性
- 职责单一
- 易于测试
- 便于维护和更新
1.3 组件的组成部分
Vue组件通常由三个部分组成:
<template>
<!-- HTML模板 -->
<div>组件的HTML结构</div>
</template>
<script>
// JavaScript逻辑
export default {
name: 'MyComponent',
// 组件选项
}
</script>
<style scoped>
/* CSS样式 */
div {
color: blue;
}
</style>
各部分说明:
- template: HTML模板,定义组件的结构
- script: JavaScript逻辑,定义组件的行为
- style: CSS样式,定义组件的外观
2. 创建第一个自定义组件
2.1 简单的Hello组件
让我们从最简单的组件开始:
<!-- HelloComponent.vue -->
<template>
<div class="hello">
<h1>Hello, Vue组件!</h1>
<p>这是我的第一个自定义组件</p>
</div>
</template>
<script>
export default {
name: 'HelloComponent'
}
</script>
<style scoped>
.hello {
text-align: center;
color: #42b983;
padding: 20px;
border: 2px solid #42b983;
border-radius: 10px;
margin: 20px;
}
h1 {
font-size: 2em;
margin-bottom: 10px;
}
p {
font-size: 1.2em;
color: #666;
}
</style>
2.2 在父组件中使用
<!-- App.vue -->
<template>
<div id="app">
<h1>我的Vue应用</h1>
<!-- 使用自定义组件 -->
<HelloComponent />
<HelloComponent />
<HelloComponent />
</div>
</template>
<script>
// 导入组件
import HelloComponent from './components/HelloComponent.vue'
export default {
name: 'App',
components: {
// 注册组件
HelloComponent
}
}
</script>
<style>
#app {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
</style>
2.3 组件注册详解
2.3.1 局部注册(推荐)
<script>
import MyComponent from './components/MyComponent.vue'
export default {
components: {
MyComponent, // ES6简写
// 完整写法:MyComponent: MyComponent
}
}
</script>
2.3.2 全局注册
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'
const app = createApp(App)
// 全局注册组件
app.component('MyComponent', MyComponent)
app.mount('#app')
2.3.3 注册方式对比
注册方式 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
局部注册 | 按需引入,减小打包体积 | 需要在每个使用的地方导入 | 大部分场景 |
全局注册 | 无需导入,直接使用 | 增加打包体积,难以追踪依赖 | 基础组件 |
3. 组件Props详解
3.1 什么是Props?
Props是父组件传递给子组件的数据。它是组件间通信的主要方式之一。
3.2 基础Props使用
3.2.1 定义Props
<!-- UserCard.vue -->
<template>
<div class="user-card">
<img :src="avatar" :alt="name" class="avatar">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
<p>年龄: {{ age }}</p>
</div>
</template>
<script>
export default {
name: 'UserCard',
props: {
name: String, // 字符串类型
email: String, // 字符串类型
age: Number, // 数字类型
avatar: String // 字符串类型
}
}
</script>
<style scoped>
.user-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin: 10px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 10px;
}
</style>
3.2.2 使用Props
<!-- 父组件 -->
<template>
<div>
<h1>用户列表</h1>
<UserCard
name="张三"
email="zhangsan@example.com"
:age="25"
avatar="https://via.placeholder.com/80"
/>
<UserCard
name="李四"
email="lisi@example.com"
:age="30"
avatar="https://via.placeholder.com/80"
/>
</div>
</template>
<script>
import UserCard from './components/UserCard.vue'
export default {
components: {
UserCard
}
}
</script>
重要注意事项:
- 字符串可以直接传递:
name="张三"
- 其他类型需要使用v-bind:
:age="25"
3.3 Props类型详解
3.3.1 基础类型
<script>
export default {
props: {
// 字符串
title: String,
// 数字
count: Number,
// 布尔值
isActive: Boolean,
// 数组
items: Array,
// 对象
user: Object,
// 函数
callback: Function,
// 日期
date: Date
}
}
</script>
3.3.2 多类型Props
<script>
export default {
props: {
// 多种类型
value: [String, Number],
// 任意类型
data: null,
// 自定义类型
person: Person // 假设Person是一个类
}
}
</script>
3.3.3 详细Props配置
<script>
export default {
props: {
// 基础类型检查
title: String,
// 必需的字符串
name: {
type: String,
required: true
},
// 带默认值的数字
age: {
type: Number,
default: 18
},
// 带默认值的对象
user: {
type: Object,
default() {
return { name: '匿名用户', age: 0 }
}
},
// 带验证的自定义prop
score: {
type: Number,
validator(value) {
return value >= 0 && value <= 100
}
},
// 多类型 + 默认值
size: {
type: [String, Number],
default: 'medium',
validator(value) {
return ['small', 'medium', 'large'].includes(value) || typeof value === 'number'
}
}
}
}
</script>
3.4 Props最佳实践
3.4.1 命名规范
<script>
export default {
props: {
// ✅ 推荐:camelCase
userName: String,
isActive: Boolean,
maxLength: Number,
// ❌ 避免:kebab-case在JavaScript中
// 'user-name': String, // 不推荐
}
}
</script>
<!-- 模板中使用kebab-case -->
<template>
<MyComponent
:user-name="name"
:is-active="active"
:max-length="50"
/>
</template>
3.4.2 Props验证示例
<!-- ProductCard.vue -->
<template>
<div class="product-card">
<img :src="product.image" :alt="product.name">
<h3>{{ product.name }}</h3>
<p class="price">¥{{ product.price }}</p>
<p class="description">{{ product.description }}</p>
<button :class="buttonClass" @click="addToCart">
加入购物车
</button>
</div>
</template>
<script>
export default {
name: 'ProductCard',
props: {
product: {
type: Object,
required: true,
validator(value) {
// 验证必需的属性
return value &&
typeof value.name === 'string' &&
typeof value.price === 'number' &&
value.price > 0
}
},
size: {
type: String,
default: 'medium',
validator(value) {
return ['small', 'medium', 'large'].includes(value)
}
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
buttonClass() {
return {
'btn': true,
'btn-small': this.size === 'small',
'btn-medium': this.size === 'medium',
'btn-large': this.size === 'large',
'btn-disabled': this.disabled
}
}
},
methods: {
addToCart() {
if (!this.disabled) {
this.$emit('add-to-cart', this.product)
}
}
}
}
</script>
<style scoped>
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin: 8px;
transition: box-shadow 0.3s;
}
.product-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.price {
font-size: 1.2em;
font-weight: bold;
color: #e74c3c;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-small { padding: 4px 8px; font-size: 0.8em; }
.btn-medium { padding: 8px 16px; font-size: 1em; }
.btn-large { padding: 12px 24px; font-size: 1.2em; }
.btn:not(.btn-disabled) {
background-color: #3498db;
color: white;
}
.btn:not(.btn-disabled):hover {
background-color: #2980b9;
}
.btn-disabled {
background-color: #bdc3c7;
color: #7f8c8d;
cursor: not-allowed;
}
</style>
4. 组件事件与通信
4.1 子组件向父组件传递数据
4.1.1 使用$emit触发事件
<!-- Counter.vue -->
<template>
<div class="counter">
<h3>计数器: {{ count }}</h3>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
export default {
name: 'Counter',
props: {
initialValue: {
type: Number,
default: 0
}
},
data() {
return {
count: this.initialValue
}
},
methods: {
increment() {
this.count++
// 触发自定义事件,传递新的值
this.$emit('count-change', this.count)
},
decrement() {
this.count--
this.$emit('count-change', this.count)
},
reset() {
this.count = this.initialValue
this.$emit('count-change', this.count)
// 触发重置事件
this.$emit('reset')
}
}
}
</script>
<style scoped>
.counter {
border: 2px solid #3498db;
border-radius: 8px;
padding: 20px;
margin: 10px;
text-align: center;
}
button {
margin: 5px;
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #3498db;
color: white;
cursor: pointer;
}
button:hover {
background-color: #2980b9;
}
</style>
4.1.2 在父组件中监听事件
<!-- 父组件 -->
<template>
<div>
<h1>计数器应用</h1>
<p>总计数: {{ totalCount }}</p>
<Counter
:initial-value="10"
@count-change="handleCountChange"
@reset="handleReset"
/>
<Counter
:initial-value="5"
@count-change="handleCountChange"
@reset="handleReset"
/>
<!-- 事件日志 -->
<div class="log">
<h3>事件日志:</h3>
<ul>
<li v-for="(log, index) in eventLogs" :key="index">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<script>
import Counter from './components/Counter.vue'
export default {
components: {
Counter
},
data() {
return {
totalCount: 15, // 10 + 5
eventLogs: []
}
},
methods: {
handleCountChange(newValue) {
console.log('计数器值改变:', newValue)
this.eventLogs.push(`计数器值改变为: ${newValue} - ${new Date().toLocaleTimeString()}`)
// 这里可以进行其他处理,比如更新总计数
},
handleReset() {
console.log('计数器被重置')
this.eventLogs.push(`计数器被重置 - ${new Date().toLocaleTimeString()}`)
}
}
}
</script>
<style>
.log {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
}
.log ul {
max-height: 200px;
overflow-y: auto;
}
.log li {
margin: 5px 0;
padding: 5px;
background-color: white;
border-radius: 3px;
}
</style>
4.2 事件验证
4.2.1 声明emits选项
<script>
export default {
name: 'MyComponent',
emits: ['update', 'delete', 'create'],
// 或者使用对象语法进行验证
emits: {
// 无验证
click: null,
// 带验证
submit: (payload) => {
return payload && payload.email && payload.password
},
// 复杂验证
'update-user': (user) => {
return user &&
typeof user.id === 'number' &&
typeof user.name === 'string'
}
},
methods: {
handleSubmit() {
const payload = { email: 'test@example.com', password: '123456' }
this.$emit('submit', payload)
}
}
}
</script>
4.3 表单组件实例
<!-- LoginForm.vue -->
<template>
<form @submit.prevent="handleSubmit" class="login-form">
<h2>用户登录</h2>
<div class="form-group">
<label for="email">邮箱:</label>
<input
id="email"
v-model="formData.email"
type="email"
required
:class="{ error: errors.email }"
@blur="validateEmail"
>
<span v-if="errors.email" class="error-message">{{ errors.email }}</span>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input
id="password"
v-model="formData.password"
type="password"
required
:class="{ error: errors.password }"
@blur="validatePassword"
>
<span v-if="errors.password" class="error-message">{{ errors.password }}</span>
</div>
<div class="form-group">
<label>
<input
v-model="formData.rememberMe"
type="checkbox"
> 记住我
</label>
</div>
<button type="submit" :disabled="!isFormValid" class="submit-btn">
{{ loading ? '登录中...' : '登录' }}
</button>
<p class="register-link">
还没有账号?
<a href="#" @click.prevent="$emit('switch-to-register')">立即注册</a>
</p>
</form>
</template>
<script>
export default {
name: 'LoginForm',
emits: {
'login-submit': (formData) => {
return formData && formData.email && formData.password
},
'switch-to-register': null
},
props: {
loading: {
type: Boolean,
default: false
}
},
data() {
return {
formData: {
email: '',
password: '',
rememberMe: false
},
errors: {
email: '',
password: ''
}
}
},
computed: {
isFormValid() {
return this.formData.email &&
this.formData.password &&
!this.errors.email &&
!this.errors.password
}
},
methods: {
validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!this.formData.email) {
this.errors.email = '邮箱不能为空'
} else if (!emailRegex.test(this.formData.email)) {
this.errors.email = '请输入有效的邮箱地址'
} else {
this.errors.email = ''
}
},
validatePassword() {
if (!this.formData.password) {
this.errors.password = '密码不能为空'
} else if (this.formData.password.length < 6) {
this.errors.password = '密码至少需要6位'
} else {
this.errors.password = ''
}
},
handleSubmit() {
this.validateEmail()
this.validatePassword()
if (this.isFormValid) {
this.$emit('login-submit', { ...this.formData })
}
}
}
}
</script>
<style scoped>
.login-form {
max-width: 400px;
margin: 0 auto;
padding: 30px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s;
}
input[type="email"]:focus,
input[type="password"]:focus {
outline: none;
border-color: #3498db;
}
input.error {
border-color: #e74c3c;
}
.error-message {
color: #e74c3c;
font-size: 14px;
margin-top: 5px;
display: block;
}
.submit-btn {
width: 100%;
padding: 12px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.submit-btn:hover:not(:disabled) {
background-color: #2980b9;
}
.submit-btn:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.register-link {
text-align: center;
margin-top: 15px;
}
.register-link a {
color: #3498db;
text-decoration: none;
}
.register-link a:hover {
text-decoration: underline;
}
</style>
5. 插槽(Slots)详解
5.1 基础插槽
5.1.1 默认插槽
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header">
<h3>{{ title }}</h3>
</div>
<div class="card-body">
<!-- 插槽内容会在这里显示 -->
<slot></slot>
</div>
<div class="card-footer" v-if="showFooter">
<small>{{ footerText }}</small>
</div>
</div>
</template>
<script>
export default {
name: 'Card',
props: {
title: {
type: String,
required: true
},
showFooter: {
type: Boolean,
default: true
},
footerText: {
type: String,
default: 'Card footer'
}
}
}
</script>
<style scoped>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin: 10px;
}
.card-header {
background-color: #f8f9fa;
padding: 15px;
border-bottom: 1px solid #e0e0e0;
}
.card-header h3 {
margin: 0;
color: #333;
}
.card-body {
padding: 20px;
}
.card-footer {
background-color: #f8f9fa;
padding: 10px 15px;
border-top: 1px solid #e0e0e0;
color: #666;
}
</style>
5.1.2 使用默认插槽
<!-- 父组件 -->
<template>
<div>
<Card title="用户信息">
<p>姓名: 张三</p>
<p>年龄: 25岁</p>
<p>职业: 前端开发工程师</p>
</Card>
<Card title="产品列表" :show-footer="false">
<ul>
<li>苹果 - ¥5.00</li>
<li>香蕉 - ¥3.00</li>
<li>橙子 - ¥4.00</li>
</ul>
</Card>
<Card title="图片展示" footer-text="点击查看大图">
<img src="https://via.placeholder.com/300x200" alt="示例图片" style="width: 100%; border-radius: 4px;">
</Card>
</div>
</template>
<script>
import Card from './components/Card.vue'
export default {
components: {
Card
}
}
</script>
5.2 具名插槽
5.2.1 定义具名插槽
<!-- Layout.vue -->
<template>
<div class="layout">
<header class="layout-header">
<slot name="header">
<!-- 默认头部内容 -->
<h1>默认标题</h1>
</slot>
</header>
<nav class="layout-sidebar">
<slot name="sidebar">
<!-- 默认侧边栏内容 -->
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">关于</a></li>
</ul>
</slot>
</nav>
<main class="layout-main">
<slot>
<!-- 默认主要内容 -->
<p>默认主要内容</p>
</slot>
</main>
<footer class="layout-footer">
<slot name="footer">
<!-- 默认底部内容 -->
<p>© 2023 我的网站</p>
</slot>
</footer>
</div>
</template>
<script>
export default {
name: 'Layout'
}
</script>
<style scoped>
.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 200px 1fr;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.layout-header {
grid-area: header;
background-color: #2c3e50;
color: white;
padding: 1rem;
}
.layout-sidebar {
grid-area: sidebar;
background-color: #34495e;
color: white;
padding: 1rem;
}
.layout-main {
grid-area: main;
padding: 2rem;
background-color: #ecf0f1;
}
.layout-footer {
grid-area: footer;
background-color: #95a5a6;
color: white;
padding: 1rem;
text-align: center;
}
.layout-sidebar ul {
list-style: none;
padding: 0;
}
.layout-sidebar li {
margin: 10px 0;
}
.layout-sidebar a {
color: white;
text-decoration: none;
}
.layout-sidebar a:hover {
text-decoration: underline;
}
</style>
5.2.2 使用具名插槽
<!-- 父组件 -->
<template>
<Layout>
<!-- 使用 v-slot 指定插槽名称 -->
<template v-slot:header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<h1>我的博客</h1>
<nav>
<a href="#" style="color: white; margin: 0 10px;">首页</a>
<a href="#" style="color: white; margin: 0 10px;">文章</a>
<a href="#" style="color: white; margin: 0 10px;">关于</a>
</nav>
</div>
</template>
<!-- 简写语法 #sidebar -->
<template #sidebar>
<div>
<h3>分类</h3>
<ul>
<li><a href="#">前端开发</a></li>
<li><a href="#">后端开发</a></li>
<li><a href="#">数据库</a></li>
<li><a href="#">运维部署</a></li>
</ul>
<h3>最新文章</h3>
<ul>
<li><a href="#">Vue 3 组件开发指南</a></li>
<li><a href="#">JavaScript 异步编程</a></li>
<li><a href="#">CSS Grid 布局详解</a></li>
</ul>
</div>
</template>
<!-- 默认插槽 -->
<div>
<article>
<h2>Vue 组件化开发最佳实践</h2>
<p class="meta">发布时间: 2023-12-01 | 作者: 张三</p>
<p>组件化是 Vue.js 的核心概念之一,通过合理的组件设计可以大大提高代码的可维护性和复用性...</p>
<p>在本文中,我们将深入探讨 Vue 组件的各个方面,包括 Props、Events、Slots 等重要概念...</p>
</article>
</div>
<template #footer>
<div>
<p>© 2023 我的博客 |
<a href="#" style="color: white;">隐私政策</a> |
<a href="#" style="color: white;">联系我们</a>
</p>
</div>
</template>
</Layout>
</template>
<script>
import Layout from './components/Layout.vue'
export default {
components: {
Layout
}
}
</script>
<style>
.meta {
color: #666;
font-size: 0.9em;
margin-bottom: 15px;
}
article {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
5.3 作用域插槽
作用域插槽允许子组件向父组件传递数据,父组件可以决定如何渲染这些数据。
5.3.1 基础作用域插槽
<!-- TodoList.vue -->
<template>
<div class="todo-list">
<h3>{{ title }}</h3>
<ul>
<li v-for="item in items" :key="item.id" class="todo-item">
<!-- 作用域插槽,传递数据给父组件 -->
<slot :item="item" :index="index" :toggleComplete="toggleComplete">
<!-- 默认内容 -->
<span :class="{ completed: item.completed }">{{ item.text }}</span>
<button @click="toggleComplete(item.id)">
{{ item.completed ? '取消完成' : '标记完成' }}
</button>
</slot>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'TodoList',
props: {
title: String,
items: Array
},
methods: {
toggleComplete(id) {
this.$emit('toggle-complete', id)
}
}
}
</script>
5.3.2 使用作用域插槽
<!-- 父组件 -->
<template>
<div>
<TodoList
title="我的任务列表"
:items="todos"
@toggle-complete="handleToggle"
>
<!-- 使用作用域插槽自定义渲染 -->
<template #default="{ item, toggleComplete }">
<div class="custom-todo">
<input
type="checkbox"
:checked="item.completed"
@change="toggleComplete(item.id)"
>
<span
:class="{
'completed': item.completed,
'urgent': item.priority === 'high'
}"
>
{{ item.text }}
</span>
<span class="priority">{{ item.priority }}</span>
<button @click="deleteItem(item.id)" class="delete-btn">删除</button>
</div>
</template>
</TodoList>
</div>
</template>
6. 动态组件
6.1 基础动态组件
<template>
<div class="tab-container">
<div class="tab-buttons">
<button
v-for="tab in tabs"
:key="tab.name"
@click="currentTab = tab.name"
:class="{ active: currentTab === tab.name }"
>
{{ tab.label }}
</button>
</div>
<!-- 动态组件 -->
<component :is="currentTab" :data="tabData[currentTab]"></component>
</div>
</template>
<script>
import UserProfile from './UserProfile.vue'
import UserSettings from './UserSettings.vue'
import UserPosts from './UserPosts.vue'
export default {
components: {
UserProfile,
UserSettings,
UserPosts
},
data() {
return {
currentTab: 'UserProfile',
tabs: [
{ name: 'UserProfile', label: '个人资料' },
{ name: 'UserSettings', label: '设置' },
{ name: 'UserPosts', label: '我的文章' }
],
tabData: {
UserProfile: { name: '张三', age: 25 },
UserSettings: { theme: 'dark', language: 'zh-CN' },
UserPosts: [{ title: '文章1' }, { title: '文章2' }]
}
}
}
}
</script>
7. 生命周期钩子
7.1 组件生命周期详解
<template>
<div class="lifecycle-demo">
<h3>生命周期演示组件</h3>
<p>计数器: {{ count }}</p>
<button @click="count++">增加</button>
<p>消息: {{ message }}</p>
<input v-model="message" placeholder="输入消息">
</div>
</template>
<script>
export default {
name: 'LifecycleDemo',
props: {
initialCount: {
type: Number,
default: 0
}
},
data() {
return {
count: this.initialCount,
message: '',
timer: null
}
},
// 组件实例创建之前
beforeCreate() {
console.log('beforeCreate: 组件实例刚创建')
console.log('此时 data 和 methods 还不可用')
},
// 组件实例创建完成
created() {
console.log('created: 组件实例创建完成')
console.log('data:', this.count)
console.log('可以访问 data 和 methods,但DOM还未挂载')
// 适合进行数据初始化
this.fetchData()
},
// DOM挂载之前
beforeMount() {
console.log('beforeMount: DOM挂载之前')
console.log('模板编译完成,但还未渲染到页面')
},
// DOM挂载完成
mounted() {
console.log('mounted: DOM挂载完成')
console.log('可以访问DOM元素')
console.log('适合进行DOM操作或启动定时器')
// 启动定时器
this.timer = setInterval(() => {
console.log(`组件运行中,当前计数: ${this.count}`)
}, 5000)
},
// 数据更新前
beforeUpdate() {
console.log('beforeUpdate: 数据更新前')
console.log('响应式数据发生变化,DOM还未重新渲染')
},
// 数据更新后
updated() {
console.log('updated: 数据更新后')
console.log('DOM已重新渲染')
},
// 组件卸载前
beforeUnmount() {
console.log('beforeUnmount: 组件卸载前')
console.log('组件仍完全可用')
// 清理工作
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
// 组件卸载后
unmounted() {
console.log('unmounted: 组件卸载后')
console.log('组件实例已销毁')
},
methods: {
fetchData() {
console.log('模拟获取数据...')
setTimeout(() => {
this.message = '数据加载完成'
}, 1000)
}
}
}
</script>
8. 混入(Mixins)
8.1 基础混入
// mixins/loggerMixin.js
export default {
data() {
return {
logs: []
}
},
methods: {
log(message) {
const timestamp = new Date().toLocaleTimeString()
this.logs.push(`[${timestamp}] ${message}`)
console.log(`[${this.$options.name}] ${message}`)
},
clearLogs() {
this.logs = []
}
},
mounted() {
this.log('组件已挂载')
}
}
8.2 使用混入
<template>
<div class="mixed-component">
<h3>混入示例组件</h3>
<button @click="handleClick">点击我</button>
<button @click="clearLogs">清空日志</button>
<div class="logs">
<h4>操作日志:</h4>
<ul>
<li v-for="(log, index) in logs" :key="index">{{ log }}</li>
</ul>
</div>
</div>
</template>
<script>
import loggerMixin from '@/mixins/loggerMixin.js'
export default {
name: 'MixedComponent',
mixins: [loggerMixin],
data() {
return {
clickCount: 0
}
},
methods: {
handleClick() {
this.clickCount++
this.log(`按钮被点击,总计: ${this.clickCount} 次`)
}
}
}
</script>
9. 最佳实践
9.1 组件设计原则
9.1.1 单一职责原则
<!-- ✅ 好的例子:职责单一 -->
<template>
<div class="user-avatar">
<img :src="src" :alt="alt" :class="sizeClass">
</div>
</template>
<script>
export default {
name: 'UserAvatar',
props: {
src: String,
alt: String,
size: {
type: String,
default: 'medium',
validator: value => ['small', 'medium', 'large'].includes(value)
}
},
computed: {
sizeClass() {
return `avatar-${this.size}`
}
}
}
</script>
9.1.2 命名规范
<!-- ✅ 推荐命名 -->
<template>
<div>
<!-- 组件名:多词、PascalCase -->
<UserProfile />
<ProductCard />
<NavigationMenu />
<!-- 属性名:kebab-case -->
<UserCard
:user-name="name"
:is-active="active"
:max-items="10"
/>
</div>
</template>
9.1.3 Props设计
<script>
export default {
props: {
// ✅ 提供类型和默认值
title: {
type: String,
required: true
},
// ✅ 提供验证器
status: {
type: String,
default: 'pending',
validator: value => ['pending', 'approved', 'rejected'].includes(value)
},
// ✅ 对象类型使用函数返回默认值
config: {
type: Object,
default: () => ({
theme: 'light',
language: 'zh-CN'
})
}
}
}
</script>
9.2 性能优化
9.2.1 异步组件
// 路由级别的代码分割
const UserProfile = () => import('./components/UserProfile.vue')
// 条件加载
const HeavyComponent = () => {
if (process.env.NODE_ENV === 'development') {
return import('./components/HeavyComponent.vue')
}
return Promise.resolve(null)
}
9.2.2 KeepAlive缓存
<template>
<div>
<!-- 缓存动态组件 -->
<KeepAlive>
<component :is="currentComponent"></component>
</KeepAlive>
<!-- 条件缓存 -->
<KeepAlive :include="['UserProfile', 'UserSettings']">
<router-view></router-view>
</KeepAlive>
</div>
</template>
10. 常见问题与解决方案
10.1 Props变化监听
<script>
export default {
props: ['value'],
data() {
return {
localValue: this.value
}
},
watch: {
// 监听Props变化
value(newVal) {
this.localValue = newVal
}
},
methods: {
updateValue(val) {
this.localValue = val
this.$emit('input', val)
}
}
}
</script>
10.2 组件间通信
// 事件总线(小型应用)
import { createApp } from 'vue'
const eventBus = createApp({}).config.globalProperties
// 发送事件
eventBus.$emit('user-login', userData)
// 监听事件
eventBus.$on('user-login', (userData) => {
console.log('用户登录:', userData)
})
10.3 样式隔离
<style scoped>
/* scoped样式只影响当前组件 */
.button {
background: blue;
}
</style>
<style module>
/* CSS Modules */
.button {
background: red;
}
</style>
<template>
<div>
<button class="button">Scoped按钮</button>
<button :class="$style.button">Module按钮</button>
</div>
</template>
11. 总结
通过本指南,您已经掌握了Vue自定义组件开发的核心知识:
- 组件基础:理解组件概念和结构
- Props传递:父子组件数据通信
- 事件系统:子父组件通信机制
- 插槽系统:内容分发和定制
- 动态组件:根据条件渲染不同组件
- 生命周期:组件各阶段的钩子函数
- 混入机制:代码复用和功能扩展
- 最佳实践:组件设计和性能优化
组件化开发是Vue.js的核心特性,熟练掌握这些概念将大大提高您的开发效率和代码质量。建议多进行实际练习,逐步构建复杂的组件系统。