Vue2.x 项目实战(一)
文章目录
Vue2.x 实现 todoList
1、前言
如果你对 vue 的基础知识还很陌生,推荐先去学习一下 vue 基础
内容参考链接Vue2.x全家桶Vue2.x全家桶参考链接
- 如果你 刚学完 vue 基础知识,想检查一下自己的学习成果
- 如果你 已学完 vue 基础知识,想快速回顾复习
- 如果你 已精通 vue 基础知识,想做个小案例
- 那不妨看完这篇文章,我保证你一定会有收获的!
2、项目演示(一睹为快)
todoList 项目演示
3、涉及知识点
- Vue基础:插值语法,常用指令,键盘事件,列表渲染,计算属性,事件监听,生命周期
- Vue进阶:props(父传子),自定义事件(任意组件间通信),自定义事件的解绑,$nextTick 异步
- 本地存储:任务记录保留在当前浏览器中,长期有效(不手动销毁则一直保留)
- 第三方库:nonoid(下载导入即可使用)
备注:
- 任意组件间的通信方式有很多种(全局事件总线,消息订阅预发布…),熟练掌握一种即可(推荐自定义事件,配置简单,容易理解)
- 本文是 vue 基础的练习项目,不涉及 vue 周边(Vuex,Vue-router)
4、项目详情(附源码及解析)
该项目有 五个组件 构成:
(1)App.vue 父组件,以上四个子组件 最终归并的地方,并实现很多功能相关方法
(2)MyHeader.vue 子组件:头部,用于用户文本框 输入添加任务事项
(3)MyList.vue 子组件:躯干,用于 呈现任务的列表
(4)MyItem.vue 子中子组件,Mylist.vue 的子组件,用于 呈现每个任务及编辑删除
(5)MyFooter 子组件,用于 显示所选个数和总个数及删除已完成任务
App.vue 父组件
- 所有子组件的汇集点
- 里面定义里很多方法,通过 props 父传子,供子组件们去使用
- 当然也有自定义事件,供子给父传值,进行页面的渲染更新
<template><!-- 最外层容器 --><div class="todo-container"><div class="todo-wrap"><!-- 头部子组件,子传父,自定义 addTodo事件,添加一个 todo对象 --><MyHeader @addTodo="addTodo"/><!-- 任务列表子组件,父传子,动态绑定对应事件 --><MyList :updateTodo="updateTodo":todos="todos":checkTodo="checkTodo":deleteTodo="deleteTodo"/><!-- 底部子组件,子传父,全选和全清除 --><MyFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"/></div></div></template><script>// 引入所需组件import MyHeader from"./components/MyHeader.vue";import MyList from"./components/MyList.vue";import MyFooter from"./components/MyFooter.vue";exportdefault{name:"App",components:{ MyHeader, MyList, MyFooter },data(){return{// 由于 todos 是 MyHeader 组件 和 MyFooter 组件都在用,所以放在APP中(状态提升)// 解析 JSON字符串 第一次使用时 null 身上没有 length 属性会报错,所以添加||,前面不能用时,置为空数组// localStorage.getItem("xxx") 用于从本地存储中读取 todostodos:JSON.parse(localStorage.getItem("todos"))||[],};},methods:{// 添加一个 todoaddTodo(todoObj){this.todos.unshift(todoObj);},// 勾选 or 取消勾选一个todocheckTodo(id){this.todos.forEach((todo)=>{if(todo.id === id) todo.done =!todo.done;});},// 更新一个 todoupdateTodo(id, title){this.todos.forEach((todo)=>{if(todo.id === id) todo.title = title;});},// 删除,todo.id !== id 就不会 push 该 todo,即删除deleteTodo(id){this.todos =this.todos.filter((todo)=> todo.id !== id);},// 全选 or 取消全选checkAllTodo(done){this.todos.forEach((todo)=>{
todo.done = done;});},// 清除所有已经完成的todoclearAllTodo(){this.todos =this.todos.filter((todo)=>{return!todo.done;});},},watch:{todos:{// 深度监视 检测到是否被勾选deep:true,handler(value){// localStorage.setItem("xxx") 用来添加 todo// 格式化为 JSON 字符串
localStorage.setItem("todos",JSON.stringify(value));},},},// 销毁前进行自定义事件的解绑beforeDestroy(){this.$off(['addTodo','checkAllTodo','clearAllTodo'])}};</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 0rgba(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-edit {
margin-right: 5px;
background-color: skyblue;border: 1px solid rgb(102,158,180);}.btn-danger:hover {color: #fff;
background-color: #bd362f;}.btn:focus {outline: none;}.todo-container {width: 600px;margin: 10px auto;}.todo-container .todo-wrap {padding: 10px;border: 1px solid #ddd;
border-radius: 5px;}</style>
MyHeader.vue 组件
- 终端键入
npm i nanoid
,安装 nanoid <style>
标签里的 scoped,表示里面定义的样式 仅在当前组件中生效
<template><div class="todo-header"><!-- 双向数据绑定 title,绑定键盘 enter 键,点击触发 add 事件,添加 title --><input
type="text"
placeholder="请输入你的任务名称,按回车键确认"
v-model="title"
@keyup.enter="add"/></div></template><script>import{ nanoid }from"nanoid";exportdefault{name:"MyHeader",data(){return{// 要输入的任务事项title:"",};},methods:{add(){// 校验数据if(!this.title.trim())returnalert("输入不能为空");// 将用户的输入包装成为一个 todo 对象,nanoid() 是随机生成的唯一值,默认为未完成事件const todoObj ={id:nanoid(),title:this.title,done:false};// 通知 App 组件去添加一个 todo 对象this.$emit("addTodo", todoObj);// 清空输入this.title ="";},},};</script><style scoped>.todo-header input {width: 578px;height: 28px;
font-size: 14px;border: 1px solid #ccc;
border-radius: 4px;padding: 4px 7px;
margin-bottom: 10px;}.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),00 8px rgba(82,168,236,0.6);}</style>
MyList.vue 组件
- 该组件即为 ul 标签包裹着 MyItem.vue 组件的果皮
- 真正的果肉在 MyItem.vue 组件里面~~
<template><ul class="todo-main"><!--:todo,动态绑定,供 MyItem.vue 使用 --><!-- 自定义 updateTodo 事件,子传父,供子组件编辑更新数据 --><MyItem
v-for="todoObj in todos":key="todoObj.id":todo="todoObj":checkTodo="checkTodo":deleteTodo="deleteTodo"
@updateTodo="updateTodo"/></ul></template><script>import MyItem from"./MyItem.vue";exportdefault{name:"MyList",components:{ MyItem },props:["todos","checkTodo","deleteTodo","updateTodo"],};</script><style scoped>.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>
MyItem.vue 组件
- 获取焦点的时候要用
$nextTick
(等 DOM 节点更新后执行),或者用 setTimeout 异步包裹也能达到同样的效果 - Vue2.x 不能监测 到 对象属性的添加或删除。因为 Vue.js 在 初始化实例时 将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue2.x 转换它,才能让它是响应的。
- 所以,当我们想要在 data 中或者 data 中的对象添加新的属性时,我们需要使用
Vue.set()
和vm.$set()
,否则是无法触发视图更新的。
<template><li><label><!-- 复选框,:checked 单向绑定 todo 是否已完成,@change 检测复选框的变化 --><input
type="checkbox":checked="todo.done"
@change="handleCheck(todo.id)"/><!-- 非编辑状态下,在 sapn 标签中展示 todo --><span v-show="!todo.isEdit">{{ todo.title }}</span><!-- 绑定失去焦点事件,更新内容。ref 打标识,用于自动获取焦点 --><input
type="text"
style="height: 22px"
v-show="todo.isEdit":value="todo.title"
@blur="handleBlur(todo, $event)"
ref="inputTitle"/></label><!-- 删除 todo --><button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button><!-- 编辑状态下,展示输入框,隐藏编辑按钮。 --><button v-show="!todo.isEdit"class="btn btn-edit" @click="handleEdit(todo)">编辑</button></li></template><script>exportdefault{name:"MyItem",// 声明接收 todo 对象,checkTodo 是否勾选,deleteTodo 删除该 todoprops:["todo","checkTodo","deleteTodo"],methods:{// 勾选 or 取消勾选handleCheck(id){// 通知 APP 组件 将对应的 todo 对象的 done 值取反this.checkTodo(id);},// 删除 todohandleDelete(id){if(confirm("确定删除当前任务吗?")){this.deleteTodo(id);}},// 编辑handleEdit(todo){// 如果 todo 身上有 isEdit,则直接修改 isEdit,否则再给 todo 添加新的 isEdit// Reflect.has(todo, 'isEdit') 或 todo.hasOwnProperty.call(todo, "isEdit") if(Reflect.has(todo,'isEdit')){
todo.isEdit =true;}else{this.$set(todo,"isEdit",true);}// DOM 节点更新后执行 this.$nextTick(()=>{this.$refs.inputTitle.focus()})},// 失去焦点,编辑框隐藏,并判断编辑后的内容是否为空,再呈现编辑后的内容handleBlur(todo, e){
todo.isEdit =false;if(!e.target.value.trim())returnalert('输入内容不能为空!')this.$emit('updateTodo', todo.id, e.target.value)},},};</script><style scoped>
span {color: orange;}
li {
list-style: none;height: 36px;
line-height: 36px;padding:0 5px;
border-bottom: 1px solid #ddd;}
li label {cursor: pointer;}
input {
margin-right: 5px;}
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>
MyFooter.vue 组件
- 底部的展示,当没有任务时隐藏该组件
- reduce() 是一个高阶函数,接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值 参考链接
<template><!-- total 不为 0 则显示底部,否则隐藏 --><div class="todo-footer" v-show="total"><label><!-- 是否全选,双向绑定 isAll --><input type="checkbox" v-model="isAll"/></label><!-- 插值语法呈现数值 --><span class="done">已完成 {{ doneTotal }}</span> /<span class="total">全部 {{ total }}</span><button class="btn btn-danger" @click="clearAll()">清除已完成任务</button></div></template><script>exportdefault{name:"MyFooter",props:["todos"],computed:{// 返回 todos 的总长度total(){returnthis.todos.length;},// 统计任务已经完成的个数doneTotal(){// reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值// pre 必需:初始值;todo 必需:当前元素;0 可选:传递给函数的初始值returnthis.todos.reduce((pre, todo)=> pre +(todo.done ?1:0),0);},// 是否全选,当被选个数和总个数相同,且总个数大于 0 时,checked 选中isAll:{get(){returnthis.doneTotal ===this.total &&this.total >0;},set(value){this.$emit("checkAllTodo", value);},},},methods:{// 清除所有已完成任务clearAll(){this.$emit("clearAllTodo");},},};</script><style scoped>.done {
font-weight: bold;color: skyblue;}.total {
font-weight: bold;color: palevioletred;}.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:-3px;
vertical-align: middle;
margin-right:-10px;}.todo-footer button {float: right;
margin-top: 5px;}</style>
5、写在最后的话
如果你是 看完全篇 阅读到了这里,我相信你一定是有收获的!
那么下面不妨打开自己的电脑,启动自己的编译器,来跟着做 / 自己做一遍吧!
好吧,我骗了你,真正学会它可能不止两个小时,但再多花点时间,你对 vue 的理解可能会有质的提升,加油~
如果这篇文章对你有些许帮助的话,不妨 三连 + 关注 支持一下~~
下一篇是 github 的搜索 demo,也是使用的 vue2.x 实现的,一起期待一下吧~
版权归原作者 前端杂货铺 所有, 如有侵权,请联系我们删除。