Todo-list 案例
- 一、组件化编码流程
- 二、props 扩展
- 三、webStorage
- 四、自定义事件
- 五、全局事件总线
- 六、消息订阅与发布
- 七、nextTick
- 八、动画与过度
-
- 1. 动画 animation
- 2. 过渡 transition
- 3. 第三方样式库 animate.css
- 九、案例代码
-
- 1. Header
- 2. List
- 3. Item
- 4. Footer
- 5. App.vue
- 6. main.js
一、组件化编码流程
- 拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突。
- 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件再用:
(1)一个组件在用:放在组件自身即可。
(2)一些组件再用:放在他们共同的父组件上(状态提示)。 - 交互——从绑定事件监听开始。
二、props 扩展
-
props 适用于:
(1)父组件 ===> 子组件 通信
(2)子组件 ===> 父组件 通信(要求父先给子一个函数) -
使用 v-model 时要切记:
v-model 绑定的值不能是 props 传过来的值,因为 props 是不可修改的! -
props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐这样做。
三、webStorage
-
存储内容大小一般支持 5MB 左右(不同浏览器可能不一样)
-
浏览器通过 Window.sessionStorage 和 Window.localStorage 属性实现本地存储机制。
-
相关 API:
(1)xxxxxStorage.setItem('key', 'value');
该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。
(2)xxxxxStorage.getItem('person');
该方法接受一个键名作为参数,返回键名对应的值。
(3)xxxxxStorage.removeItem('key');
该方法接受一个键名作为参数,并把该键名从存储中删除。
(4)xxxxxStorage.clear();
该方法会清空存储中的所有数据。 -
备注:
(1)SessionStorage 存储的内容会随着浏览器窗口关闭而消失。
(2)LocalStorage 存储的内容,需要手动清除才会消失。
(3)xxxxxStorage.getItem(xxx);
如果 xxx 对应的 value 获取不到,那么 getItem 的返回值是 null。
(4)JSON.parse(null)
的结果依然是 null。
四、自定义事件
-
自定义事件是一种组件间通信的方式,适用于:【子组件 ===> 父组件】
-
使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)
-
绑定自定义事件:
(1)第一种方式,通过v-on
,在父组件中:Demo @atguigu="test"
或Demo v-on:atguigu="test"
(2)第二种方式,通过ref
,在父组件中:<demo ref="demo">methods:{test(){...} }, mounted(){this.$refs.demo.$on('ayguigu', this.test) }
(3)若想让自定义事件只能触发一次,可以用
.once
修饰符,或$once
方法。 -
触发自定义事件:
this.$emit('atguigu', 传给父的参数数据)
-
解绑自定义事件:解绑一个
this.$off('atguigu')
、解绑多个this.$off(['atguigu', 'demo', ...])
、解绑全部this.$off()
-
组件上也可以绑定原生 DOM 事件,需要使用
native
修饰符。<student @click.native="show"></student>
-
注意:通过
this.$refs.demo.$on('ayguigu', 回调)
绑定自定义时间时,回调要么配置在 methods 中,要么用箭头函数,否则回调中的 this 会指向子组件。
五、全局事件总线
-
全局事件总线是一种组件间通信的方式,适用于任意组件间的通信。
-
安装全局事件总线:
-
使用全局事件总线:
(1)接收数据:A组件想接收数据,则在A组件中给 $bus 绑定自定义事件,事件的回调留在A组件自身。methods() { demo(data){ ......} } ...... mounted() { this.$bus.$on('xxx', this.demo) }
(2)提供数据:
this.$bus.$emit('xxx', 数据)
-
最好在 beforeDestroy 钩子中,用
$off
去解绑当前组件所用到的事件。
六、消息订阅与发布
-
消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信。
-
使用步骤:
(1)安装第三方库 pubsub:npm i pubsub-js
(2)引入:import pubsub from "pubsub-js";
(3)接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。methods() { demo(data){ ......} } ...... mounted() { this.pid = pubsub.subscribe('xxx', this.demo) // 订阅消息 }
(4)提供数据:
pubsub.publish('xxx', 数据)
-
最好在 beforeDestroy 钩子中,用
pubsub.unsubscribe(this.pid);
去取消订阅。
七、nextTick
-
语法:
this.$nextTick(回调函数)
-
作用:nextTick 是一个生命周期钩子,在下一次 DOM 更新结束后执行其指定的回调。
-
什么时候用:当改变数据后,要基于更新后的新 DOM 进行某些操作时,要在 nextTick 所指定的回调函数中执行。
八、动画与过度
-
作用:在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名。
-
图示:
-
写法:
(1)准备好样式:
元素进入的样式:
① v-enter:进入的起点
② v-enter-active:进入过程中
① v-enter-to:进入的终点
元素离开的样式:
① v-leave:离开的起点
② v-leave-active:离开过程中
① v-leave-to:离开的终点 -
使用
<transition>
包裹要过度的元素,并配置 name 属性:<transition><h1 v-show="isShow">你好啊!</h1> </transition>
-
备注:若有多个元素需要过度,则需要使用:
<transition-group>
,且每个元素都要指定key
值。
1. 动画 animation
<template><div><transition-group name="hello" appear><h2 v-show="show" key="1">学校名称:{
{ name }}</h2><h2 v-show="show" key="2">学校地址:{
{ address }}</h2></transition-group></div>
</template><style lang="css" scoped>h2 {
background-color: pink;}.hello-enter-active {
animation: identifier 1s;}.hello-leave-active {
animation: identifier 1s reverse;}@keyframes identifier {
from {
transform: translateX(-100%);}to {
transform: translateX(0);}} </style>
2. 过渡 transition
<template><div><transition-group name="hello" appear><h2 v-show="show" key="1">学校名称:{
{ name }}</h2><h2 v-show="show" key="2">学校地址:{
{ address }}</h2></transition-group></div>
</template><style lang="css" scoped>h2 {
background-color: pink;}.hello-enter,.hello-leave-to {
transform: translateX(-100%);}.hello-enter-to,.hello-leave {
transform: translateX(0);}.hello-enter-active,.hello-leave-active {
transition: 1s;} </style>
3. 第三方样式库 animate.css
-
安装
npm i animate.css
-
引入,在
<script>
中引入<script>import 'animate.css' </script>
-
使用
<template><div><transition-groupname="animate__animated animate__bounce"enter-active-class="animate__jello"leave-active-class="animate__backOutUp"appear><h2 v-show="!show" key="1">学校名称:{ { name }}</h2><h2 v-show="show" key="2">学校地址:{ { address }}</h2></transition-group></div> </template><script> import "animate.css"; </script><style lang="css" scoped>h2 { background-color: pink;} </style>
九、案例代码
1. Header
<template><div class="todo-header"><inputtype="text"placeholder="请输入你的任务名称,按回车键确认"v-model="title"@keyup.enter="add"/></div>
</template><script> import {
nanoid } from "nanoid"; export default {
name: "AddHeader",data() {
return {
title: "",};},methods: {
add() {
if (!this.title.trim()) return alert("输入不能为空");// 包装成对象const todo = {
id: nanoid(),title: this.title,done: false,};this.$emit('headerAddTodo',todo)this.title = "";},}, }; </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. List
<template><ul class="todo-main" v-if="todo.length"><transition-groupname="animate__animated animate__bounce"enter-active-class="animate__bounceInRight"leave-active-class="animate__backOutRight"appear><todo-item v-for="item in todo" :key="item.id" :todo="item" /></transition-group></ul>
</template><script> import "animate.css"; import TodoItem from "./TodoItem.vue"; export default {
components: {
TodoItem },name: "TodoList",props: ["todo"], }; </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>
3. Item
<template><li><label><input type="checkbox" :checked="todo.done" @change="handle(todo.id)" /><span v-show="!todo.isEdit"> {
{ todo.title }}</span></label><button class="btn btn-danger" @click="ListDelete(todo.id)">删除</button><button v-show="!todo.isEdit" class="btn btn-edit" @click="editItem(todo)">编辑</button><inputv-show="todo.isEdit"type="text":value="todo.title"ref="itemInput"@keyup.enter="inputBlur(todo, $event)"@blur="inputBlur(todo, $event)"/></li>
</template><script> export default {
name: "TodoItem",props: ["todo"],methods: {
handle(id) {
this.$bus.$emit("checkTodo", id);},ListDelete(id) {
if (confirm("确定删除吗?")) {
this.$bus.$emit("deleteTodo", id);}},editItem(todo) {
if (todo.hasOwnProperty("isEdit")) {
todo.isEdit = true;} else {
this.$set(todo, "isEdit", true);}this.$nextTick(function () {
this.$refs.itemInput.focus();});},inputBlur(todo, e) {
todo.isEdit = false;// console.log(todo.id, e.target.value);if (e.target.value.trim() === "") return alert("你没有输入值");this.$bus.$emit("todotitle", todo.id, e.target.value);},}, }; </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>
4. Footer
<template><div class="todo-footer" v-if="todo.length"><label><input type="checkbox" v-model="checkAll" /></label><span><span>已完成{
{ doneTotal }}</span> / 全部{
{ todo.length }}</span><button class="btn btn-danger" @click="clearAll">清除已完成任务</button></div>
</template><script> export default {
name: "SelectFooter",props: ["todo"],computed: {
doneTotal() {
return this.todo.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);},checkAll: {
get() {
return this.todo.length === this.doneTotal;},set(value) {
this.$emit("checkAllTodo", value);},},},methods: {
clearAll() {
this.$emit("clearAllTodo")},}, }; </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>
5. App.vue
<template><div id="app"><div class="todo-container"><div class="todo-wrap"><add-header @headerAddTodo="addTodo" /><todo-list :todo="todo" /><select-footer:todo="todo"@checkAllTodo="checkAllTodo"@clearAllTodo="clearAllTodo"/></div></div></div>
</template><script> import AddHeader from "./components/AddHeader"; import SelectFooter from "./components/SelectFooter"; import TodoList from "./components/TodoList";export default {
name: "App",components: {
AddHeader,SelectFooter,TodoList,},data() {
return {
todo: JSON.parse(localStorage.getItem("todo")) || [],};},methods: {
// 添加一个todoaddTodo(todo) {
this.todo.unshift(todo);},// 勾选or取消勾选checkTodo(id) {
this.todo.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done;});},// 修改editTodo(id, val) {
this.todo.forEach((todo) => {
if (id === todo.id) todo.title = val.trim();});},// 删除deleteTodo(id) {
this.todo = this.todo.filter((todo) => todo.id !== id);},checkAllTodo(check) {
this.todo.forEach((todo) => (todo.done = check));},clearAllTodo() {
const x = this.todo.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);if (x <= 0) {
alert("没有勾选任何TODO");} else {
if (confirm("确定删除所有已完成吗?")) {
this.todo = this.todo.filter((todo) => !todo.done);}}},},watch: {
todo: {
deep: true,handler(value) {
localStorage.setItem("todo", JSON.stringify(value));},},},created() {
this.todo.forEach((todo) => {
if (todo.hasOwnProperty("isEdit")) {
todo.isEdit = false;}});},mounted() {
this.$bus.$on("checkTodo", this.checkTodo);this.$bus.$on("deleteTodo", this.deleteTodo);this.$bus.$on("todotitle", this.editTodo);},beforeDestroy() {
this.$bus.$off(["checkTodo", "deleteTodo", "todotitle"]);}, }; </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-edit {
color: #fff;background-color: skyblue;border: 1px solid rgb(106, 160, 182);margin-right: 5px; } .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>
6. main.js
import Vue from 'vue'
import App from './App.vue'Vue.config.productionTip = falsenew Vue({
render: h => h(App),beforeCreate() {
Vue.prototype.$bus = this;},
}).$mount('#app')