文章目录
前言
在学习完成Spring Boot之后,狗子我便开始了 Vue 之旅,被迫走上前端的路子😤。在入门了 Vue 的组件之后,便打算用这一个简单的案例将之前 Vue 基础串联起来进行一个实践,若哪里有问题欢迎留言提出😁
1、创建Vue项目
打开需要创建vue项目的目录,然后在路径栏中输入cmd进入控制台
在控制台中输入vue create 项目名
,选择 vue2 环境进行搭建。
注意的是,这条命令能够运行的前提是 node 环境已经搭建好并且是脚手架已经安装好了的才可以,还没有配置的小伙伴自行去查安装的教程噢,这里就不写出来了。
使用 IDE (狗子我用的是VsCode)打开这个todo-list
文件夹,这时我们的项目已经创建完毕了
这里吐槽一点,个人觉得这样子创建不太方便,然后目前还没有找到一个比较好的创建方式,就只能这样子玩了,可能是学的还不够。
2、搭建项目
在创建完成项目之后需要定下基本的框架,即分析好可以拆分成哪些组件,在这里选择的是拆成四个组件,对应下图分别是
- 红:MyHeader,用于获取输入的任务内容
- 绿:MyList,用于整合输入的一条条数据
- 蓝:MyItem,用于存放每一条输入的数据
- 粉:MyFooter,用于展示底部对全部数据进行操作的内容
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>
这时项目的基本架构就已经搭建好了,目录如下
3、初始化列表
在这里我们就开始需要思考一些事情了,我们输入的数据要存放到哪里呢?这个数据模型又是什么结构呢是吧,这些都值得我们去考虑一番。
- 数据存放按照正常的想法,应该是要放在 MyListy 组件中的对吧,开发中也确实是这样子。但是这也迎来了一个新问题,到时候我们的输入框肯定也要操作这个数据的,这又怎么从 MyList 中传递数据到 MyHeader 中去呢?因为这两个数据同级组件,目前狗子我还没有能力在这两个之间进行传递。
- 这时就想到了一个中间人,他们的老爹:App,将数据存放到App中,然后通过父亲当中间人将数据分别传递给各个儿子,毕竟爸爸拥有的东西,儿子们也得拥有是吧。
- 数据模型由于存放的是输入的内容,因此很容易想到:哎呀,设置一个content存放数据,再来一个done来判断是否完成不久好了嘛。这时问题又来了,那我们到时候全选以及删除用什么做标识呢是吧。
- 因此还需要设置一个id,开发中一般使用数据库中的自增设置id,但是这里狗子还没弄数据持久层,因此就需要手动给它亲手配上。这时数据模型就定下来了:
{id: '001', content: '去植发', done: false}
- 因此还需要设置一个id,开发中一般使用数据库中的自增设置id,但是这里狗子还没弄数据持久层,因此就需要手动给它亲手配上。这时数据模型就定下来了:
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、结果演示
4、新增数据
在新增数据中估计最麻烦的就是id的数据怎么创建,有人可能会说了,刚刚不是到003了嘛,我给他写上一个004不就好了。嗯嗯嗯……好像对了,又好像没完全对,插入到第四条的时候确实是这样子没错,但是如果后面还需要插入的话岂不是每个id都是004了嘛,这就违反了id的定义了。
- 这时,有一个很有用的东西它蹦出来了——UUID,这是生成一个通用唯一识别码的插件,但是它有个缺点,就是太大了,在这里用它有点杀猪用牛刀的感觉,因此下面狗子我用的是它的崽——nanoid。在控制台中输入
npm i nanoid
即可安装,轻量便捷,你还在等什么!(是时候打🧧了)
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,按下回车后会发现成功实现了新增的功能。
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、结果演示
点亮“去护发”以及点击删除“掉头发”会发现成功实现了这两个功能
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). 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
(2). 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
1). 一个组件在用:放在组件自身即可。
2). 一些组件在用:放在他们共同的父组件上(状态提升)。
(3). 实现交互:从绑定事件开始。
-
props适用于:
(1). 父组件 ==> 子组件 通信
(2). 子组件 ==> 父组件 通信(要求父先给子一个函数)
-
使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
-
props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。
搞定下班!!!