文章目录
- 📚Todo-list 案例
- 🐇组件化编码流程(通用)
- 🐇实现静态组件
- 🐇展示动态数据
- 🐇交互
- ⭐️添加一个todo
- ⭐️todo勾选实现
- ⭐️删除功能实现
- ⭐️底部统计功能实现
- ⭐️底部全选功能实现
- ⭐️底部一键清除功能实现
- 📚案例小结
- 📚浏览器本地存储
- 📚TodoList本地存储
学习链接:尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通,本文对应p70-p79,博客参考尚硅谷公开笔记,补充记录实操。
📚Todo-list 案例
🐇组件化编码流程(通用)
- 实现静态组件:抽取组件,使用组件实现静态页面效果。
- 展示动态数据:
- 数据的类型、名称是什么?
- 数据保存在哪个组件?
- 交互——从绑定事件监听开始。
🐇实现静态组件
-
组件名:不要和原有标签名冲突(不管大小写,例如Header),开发中也一般不用
MyHeader
,Vue鼓励采用UserHeade.vue
类似命名。 -
注册好先搭结构,链接好层级关系(关注地址的正确链接)
<!-- App.vue --> <script> import UserHeader from './components/UserHeader.vue' import UserList from './components/UserList' import UserFooter from './components/UserFooter' export default { name:'App', components:{UserHeader,UserList,UserFooter} } </script>
<!-- UserList.vue --> <script> import UserItem from '../components/UserItem' export default { name:'UserList', components:{UserItem} } </script>
-
样式套用:
- 先都放到
App.vue
里,然后再拆,先拆结构,再拆样式。 - 拆结构的时候,
App.vue
里的结构剪切后,要连带着补上标签,防忘。 - 拆样式的时候,在特定vue对应样式可补上
scoped
,放冲突。
- 先都放到
🐇展示动态数据
- 数据的类型、名称是什么? 一堆数据用数组,每个数据里的属性用对象。
- 数据保存在哪个组件? 那个组件要展示就给谁,即谁用给谁——UserList。
- 【重要】链接上数据发送
:demo='xxx'
和接收props:[demo]
。
UserList.vue
关键部分<template> <ul class="todo-main"> <UserItem v-for="todoObj in todos" :key="todoObj.id" :fasong="todoObj"></UserItem> </ul> </template> <script> import UserItem from '../components/UserItem.vue' export default { name:'UserList', components:{UserItem}, data(){ return{ todos:[ {id:'001',title:'吃早饭',done:true}, {id:'002',title:'睡午觉',done:false}, {id:'003',title:'散散步',done:true}, ] } } } </script>
UserItem.vue
关键部分<template> <li> <label> <input type="checkbox" :checked="fasong.done"/> <span>{{fasong.title}}</span> </label> <button class="btn btn-danger" style="display:none">删除</button> </li> </template> <script> export default { name:'UserItem', // 声明接收发送内容 props:['fasong'] } </script>
- 目前的设置是数据都放List且暂时还都合理。
🐇交互
⭐️添加一个todo
- id自动生成借助
nanoid
库
- 遇到的问题:按暂时的知识量,兄弟vue(header和list)之间的数据传输很难办——解决办法:把数据交给“爹”
App.vue
。具体通过爹提前给儿传一个函数(props也可以传函数),然后儿把数据借助函数传给爹实现。
-
UserList.vue
关键部分<template> <ul class="todo-main"> <UserItem v-for="todoObj in todos" :key="todoObj.id" :fasong="todoObj"></UserItem> </ul> </template> <script> import UserItem from '../components/UserItem.vue' export default { name:'UserList', components:{UserItem}, props:['todos'] } </script>
-
UserHeader.vue
关键部分<template> <div class="todo-header"> <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/> </div> </template> <script> import {nanoid} from 'nanoid' export default { name:'UserHeader', props:['addTodo'], methods:{ add(e){ // 将用户输入包装成为一个todo对象 const todoObj = {id:nanoid(),title:e.target.value,done:false} this.addTodo(todoObj) } } } </script>
-
App.vue
关键部分<template> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <UserHeader :addTodo="addTodo"></UserHeader> <UserList :todos="todos"></UserList> <UserFooter></UserFooter> </div> </div> </div> </template> <!-- App.vue --> <script> import UserHeader from './components/UserHeader.vue' import UserList from './components/UserList' import UserFooter from './components/UserFooter' export default { name:'App', components:{UserHeader,UserList,UserFooter}, data(){ return{ todos:[ {id:'001',title:'吃早饭',done:true}, {id:'002',title:'睡午觉',done:false}, {id:'003',title:'散散步',done:true}, ] } }, methods:{ addTodo(todoObj){ this.todos.unshift(todoObj) } } } </script>
- 进一步完善:添加完后输入框清空。
add(e){ // 将用户输入包装成为一个todo对象 const todoObj = {id:nanoid(),title:e.target.value,done:false} this.addTodo(todoObj) e.target.value = '' }
- 进一步完善:输入框必须有输入才能提交,这里不借助event,而是通过
v-model
完成数据读取。<template> <div class="todo-header"> <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/> </div> </template> <script> import {nanoid} from 'nanoid' export default { name:'UserHeader', props:['addTodo'], methods:{ add(){ // 校验数据 if(!this.title) return alert('输入不能为空') // 将用户输入包装成为一个todo对象 const todoObj = {id:nanoid(),title:this.title,done:false} // 通知APP组件去添加一个todo对象 this.addTodo(todoObj) // 清空输入 this.title = '' } } } </script>
- 这时会出现以下警告,而且清空失效。
- 修改(给
title
定义)<script> import {nanoid} from 'nanoid' export default { name:'UserHeader', props:['addTodo'], data(){ return{ title:"" } }, methods:{ add(){ // 校验数据 if(!this.title) return alert('输入不能为空') // 将用户输入包装成为一个todo对象 const todoObj = {id:nanoid(),title:this.title,done:false} // 通知APP组件去添加一个todo对象 this.addTodo(todoObj) // 清空输入 this.title = '' } } } </script>
- 其他注意点:函数命名不能重复(
add
、addtodo
)。
⭐️todo勾选实现
- 现在可以勾选,但是vue实际的数据是没有变化的。
- 关键点:
- 数据在哪,关于数据的操作就在哪——在
App.vue
里定义函数。 App.vue
对Item.vue
是爷爷对孙子的关系,相关传输要先给他爸List.vue
,再由他爸给他(现阶段)。
- 数据在哪,关于数据的操作就在哪——在
UserItem.vue
关键代码<template> <li> <label> <input type="checkbox" :checked="fasong.done" @change="handleCheck(fasong.id)"/> <span>{{fasong.title}}</span> </label> <button class="btn btn-danger" style="display:none">删除</button> </li> </template> <script> export default { name:'UserItem', // 声明接收发送内容 props:['fasong','checkTodo'], methods:{ handleCheck(id){ // 通知App组件将对应的todo对象的done值取反 this.checkTodo(id) } } } </script>
UserList.vue
关键代码<template> <ul class="todo-main"> <UserItem v-for="todoObj in todos" :key="todoObj.id" :fasong="todoObj" :checkTodo="checkTodo" ></UserItem> </ul> </template> <script> import UserItem from '../components/UserItem.vue' export default { name:'UserList', components:{UserItem}, props:['todos','checkTodo'] } </script>
App.vue
关键代码<template> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <UserHeader :addTodo="addTodo"></UserHeader> <UserList :todos="todos" :checkTodo="checkTodo"></UserList> <UserFooter></UserFooter> </div> </div> </div> </template> <!-- App.vue --> <script> import UserHeader from './components/UserHeader.vue' import UserList from './components/UserList' import UserFooter from './components/UserFooter' export default { name:'App', components:{UserHeader,UserList,UserFooter}, data(){ return{ todos:[ {id:'001',title:'吃早饭',done:true}, {id:'002',title:'睡午觉',done:false}, {id:'003',title:'散散步',done:true}, ] } }, methods:{ // 数据在哪,对数据的操作就在哪 // 添加一个todo addTodo(todoObj){ this.todos.unshift(todoObj) }, // 勾选or取消勾选一个todo checkTodo(id){ this.todos.forEach((todo)=>{ if(todo.id === id) todo.done = !todo.done }) } } } </script>
- 用
v-model
实现:就是在上一个功能实现的基础上,忽略本功能实现的之前代码,只将:checked="fasong.done"
改为v-model="fasong.done"
,但这里已经和视频有出入,即vue版本更新后,这个方法不可行,会报错(本质是props只读):
⭐️删除功能实现
- 在Item里加一个鼠标悬浮效果
li:hover{ background-color: #ddd }
- 让删除按钮悬浮出现:结构里的内联style
style="display:none"
删掉,添加悬浮条件(前边默认设置为none)li:hover button{ display: block; }
- 交互实现:点击按钮,拿到id,把对应id的事件删除。
- 这里依旧注意函数名称设置问题,不要用默认名称,会混乱会报错!
- 依旧是
App.vue
对Item.vue
是爷爷对孙子的关系,相关传输要先给他爸List.vue
,再由他爸给他(现阶段)
-
UserItem.vue
关键代码<template> <li> <label> <input type="checkbox" :checked="fasong.done" @change="handleCheck(fasong.id)"/> <span>{{fasong.title}}</span> </label> <button class="btn btn-danger" @click="handleDelete(fasong.id)">删除</button> </li> </template> <script> export default { name:'UserItem', // 声明接收发送内容 props:['fasong','checkTodo','deleteTodo'], methods:{ // 勾选or取消勾选 handleCheck(id){ // 通知App组件将对应的todo对象的done值取反 this.checkTodo(id) }, // 删除 handleDelete(id){ if(confirm('确定删除吗?')){ // 通知App组件删除 this.deleteTodo(id) } } } } </script>
-
UserList.vue
关键代码<template> <ul class="todo-main"> <UserItem v-for="todoObj in todos" :key="todoObj.id" :fasong="todoObj" :checkTodo="checkTodo" :deleteTodo="deleteTodo" ></UserItem> </ul> </template> <script> import UserItem from '../components/UserItem.vue' export default { name:'UserList', components:{UserItem}, props:['todos','checkTodo','deleteTodo'] } </script>
-
App.vue
关键代码<template> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <UserHeader :addTodo="addTodo"></UserHeader> <UserList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"></UserList> <UserFooter></UserFooter> </div> </div> </div> </template> <!-- App.vue --> <script> import UserHeader from './components/UserHeader.vue' import UserList from './components/UserList' import UserFooter from './components/UserFooter' export default { name:'App', components:{UserHeader,UserList,UserFooter}, data(){ return{ todos:[ {id:'001',title:'吃早饭',done:true}, {id:'002',title:'睡午觉',done:false}, {id:'003',title:'散散步',done:true}, ] } }, methods:{ // 数据在哪,对数据的操作就在哪 // 添加一个todo addTodo(todoObj){ this.todos.unshift(todoObj) }, // 勾选or取消勾选一个todo checkTodo(id){ this.todos.forEach((todo)=>{ if(todo.id === id) todo.done = !todo.done }) }, // 删除一个todo deleteTodo(id){ // this.todos = this.todos.filter((todo)=>{ // return todo.id !== id // }) // 精简写法 this.todos = this.todos.filter(todo => todo.id != id) } } } </script>
⭐️底部统计功能实现
- 把
todos
传给Footer
,在App.vue
添加<UserFooter :todos="todos"></UserFooter>
。 UserFooter.vue
- 读取
todos.length
作为全部数值显示。 - 计算属性,算
done
为true
的数量。
<template> <div class="todo-footer"> <label> <input type="checkbox"/> </label> <span> <span>已完成{{doneTotal}}</span> / 全部{{todos.length}} </span> <button class="btn btn-danger">清除已完成任务</button> </div> </template> <script> export default { name:'UserFooter', props:['todos'], computed:{ doneTotal(){ // 法一 // let i = 0 // this.todos.forEach((todo)=>{ // if(todo.done) i++ // }) // return i // 法二 return this.todos.reduce((pre,current)=> pre + (current.done ? 1 : 0),0) } } } </script>
- 读取
⭐️底部全选功能实现
- 考虑实际情境的细节优化。
- 同样也是对todos的操作写到
App.vue
。 App.vue
关键代码<template> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <UserHeader :addTodo="addTodo"></UserHeader> <UserList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"></UserList> <UserFooter :todos="todos" :checkAllTodo="checkAllTodo"></UserFooter> </div> </div> </div> </template> <!-- App.vue --> <script> import UserHeader from './components/UserHeader.vue' import UserList from './components/UserList' import UserFooter from './components/UserFooter' export default { name:'App', components:{UserHeader,UserList,UserFooter}, data(){ return{ todos:[ {id:'001',title:'吃早饭',done:true}, {id:'002',title:'睡午觉',done:false}, {id:'003',title:'散散步',done:true}, ] } }, methods:{ // 数据在哪,对数据的操作就在哪 // 添加一个todo addTodo(todoObj){ this.todos.unshift(todoObj) }, // 勾选or取消勾选一个todo checkTodo(id){ this.todos.forEach((todo)=>{ if(todo.id === id) todo.done = !todo.done }) }, // 删除一个todo deleteTodo(id){ // this.todos = this.todos.filter((todo)=>{ // return todo.id !== id // }) // 精简写法 this.todos = this.todos.filter(todo => todo.id != id) }, // 全选or取消全选 checkAllTodo(done){ this.todos.forEach((todo)=>{ todo.done = done }) } } } </script>
UserFooter.vue
关键代码<template> <div class="todo-footer" v-show="total"> <label> <input type="checkbox" :checked="isAll" @change="checkAll"/> </label> <span> <span>已完成{{doneTotal}}</span> / 全部{{total}} </span> <button class="btn btn-danger">清除已完成任务</button> </div> </template> <script> export default { name:'UserFooter', props:['todos','checkAllTodo'], computed:{ total(){ return this.todos.length }, doneTotal(){ // 法一 // let i = 0 // this.todos.forEach((todo)=>{ // if(todo.done) i++ // }) // return i // 法二 return this.todos.reduce((pre,current)=> pre + (current.done ? 1 : 0),0) }, isAll(){ return this.doneTotal === this.total && this.total > 0 } }, methods:{ checkAll(e){ this.checkAllTodo(e.target.checked) } } } </script>
- 优化
UserFooter.vue
:借助v-model
及计算属性(之前v-model
失效是因为绑到props
了)。<template> <div class="todo-footer" v-show="total"> <label> <input type="checkbox" v-model="isAll"/> </label> <span> <span>已完成{{doneTotal}}</span> / 全部{{total}} </span> <button class="btn btn-danger">清除已完成任务</button> </div> </template> <script> export default { name:'UserFooter', props:['todos','checkAllTodo'], computed:{ total(){ return this.todos.length }, doneTotal(){ return this.todos.reduce((pre,current)=> pre + (current.done ? 1 : 0),0) }, isAll:{ get(){ return this.doneTotal === this.total && this.total > 0 }, set(value){ this.checkAllTodo(value) } } } } </script>
⭐️底部一键清除功能实现
App.vue
部分<template> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <UserHeader :addTodo="addTodo"></UserHeader> <UserList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"></UserList> <UserFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"></UserFooter> </div> </div> </div> </template> <!-- App.vue --> <script> import UserHeader from './components/UserHeader.vue' import UserList from './components/UserList' import UserFooter from './components/UserFooter' export default { name:'App', components:{UserHeader,UserList,UserFooter}, data(){ return{ todos:[ {id:'001',title:'吃早饭',done:true}, {id:'002',title:'睡午觉',done:false}, {id:'003',title:'散散步',done:true}, ] } }, methods:{ // 数据在哪,对数据的操作就在哪 // 添加一个todo addTodo(todoObj){ this.todos.unshift(todoObj) }, // 勾选or取消勾选一个todo checkTodo(id){ this.todos.forEach((todo)=>{ if(todo.id === id) todo.done = !todo.done }) }, // 删除一个todo deleteTodo(id){ // this.todos = this.todos.filter((todo)=>{ // return todo.id !== id // }) // 精简写法 this.todos = this.todos.filter(todo => todo.id != id) }, // 全选or取消全选 checkAllTodo(done){ this.todos.forEach((todo)=>{ todo.done = done }) }, // 清除所有已经完成的todo clearAllTodo(){ this.todos = this.todos.filter((todo)=>{ return !todo.done }) } } } </script>
UserFooter.vue
部分<template> <div class="todo-footer" v-show="total"> <label> <input type="checkbox" v-model="isAll"/> </label> <span> <span>已完成{{doneTotal}}</span> / 全部{{total}} </span> <button class="btn btn-danger" @click="clearAll">清除已完成任务</button> </div> </template> <script> export default { name:'UserFooter', props:['todos','checkAllTodo','clearAllTodo'], computed:{ total(){ return this.todos.length }, doneTotal(){ return this.todos.reduce((pre,current)=> pre + (current.done ? 1 : 0),0) }, isAll:{ get(){ return this.doneTotal === this.total && this.total > 0 }, set(value){ this.checkAllTodo(value) } } }, methods:{ clearAll(){ this.clearAllTodo() } } } </script>
📚案例小结
-
组件化编码流程:
- 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
- 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
- 一个组件在用:放在组件自身即可。
- 一些组件在用:放在他们共同的父组件上(状态提升)。
- 实现交互:从绑定事件开始。
-
props适用于:
- 父组件 ==> 子组件 通信
- 子组件 ==> 父组件 通信(要求父先给子一个函数)
-
使用
v-model
时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
📚浏览器本地存储
-
浏览器的搜索历史就是借助了本地存储。
-
存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
-
浏览器端通过
Window.sessionStorage
和Window.localStorage
属性来实现本地存储机制。 -
相关API:
xxxxxStorage.setItem('key', 'value');
:该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。xxxxxStorage.getItem('person');
:该方法接受一个键名作为参数,返回键名对应的值。xxxxxStorage.removeItem('key');
:该方法接受一个键名作为参数,并把该键名从存储中删除。xxxxxStorage.clear()
:该方法会清空存储中的所有数据。
-
备注:
- SessionStorage存储的内容会随着浏览器窗口关闭而消失。
- LocalStorage存储的内容,需要手动清除才会消失。
xxxxxStorage.getItem(xxx)
如果xxx对应的value获取不到,那么getItem的返回值是null。JSON.parse(null)
的结果依然是null。
localStorage.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>localStorage</title> </head> <body> <h2>localStorage</h2> <button onclick="saveData()">点我保存一个数据</button> <button onclick="readData()">点我读取一个数据</button> <button onclick="deleteData()">点我删除一个数据</button> <button onclick="deleteAllData()">点我清空一个数据</button> <script type="text/javascript" > let p = {name:'张三',age:18} function saveData(){ localStorage.setItem('msg','hello!!!') localStorage.setItem('msg2',666) localStorage.setItem('person',JSON.stringify(p)) } function readData(){ console.log(localStorage.getItem('msg')) console.log(localStorage.getItem('msg2')) const result = localStorage.getItem('person') console.log(JSON.parse(result)) } function deleteData(){ localStorage.removeItem('msg2') } function deleteAllData(){ localStorage.clear() } </script> </body> </html>
- **
localStorage.html
**即对应部分API换成sessionStorage
📚TodoList本地存储
- 目的:自添加事项刷新后不清除。
- 在
App.vue
添加watch,同时data里配套读取export default { name:'App', components:{UserHeader,UserList,UserFooter}, data(){ return{ todos:JSON.parse(localStorage.getItem('todos')) || [] } }, methods:{ // 数据在哪,对数据的操作就在哪 // 添加一个todo addTodo(todoObj){ this.todos.unshift(todoObj) }, // 勾选or取消勾选一个todo checkTodo(id){ this.todos.forEach((todo)=>{ if(todo.id === id) todo.done = !todo.done }) }, // 删除一个todo deleteTodo(id){ // this.todos = this.todos.filter((todo)=>{ // return todo.id !== id // }) // 精简写法 this.todos = this.todos.filter(todo => todo.id != id) }, // 全选or取消全选 checkAllTodo(done){ this.todos.forEach((todo)=>{ todo.done = done }) }, // 清除所有已经完成的todo clearAllTodo(){ this.todos = this.todos.filter((todo)=>{ return !todo.done }) } }, watch:{ todos:{ // 开启深度监视 deep:true, handler(value){ localStorage.setItem('todos',JSON.stringify(value)) } } } }
- 刷新后不清除