Vue组件化开发小案例

发布于:2025-07-20 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

组件化是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-ifv-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. 案例解析

  1. 状态管理

    • 共享状态(todos)提升到最接近的共同祖先组件(App)

    • 每个子组件只维护自己独立的状态

  2. 组件通信

    • 父→子:通过props传递数据(如todos)

    • 子→父:通过自定义事件(如add-todo)

    • 兄弟组件:通过共同父组件中转

  3. 最佳实践

    • 使用计算属性派生状态(activeCount)

    • 为列表项设置唯一的key(基于id而非index)

    • 使用scoped样式避免样式污染

六、总结与展望

通过本文的完整案例,我们实践了Vue组件化开发的完整流程:

  1. 拆分静态组件:按功能划分组件结构

  2. 设计数据流:确定状态存放位置

  3. 实现组件通信:props向下,事件向上

  4. 完善交互细节:处理用户输入和反馈


网站公告

今日签到

点亮在社区的每一天
去签到