前言
组件化是Vue.js最核心的开发思想,掌握组件化开发流程和技巧对于构建可维护的大型应用至关重要。本文从一个小案例出发讲解最基本的组件开发步骤,及组件间基本的通信方法.
一、组件化开发流程
1. 组件拆分原则
1.1 静态组件拆分
拆分准则:
单一职责原则:每个组件只负责一个功能
高内聚低耦合:相关功能放在同一组件
命名规范:使用多单词命名避免冲突
示例:
TodoApp/
├── TodoHeader.vue # 头部组件
├── TodoList.vue # 列表组件
│ ├── TodoItem.vue # 列表项组件
├── TodoFooter.vue # 底部组件
└── TodoAdd.vue # 添加任务组件
1.2 动态组件实现
数据存放策略:
数据使用范围 | 存放位置 | 示例 |
---|---|---|
单个组件使用 | 组件自身data | TodoItem的完成状态 |
多个组件共享 | 共同父组件 | TodoList的任务列表数据 |
全局共享 | Vuex/Pinia | 用户认证状态 |
2. 组件通信方式
2.1 父传子:Props
<!-- ParentComponent.vue -->
<template>
<ChildComponent :todos="todoList" />
</template>
<script>
export default {
data() {
return {
todoList: ['Learn Vue', 'Build Project']
}
}
}
</script>
<!-- ChildComponent.vue -->
<template>
<ul>
<li v-for="(todo, index) in todos" :key="index">{{ todo }}</li>
</ul>
</template>
<script>
export default {
props: ["todos"]
}
</script>
2.2 子传父:自定义事件
<!-- ParentComponent.vue -->
<template>
<ChildComponent :AddTodo="handleAddTodo" />
</template>
<script>
export default {
methods: {
handleAddTodo(newTodo) {
this.todoList.push(newTodo)
}
}
}
</script>
<!-- ChildComponent.vue -->
<template>
<button @click="addTodo">Add Todo</button>
</template>
<script>
export default {
methods: {
addTodo(newTodo) {
AddTodo(newTodo)
}
},
props:['AddTodo']
}
</script>
2.3 兄弟组件通信
通过共同的父组件中转:
Parent
├── ChildA (props method)
└── ChildB (接收props)
3. 交互实现技巧
使用
v-model
实现双向绑定(绑定非props变量)事件修饰符优化交互(
.prevent
,.stop
等)条件渲染控制组件显示(
v-if
,v-show
)
注意事项:
避免直接修改props,应使用事件通知父组件
v-model不能直接绑定props值
对象类型props的属性修改Vue不会报错,但不推荐
二、组件化开发实战
1. 项目结构设计
TodoApp/
├── components/
│ ├── TodoHeader.vue
│ ├── TodoList.vue
│ ├── TodoItem.vue
│ ├── TodoFooter.vue
│
└── App.vue
2. 核心代码实现
App.vue (根组件)
<template>
<div class="todoContainer">
<div class="todoWrap">
<MyHeader :receive="AddElement" />
<MyList :todos="todos" :receiveCheck="receiveCheck" :receiveDelete="receiveDelete"/>
<MyFooter :todos="todos" :receiveCheckAll="receiveCheckAll"
:receiveClearAll="receiveClearAll"/>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue';
import MyList from './components/MyList.vue'
import MyFooter from './components/MyFooter.vue';
export default {
name:'App',
components:{
MyHeader,
MyList,
MyFooter
},
data(){
return {
msg:'欢迎来公司参观',
todos:[
{id:'001', title:"足球", done:true},
{id:'002', title:"蓝球", done:false},
{id:'003', title:"排球", done:true},
]
}
},
methods:{
showDOM(){
console.log(this.$refs.title)
console.log(this.$refs.btn)
console.log(this.$refs.com)
},
AddElement(obj){
this.todos.unshift(obj)
},
receiveCheck(id){
this.todos.forEach((t)=>{
if(t.id===id)
{
t.done = !t.done;
}
})
},
receiveDelete(id){
this.todos = this.todos.filter((t)=>{
return t.id!==id;
})
},
receiveCheckAll(done){
this.todos.forEach((t)=>{
t.done=done;
})
},
receiveClearAll(){
this.todos = this.todos.filter((t)=>{
return !t.done
})
}
}
}
</script>
<style>
body{
background: #fff;
}
.btn{
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn1{
color:#fff,
background-color #da4f49;
border: 1px solid #bd362f;
}
.btn1:hover{
color:#fff,
background-color #bd362f;
}
.btn:focus{
outline:none
}
.todoContainer{
width:600px;
margin: 0 auto;
}
.todoContainer .todoWrap{
padding:10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Header组件
<template>
<div class="todoHeader">
<input type="text" placeholder="请输入你的任务名称,并按回车确认" v-model="title" @keyup.enter="Add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name:'MyHeader',
data(){
return {
title:''
}
},
methods:{
Add(){
if(!this.title.trim())
{
alert("请输入选项")
}
else{
const todoObjTmp ={id: nanoid(), title:this.title,done:false};
this.receive(todoObjTmp);
this.title="";
}
}
},
props:["receive"]
}
</script>
<style scoped>
.todoHeader input{
width:560px;
height:28px;
font-size:14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
background: #ccc;
}
.todoHeader input:focus{
outline:none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
注意:nanoid是一个轻量级的生成uuid的库 npm i nanoid
Footer组件
<template>
<div class="todoFooter" v-show="total">
<label>
<input type="checkbox" v-model="isAllChecked"/>
<!-- <input type="checkbox" :checked="isAllChecked" @change="checkAll"/> -->
<span>已完成{{doneTotal}}</span> / 全部{{total}}
</label>
<button class="btn btn1" @click="clearAll">清除已完成的任务</button>
</div>
</template>
<script>
export default {
name:'MyFooter',
props:['todos','receiveCheckAll','receiveClearAll'],
methods:{
checkAll(event){
this.receiveCheckAll(event.target.checked)
},
clearAll(){
this.receiveClearAll();
}
},
computed:{
total(){
return this.todos.length;
},
isAllChecked:{
set(value)
{
this.receiveCheckAll(value)
},
get()
{
return this.total === this.doneTotal && this.total > 0
}
},
doneTotal(){
// let i=0;
// this.todos.forEach((t)=>{
// if(t.done)
// {
// ++i
// }
// })
// return i;
return this.todos.reduce((pre,current)=>{return pre + (current.done ? 1 : 0)},0)
}
}
}
</script>
<style scoped>
.todoFooter{
height:40px;
line-height: 40px;
padding-left: 6px;
margin-top: 4px;
}
.todoFooter label{
display:inline-block;
margin-right: 20px;
cursor:pointer;
}
.todoFooter label input{
position: relative;
top:-1px;
vertical-align: middle;
margin-right: 5px;
}
.todoFooter button{
float:right;
margin-top:5px;
}
</style>
List组件
<template>
<ul class="todoList">
<MyItem v-for="todoObj in todos" :key="todoObj.id"
:todo="todoObj" :checkFuc="receiveCheck" :deleteFunc="receiveDelete"/>
</ul>
</template>
<script>
import MyItem from './MyItem.vue';
export default {
name:'MyList',
components:{
MyItem,
},
props:['todos','receiveCheck','receiveDelete']
}
</script>
<style scoped>
.todoMain{
margin-left: 0px;
border:1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.toEmpty{
height:40px;
line-height:40px;
border:1px solid #ddd;
border-radius: 2px;
padding-left: 2px;
margin-top: 10px;
}
</style>
Item组件
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn1" @click="handleDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name:'MyItem',
props:['todo','checkFuc', 'deleteFunc'],
mounted(){
console.log(this.todo)
},
methods:{
handleCheck(id){
this.checkFuc(id);
},
handleDelete(id){
if(confirm("确定删除吗?")){
this.deleteFunc(id);
}
}
}
}
</script>
<style scoped>
li{
list-style: none;
height:36px;
line-height:36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label{
float:left;
cursor: pointer;
}
li label li input{
vertical-align: middle;
margin-right: 6px;
position: relative;
top:-1px;
}
li button{
float:right;
display: none;
margin-top: 3px;
}
li:before{
content:initial;
}
li:last-child{
border-bottom: none ;
}
li:hover{
background:#ddd;
}
li:hover button{
display:block
}
</style>
3. 案例解析
状态管理:
共享状态(todos)提升到最接近的共同祖先组件(App)
每个子组件只维护自己独立的状态
组件通信:
父→子:通过props传递数据(如todos)
子→父:通过自定义事件(如add-todo)
兄弟组件:通过共同父组件中转
最佳实践:
使用计算属性派生状态(activeCount)
为列表项设置唯一的key(基于id而非index)
使用scoped样式避免样式污染
六、总结与展望
通过本文的完整案例,我们实践了Vue组件化开发的完整流程:
拆分静态组件:按功能划分组件结构
设计数据流:确定状态存放位置
实现组件通信:props向下,事件向上
完善交互细节:处理用户输入和反馈