Vue入门案例--TodoList备忘录

前言

在学习完成Spring Boot之后,狗子我便开始了 Vue 之旅,被迫走上前端的路子😤。在入门了 Vue 的组件之后,便打算用这一个简单的案例将之前 Vue 基础串联起来进行一个实践,若哪里有问题欢迎留言提出😁

image-20220405120228024

1、创建Vue项目

打开需要创建vue项目的目录,然后在路径栏中输入cmd进入控制台

image-20220405115408610

在控制台中输入vue create 项目名,选择 vue2 环境进行搭建。

注意的是,这条命令能够运行的前提是 node 环境已经搭建好并且是脚手架已经安装好了的才可以,还没有配置的小伙伴自行去查安装的教程噢,这里就不写出来了。

image-20220405115637761

使用 IDE (狗子我用的是VsCode)打开这个todo-list文件夹,这时我们的项目已经创建完毕了

这里吐槽一点,个人觉得这样子创建不太方便,然后目前还没有找到一个比较好的创建方式,就只能这样子玩了,可能是学的还不够。

2、搭建项目

在创建完成项目之后需要定下基本的框架,即分析好可以拆分成哪些组件,在这里选择的是拆成四个组件,对应下图分别是

  • 红:MyHeader,用于获取输入的任务内容
  • 绿:MyList,用于整合输入的一条条数据
  • 蓝:MyItem,用于存放每一条输入的数据
  • 粉:MyFooter,用于展示底部对全部数据进行操作的内容

image-20220405120507464

2.1、MyHeader编写

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

<script>
    export default {
        name: 'MyHeader',
    }
</script>

<style scoped>
    /*header*/
	.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>

2.2、MyList编写

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

<script>
    import MyItem from './MyItem';

    export default {
        name: 'MyList',
        components: {
            MyItem
        },
    }
</script>

<style scoped>
    /*main*/
	.todo-main {
		margin-left: 0px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding: 0px;
	}

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

2.3、MyItem编写

<template>
    <li>
		<label>
			<input type="checkbox"/>
			<span>xxxxxx</span>
		</label>
		<button class="btn btn-danger">删除</button>
	</li>
</template>

<script>
    export default {
        name: 'MyItem',
    }
</script>

<style scoped>
    /*item*/
	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: #ddd;
	}
	
	li:hover button{
		display: block;
	}
</style>

2.4、MyFooter编写

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

<script>
export default {
    name: 'MyFooter',
}
</script>

<style scoped>
    /*footer*/
	.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>

2.5、App编写

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

<script>
  import MyHeader from "./components/MyHeader";
  // 因为MyItem是MyList中的组件,因此在App组件中不需要再引入MyItem组件
  import MyList from "./components/MyList";
  import MyFooter from './components/MyFooter';
  export default {
    name: 'App',
    components: {MyHeader, MyList, MyFooter}
  }
</script>

<style>
  /*base*/
	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>

这时项目的基本架构就已经搭建好了,目录如下

image-20220405121540044

3、初始化列表

在这里我们就开始需要思考一些事情了,我们输入的数据要存放到哪里呢?这个数据模型又是什么结构呢是吧,这些都值得我们去考虑一番。

  • 数据存放按照正常的想法,应该是要放在 MyListy 组件中的对吧,开发中也确实是这样子。但是这也迎来了一个新问题,到时候我们的输入框肯定也要操作这个数据的,这又怎么从 MyList 中传递数据到 MyHeader 中去呢?因为这两个数据同级组件,目前狗子我还没有能力在这两个之间进行传递。
    • 这时就想到了一个中间人,他们的老爹:App,将数据存放到App中,然后通过父亲当中间人将数据分别传递给各个儿子,毕竟爸爸拥有的东西,儿子们也得拥有是吧。
  • 数据模型由于存放的是输入的内容,因此很容易想到:哎呀,设置一个content存放数据,再来一个done来判断是否完成不久好了嘛。这时问题又来了,那我们到时候全选以及删除用什么做标识呢是吧。
    • 因此还需要设置一个id,开发中一般使用数据库中的自增设置id,但是这里狗子还没弄数据持久层,因此就需要手动给它亲手配上。这时数据模型就定下来了:{id: '001', content: '去植发', done: false}

3.1、App定义数据

在这里没把样式在这里贴上,在定义好数据模型之后不要忘记给 MyList 用v-bind绑定一手,将参数丢过去

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

<script>
  import MyHeader from "./components/MyHeader";
  // 因为MyItem是MyList中的组件,因此在App组件中不需要再引入MyItem组件
  import MyList from "./components/MyList";
  import MyFooter from './components/MyFooter';
  export default {
    name: 'App',
    components: {MyHeader, MyList, MyFooter},
    data() {
      return {
        //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
        todos: [
          {id: '001', content: '掉头发', done: true},
          {id: '002', content: '去植发', done: false},
          {id: '003', content: '去护发', done: false}
        ]
      }
    }
  }
</script>

3.2、MyList处理数据

在这里通过传递过来的todos决定需要展示多少条MyItem,记住记住,v-for中要加上:key,否则会报错。

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

<script>
    import MyItem from './MyItem';

    export default {
        name: 'MyList',
        // //声明接收App传递过来的数据
        props: ['todos'],
        components: {
            MyItem
        }
    }
</script>

3.3、MyItem数据展示

在MyItem中要对传递过来的每一条todo展示到页面上去

<template>
    <li>
		<label>
			<input type="checkbox" :checked="todo.done"/>
			<span>{{todo.content}}</span>
		</label>
		<button class="btn btn-danger">删除</button>
	</li>
</template>

<script>
    export default {
        name: 'MyItem',
        props: ['todo']
    }
</script>

3.4、结果演示

image-20220405142156465

4、新增数据

在新增数据中估计最麻烦的就是id的数据怎么创建,有人可能会说了,刚刚不是到003了嘛,我给他写上一个004不就好了。嗯嗯嗯……好像对了,又好像没完全对,插入到第四条的时候确实是这样子没错,但是如果后面还需要插入的话岂不是每个id都是004了嘛,这就违反了id的定义了。

  • 这时,有一个很有用的东西它蹦出来了——UUID,这是生成一个通用唯一识别码的插件,但是它有个缺点,就是太大了,在这里用它有点杀猪用牛刀的感觉,因此下面狗子我用的是它的崽——nanoid。在控制台中输入npm i nanoid即可安装,轻量便捷,你还在等什么!(是时候打🧧了)

image-20220405145023125

4.1、App定义方法

在这里需要定义一个方法addTodo接收MyHeader传递过来的todoObj对象插入到todos数组中,问题来了,之前都是老爹传东西给儿子,现在儿子怎么传参到老爹这边来呢?

  • 解决办法就是将定义的addTodo先传递过去给MyHeader,让MyHeader自己调用这个方法使得App中的数据数据可以得到更新。(为了突出方法把其余和前面相同的代码删除了)
<template>
  <div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader :addTodo="addTodo"/>
				<MyList :todos="todos"/>
				<MyFooter/>
			</div>
		</div>
	</div>
</template>

<script>
  export default {
    methods: {
      // 新增数据
      addTodo(todoObj) {
        this.todos.unshift(todoObj);
      }
    }
  }
</script>

4.2、MyHeader获取数据并插入

在这里除了需要接收App传递过来的方法,自己还需要引入前面说到的nanoid以及拥有自己的content属性用来绑定输入框。

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

<script>
    import {nanoid} from 'nanoid';
    export default {
        name: 'MyHeader',
        props: ['addTodo'],
        data() {
            return {
                content: ''
            }
        },
        methods: {
            add() {
                // 校验数据
                if (!this.content.trim()) {
                    return alert('输入不能为空');
                }
                // 将用户的输入封装成一个todo对象
                const todoObj = {id:nanoid(), content: this.content, done: false};
                // 通知App组件去添加一个todo对象
                this.addTodo(todoObj);
                // 清空输入框
                this.content = '';
            }
        }
    }
</script>

4.3、结果演示

在输入框中输入test,按下回车后会发现成功实现了新增的功能。

image-20220405145941080

5、勾选和删除

原本这两个想着分开写的,但是发现这两个的逻辑基本一样,就放到了一起,都是在App组件中定义好勾选以及删除的方法,然后通过v-bind进行传递到MyItem组件中去

5.1、App定义方法

同样为了突出这一模块写的方法,隐藏掉了和之前重复的代码

<template>
  <div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader :addTodo="addTodo"/>
				<MyList 
                  :todos="todos" 
                  :checkTodo="checkTodo"
                  :delectTodo="delectTodo"
                />
				<MyFooter/>
			</div>
		</div>
	</div>
</template>

<script>
  export default {
    methods: {
      // 勾选或取消单个todo
      checkTodo(id) {
        this.todos.forEach((todo) => {
          if (todo.id === id) {
            todo.done = !todo.done;
          }
        })
      },
      // 删除单个todo
      delectTodo(id) {
        this.todos = this.todos.filter(todo => todo.id !== id);
      }
    }
  }
</script>

5.2、MyList中转

勾选和删除因为都是MyItem要使用,因此要借助MyList进行中转

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

<script>
    import MyItem from './MyItem';

    export default {
        name: 'MyList',
        // //声明接收App传递过来的数据
        props: ['todos', 'checkTodo', 'delectTodo'],
        components: {
            MyItem
        }
    }
</script>

5.3、MyItem调用方法

这里要注意的就是需要将id值作为实参进行传参。

到这里有小伙伴可能会说了,这里用v-model直接修改todos数组中的数据不就好了嘛,那用得了这么麻烦。确实这样子很简单明了,但是有一个很大的问题,这违反了vue中props的原则,即我们不能去修改props中的值,这会造成不可描述的意外错误。

<template>
    <li>
		<label>
			<input type="checkbox" :checked="todo.done" @click="checkTodo(todo.id)"/>
			<span>{{todo.content}}</span>
		</label>
		<button class="btn btn-danger" @click="delectTodo(todo.id)">删除</button>
	</li>
</template>

<script>
    export default {
        name: 'MyItem',
        props: ['todo', 'checkTodo', 'delectTodo']
    }
</script>

5.4、结果演示

点亮“去护发”以及点击删除“掉头发”会发现成功实现了这两个功能

image-20220405200432264

6、底部交互实现

底部主要是一个全选按钮以及删除选中项的功能,主要也是和上面的勾选和删除差不多,直接上代码

6.1、App定义方法

<template>
  <div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader :addTodo="addTodo"/>
				<MyList 
          :todos="todos" 
          :checkTodo="checkTodo"
          :delectTodo="delectTodo"
        />
				<MyFooter
          :todos="todos"
          :checkAllTodos="checkAllTodos"
          :delectCheckedTodos="delectCheckedTodos"
        />
			</div>
		</div>
	</div>
</template>

<script>
  export default {
    methods: {
      // 全选或全不选todos
      checkAllTodos(done) {
        this.todos.forEach((todo) => {
          todo.done = done;
        })
      },
      // 删除选中的所有项
      delectCheckedTodos() {
        this.todos = this.todos.filter(todo => !todo.done);
      }
    }
  }
</script>

6.2、MyFooter处理展示数据

<template>
	<!-- 如果没有数据,这一块div就不展示 -->
    <div class="todo-footer" v-show="total">
		<label>
			<input type="checkbox" v-model="isAll"/>
		</label>
		<span>
			<span>已完成{{checkedTotal}}</span> / 全部{{total}}
		</span>
		<button class="btn btn-danger" @click="delectCheckedTodos">清除已完成任务</button>
	</div>
</template>

<script>
export default {
    name: 'MyFooter',
    props: ['checkAllTodos', 'delectCheckedTodos', 'todos'],
    computed: {
        // 总数
        total() {
            return this.todos.length;
        },
        // 选中的个数
        checkedTotal() {
            return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
        },
        isAll: {
            get() {
                // 当勾选中的个数和总数相等且总数大于零时才checked
                return this.checkedTotal === this.total && this.total > 0;
            },
            set(value) {
                this.checkAllTodos(value);
            }
        }
    }
}
</script>

7、总结

  1. 组件化编码流程:

    ​ (1). 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。

    ​ (2). 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

    ​ 1). 一个组件在用:放在组件自身即可。

    ​ 2). 一些组件在用:放在他们共同的父组件上(状态提升)。

    ​ (3). 实现交互:从绑定事件开始。

  2. props适用于:

    ​ (1). 父组件 ==> 子组件 通信

    ​ (2). 子组件 ==> 父组件 通信(要求父先给子一个函数)

  3. 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

  4. props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

搞定下班!!!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈宝子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值