首页 前端知识 通过Vue小型项目todoList清单搞懂组件化编码流程,建议收藏

通过Vue小型项目todoList清单搞懂组件化编码流程,建议收藏

2024-06-20 00:06:58 前端知识 前端哥 309 836 我要收藏

前言

最近学习了Vue,跟着教程完成了一个简单的demo—todoList清单,功能主要有添加、删除待办事件,统计已完成的事件数,一键清空列表等,运用了组件化的编码流程,巩固了父子组件传值的知识点

最终得到的效果如下
在这里插入图片描述

一、静态页面

根据功能拆分页面,项目的组织结构如下

在这里插入图片描述

App.vue文件

<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader/>
        <MyList/>
        <MyFooter/>
      </div>
    </div>
  </div>
</template>

<script>
// 引入组件
import MyHeader from "@/components/MyHeader";
import MyList from "@/components/MyList";
import MyFooter from "@/components/MyFooter";
export default {
  name: 'app',
  components:{MyList, MyHeader,MyFooter} // 注册
}
</script>

<style lang="less">
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;
}
.btn-danger{
  color: #fff;
  background-color:#da4f49;
  border: 1px solid #bd362f;
}
.btn-danger:hover{
  color: #fff;
  background-color: #bd362f;
}
.btn:focus{
  outline:none;
}
.todo-container{
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap{
  padding: 10px;
  border:1px solid #ddd;
  border-radius: 5px;
}
</style>

输入框部分拆分为头部组件
MyHeader.vue文件

<template>
  <div class="todo-header">
    <input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
  </div>
</template>

<script>
export default {
  data(){
    return{
    };
  },
  methods:{
  }
};
</script>

<style lang="less" scoped>
.todo-header input{
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}
.todo-header 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>

事件列表的每一行拆分为一个组件,因为每一行的结构相同,如果有多行,就可以在其父组件里面通过 v-for 指令遍历
MyItem.vue文件

<template>
  <li>
    <label>
      <input type="checkbox"/>
      <span>吃饭</span>
    </label>
    <button class="btn btn-danger" >删除</button><!--style="display:none"-->
  </li>
</template>

<script>
export default {
  name: "MyItem",
  data(){
    return{
    }
  }
}
</script>

<style lang="less" 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;
}
</style>

事件列表拆分为一个组件,作为MyItem.vue组件的父组件
MyList.vue文件

<template>
  <ul class="todo-main">
    <MyItem/>
    <MyItem/>
  </ul>
</template>

<script>
// 引入子组件
import MyItem from "@/components/MyItem";
export default {
  name: "MyList",
  components:{MyItem} // 注册子组件
}
</script>

<style scoped>
.todo-main{
  margin-left: 0;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0;
}
.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
</style>

底部统计与全选功能部分拆分为一个组件
MyFooter.vue文件

<template>
  <div class="todo-footer">
    <label>
      <input type="checkbox"/>
    </label>
    <span>
          <span>已完成0</span> / 全部
    </span>
    <button class="btn btn-danger">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: "MyFooter"
}
</script>

<style scoped>
.todo-footer{
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}
.todo-footer label{
  display: inline-block;
  margin-right: 20px;
  cursor:pointer;
}
.todo-footer label input{
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}
.todo-footer button{
  float: right;
  margin-top: 5px;
}
</style>

效果图如下

在这里插入图片描述

二、绑定动态数据

1、数据存储的方式

一堆要做的事情是一个数组,一个个要做的事情是对象。因此可以使用数组来存储多个要做的事情,每个事情是一个对象,包含属性id(标识)、title(名称)、done(是否完成)。

因为在MyList.vue组件中展示数据,所以该数组写在MyList文件中。

data(){
    return{
      todos:[
        {id:'001',title:'吃饭',done:true},
        {id:'002',title:'写代码',done:false},
        {id:'003',title:'看电视',done:false}
      ]
    }
  }

2、数据展示方式

因为存储事情的数组todos在MyList中,而每一个事情todoObj在MyItem中展示,所以需要把每一个事情传递到MyItem文件中,相当于父组件给子组件传值,用到props。

MyList文件中
使用 v-for 遍历todos数组, :todo=“todoObj” 将每一个事情传到MyItem组件

<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>

MyItem文件中
使用props接收父组件传递的数据
props:[‘todo’],接收父组件传递的每一个todoObj,注意是 todo

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done"/><!--checked动态绑定todo的done属性,判断事情是否完成(完成打钩) -->
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" >删除</button><!--style="display:none"-->
  </li>
</template>

<script>
export default {
  name: "MyItem",
  props:['todo'],//接收父组件传递的每一个todoObj
  data(){
    return{
    }
  }
}

3、添加数据

每一个事件需要有一个唯一标识即id,这样才能通过标识来删除和选择该事件
id生成方法,使用 nanoid JavaScript库
node.js安装 nanoid 的方式

npm install nanoid

引入nanoid

import {nanoid} from 'nanoid'

使用方式

    add(e){
      const obj={id:nanoid(),title:e.target.value,done:false};
      console.log(obj);
    }

在这里插入图片描述

可以看到,生成的id是唯一的。

输入的待办事件需要从MyHeader组件传到MyList组件中,涉及兄弟组件传值
兄弟组件传值的方法有三种

  1. 使用vuex,把数据放在store仓库中,这样任意组件都可以获取数据
  2. 使用bus总线传值
  3. 子组件1–>父组件–>子组件2

下面使用方法3,比较通俗易懂,实现起来也比较简单,但是如果有很多数据需要传递,则建议使用vuex

(1)修改为父子组件传值

这里将保存事件的数组放到App.vue文件中,转为父子组件传值方式,即组件MyHeader传输入的待办事件给父组件App,App再传给子组件MyList

App.vue文件修改为

<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader :addTodo="addTodo"/><!--传递方法 -->
        <MyList :todos="todos"/>
        <MyFooter/>
      </div>
    </div>
  </div>
</template>

<script>
import MyHeader from "@/components/MyHeader";
import MyList from "@/components/MyList";
import MyFooter from "@/components/MyFooter";
export default {
  name: 'app',
  components:{MyList, MyHeader,MyFooter},
  data(){
    return{
      todos:[
        {id:'001',title:'吃饭',done:true},
        {id:'002',title:'写代码',done:false},
        {id:'003',title:'看电视',done:false}
      ]
    };
  },
  methods:{
      //添加事件对象
    addTodo(todoObj){
      this.todos.unshift(todoObj);//unshift添加到数组首位
    }
  }
}
</script>

MyList文件修改为

<template>
  <ul class="todo-main">
    <MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
  </ul>
</template>

<script>
import MyItem from "@/components/MyItem";
export default {
  name: "MyList",
  components:{MyItem},
  props:['todos'],
  data(){
    return{
    }
  }
}
</script>

MyHeader文件修改为

<template>
  <div class="todo-header">
    <!--绑定键盘事件keyup.enter -->
    <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
  </div>
</template>

<script>
import {nanoid} from 'nanoid'
export default {
  name:'MyHeader',
  props:['addTodo'], // 接受父组件传递的方法
  data(){
    return{
    };
  },
  methods:{
    add(e){
      // 校验输入的内容
      if(e.target.value.trim() === ''){
        alert('输入不能为空')
        return;
      }
      // 将用户的输入包装成一个对象
      const todoObj={id:nanoid(),title:e.target.value,done:false};
      // 调用父组件传过来的函数,往数组中添加一个事件对象
      this.addTodo(todoObj);
      e.target.value = '';// 添加完后将输入框清空
    }
  }
};
</script>

4、勾选或取消勾选

涉及多层组件传值:App—>MyList —>MyItem

App文件中添加方法

// 勾选或取消勾选
    checkTodo(id){
      this.todos.forEach((todo)=>{
        if(todo.id === id)
          todo.done = !todo.done;
      })
    }

传给MyList

<MyList :todos="todos" :checkTodo="checkTodo"/>

MyList中用props接收后再传给MyItem,MyItem接收

<input type="checkbox" :checked="todo.done" @click="handleTodo(todo.id)"/>
methods:{
    handleTodo(id){
      this.checkTodo(id);//调用父组件传递的checkTodo方法
    }
  }

详细代码,后面附上。

5、删除事件

控制删除按钮显示与隐藏的css代码

li button{
  float: right;
  display: none;
  margin-top: 3px;
}
li:hover button{
  display: block;// 控制删除按钮显示(鼠标悬浮在该li上)与隐藏
}

类似于勾选或取消勾选,都是多层组件传值。

App文件中添加方法

// 删除一个事件
    deleteTodo(id){
      // filter方法会生成一个新数组,所以需要重新给todos赋值
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id;// 等于该id的会被过滤掉
      })
    }

将deleteTodo方法先传给MyList文件,在从MyList文件传到MyItem文件。

MyItem文件中button绑定click事件

<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
handleDelete(id){
      this.deleteTodo(id);//调用父组件传递的方法
    }

6、底部统计

在这里插入图片描述

分为两部分

(1)统计已完成的事件数和事件总数

使用计算属性computed完成

<span>已完成{{doneTotal}}</span> / 全部{{total}}
computed:{
    doneTotal(){
      // 方法1,使用计数器
      /*let cnt = 0;
      this.todos.forEach(todo =>{
        if(todo.done)cnt++;
      })
      return cnt;*/

      // 方法2,使用数组reduce方法
      return this.todos.reduce((pre,todo)=>{
        return pre + (todo.done ? 1:0);
      },0)
    },
    total(){
      return this.todos.length;
    },
    isAll(){
      return this.total === this.doneTotal && this.total>0 //保证总数为0时不勾选
    }
  },
(2)勾选全部

父组件App中传递checkAllTodo方法给MyFooter子组件

<label>
      <input type="checkbox" :checked="isAll" @click="checkAll"/>
</label>
// 全选或全不选
    checkAllTodo(done){
      this.todos.forEach(todo =>{
        todo.done=done;
      })
    },
checkAll(e){
      this.checkAllTodo(e.target.checked);//传入的是checked属性,不是value
    },
(3)清除已完成的事件

父子组件传函数clearAllTodo

<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
// 删除已完成的所有事件
    clearAllTodo(){
      this.todos = this.todos.filter(todo =>{
        return !todo.done;// 过滤掉done值为true的事件,留下done为false的事件
      })
    }
clearAll(){
      this.clearAllTodo();
    }

当没有事件时,显示效果应为

在这里插入图片描述

使用v-show控制标签显示与隐藏

<div class="todo-footer" v-show="total">

三、总结

1、组件化编码流程

(1)拆分静态组件

拆分页面,抽取组件,组件要按功能点拆分,命名不要与HTML标签重名。

(2)实现动态组件

首先要考虑好数据的存放位置,数据是一个组件再用,还是多个组件同时用。

  • 一个组件在用:放在该组件即可
  • 多个组件共用:放在他们的共同父组件里(状态提升)
  • 全局组件:如果某个组件不涉及路由切换,就可以注册为全局组件,然后在其他组件中不需要引入即可使用该全局组件,比如导航栏就可以注册为全局组件

然后判断数据该使用哪种数据类型来保存

  • 数据是单一数据,只有一个值,可以使用字符串、数字、布尔型存储
  • 数据有多个值,每个值没有其他属性,可以使用数组存储
  • 数据有多个属性,但只有一个值,可以使用对象来存储
  • 数据既有多个值,每个值又有多个属性,可以使用数组存储多个值,每个值为一个对象
(3)实现交互效果

给标签绑定事件函数,编写对应的业务逻辑代码

2、props使用范围

  1. 父组件 —> 子组件 通信
  2. 子组件 —> 父组件 通信(要求父组件先传给子组件一个函数)

3、其他注意点

  1. 使用v-model时,绑定的值不能是props传过来的的值,因为props是不可以被修改的;
  2. props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但是不推荐这样做。

四、全部代码

(1)App.vue
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader :addTodo="addTodo"/>
        <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
        <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
      </div>
    </div>
  </div>
</template>

<script>
import MyHeader from "@/components/MyHeader";
import MyList from "@/components/MyList";
import MyFooter from "@/components/MyFooter";
export default {
  name: 'app',
  components:{MyList, MyHeader,MyFooter},
  data(){
    return{
      todos:[
        {id:'001',title:'吃饭',done:true},
        {id:'002',title:'写代码',done:false},
        {id:'003',title:'看电视',done:false}
      ]
    };
  },
  methods:{
    // 添加一个事件对象
    addTodo(todoObj){
      this.todos.unshift(todoObj);
    },
    // 勾选或取消勾选
    checkTodo(id){
      this.todos.forEach((todo)=>{
        if(todo.id === id)
          todo.done = !todo.done;
      })
    },
    // 删除一个事件
    deleteTodo(id){
      // filter方法会生成一个新数组,所以需要重新给todos赋值
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id;// 等于该id的会被过滤掉
      })
    },
    // 全选或全不选
    checkAllTodo(done){
      this.todos.forEach(todo =>{
        todo.done=done;
      })
    },
    // 删除已完成的所有事件
    clearAllTodo(){
      this.todos = this.todos.filter(todo =>{
        return !todo.done;// 过滤掉done值为true的事件,留下done为false的事件
      })
    }
  }
}
</script>

<style lang="less">
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;
}
.btn-danger{
  color: #fff;
  background-color:#da4f49;
  border: 1px solid #bd362f;
}
.btn-danger:hover{
  color: #fff;
  background-color: #bd362f;
}
.btn:focus{
  outline:none;
}
.todo-container{
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap{
  padding: 10px;
  border:1px solid #ddd;
  border-radius: 5px;
}
</style>

(2)MyHeader.vue
<template>
  <div class="todo-header">
    <!--绑定键盘事件keyup.enter -->
    <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
  </div>
</template>

<script>
import {nanoid} from 'nanoid'
export default {
  name:'MyHeader',
  props:['addTodo'],
  methods:{
    add(e){
      // 校验输入的内容
      if(e.target.value.trim() === ''){
        alert('输入不能为空')
        return;
      }
      // 将用户的输入包装成一个对象
      const todoObj={id:nanoid(),title:e.target.value,done:false};
      // 调用父组件传过来的函数,往数组中添加一个事件对象
      this.addTodo(todoObj);
      e.target.value = '';// 添加完后将输入框清空
    }
  }
};
</script>

<style lang="less" scoped>
.todo-header input{
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}
.todo-header 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>
(3)MyList.vue
<template>
  <ul class="todo-main">
    <MyItem v-for="todoObj in todos"
            :key="todoObj.id"
            :todo="todoObj"
            :checkTodo="checkTodo"
            :deleteTodo="deleteTodo"/>
  </ul>
</template>

<script>
import MyItem from "@/components/MyItem";
export default {
  name: "MyList",
  components:{MyItem},
  props:['todos','checkTodo','deleteTodo'],
}
</script>

<style scoped>
.todo-main{
  margin-left: 0;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0;
}
.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
</style>
(4)MyItem.vue
<template>
  <li>
    <label>
      <!--checked动态绑定todo的done属性,判断事情是否完成(完成打钩) -->
      <input type="checkbox" :checked="todo.done" @click="handleTodo(todo.id)"/>
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
  </li>
</template>

<script>
export default {
  name: "MyItem",
  props:['todo','checkTodo','deleteTodo'],
  methods:{
    handleTodo(id){
      this.checkTodo(id);
    },
    handleDelete(id){
      this.deleteTodo(id);
    }
  }
}
</script>

<style lang="less" 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-color: #dddddd;
}
li:hover button{
  display: block;// 控制删除按钮显示(鼠标悬浮在该li上)与隐藏
}
</style>
(5)MyFooter.vue
<template>
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" :checked="isAll" @click="checkAll"/>
    </label>
    <span>
          <span>已完成{{doneTotal}}</span> / 全部{{total}}
    </span>
    <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: "MyFooter",
  props:['todos','checkAllTodo','clearAllTodo'],
  computed:{
    doneTotal(){
      // 方法1,使用计数器
      /*let cnt = 0;
      this.todos.forEach(todo =>{
        if(todo.done)cnt++;
      })
      return cnt;*/

      // 方法2,使用数组reduce方法
      return this.todos.reduce((pre,todo)=>{
        return pre + (todo.done ? 1:0);
      },0)
    },
    total(){
      return this.todos.length;
    },
    isAll(){
      return this.total === this.doneTotal && this.total>0
    }
  },
  methods:{
    checkAll(e){
      this.checkAllTodo(e.target.checked);
    },
    clearAll(){
      this.clearAllTodo();
    }
  }
}
</script>

<style scoped>
.todo-footer{
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}
.todo-footer label{
  display: inline-block;
  margin-right: 20px;
  cursor:pointer;
}
.todo-footer label input{
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}
.todo-footer button{
  float: right;
  margin-top: 5px;
}
</style>
(6)效果展示

勾选删除单个事件
在这里插入图片描述
一键全选事件
在这里插入图片描述
没有事件时的效果
在这里插入图片描述

转载请注明出处或者链接地址:https://www.qianduange.cn//article/12906.html
标签
评论
发布的文章

jQuery实现简单抽奖大转盘

2024-07-01 23:07:44

jQuery思维导图

2024-07-01 23:07:43

在jQuery中添加表格行

2024-07-01 23:07:36

jquery数据类型转换

2024-07-01 23:07:36

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!