TodoList项目学习总结
项目文档:
http://www.godbasin.com/vue-ebook/vue-ebook/8.html#_8-1-%E5%8D%95%E7%BB%84%E4%BB%B6-todo-list
项目总体架构(粗略总结):
- main.js配置路由、导入App.vue和Todo.vue,el选择挂载dom结点,render把App组件渲染到el挂载的结点上,
- App.vue中的template模板中<router-view>渲染最高级路由匹配的路径和Todo组件,导入ConfirmDialog组件,动态组件<component :is>由:is来控制,
- Todo.vue中template模板包含头部,<Todoitem>组件(v-for遍历computedTodos展示对象的Todoitem组件),尾部标签选择栏,导入Todoitem组件,其中点击标签改变,<router-link :to="{query: {state: 'active'}}>传入参数,根据参数改变计算属性computedTodos
- Todoitem.vue中导入confirm.js方法(js方法中导入vm实例),点击删除按钮,则调用js方法,通过$vm.emit触发事件,在App.vue中监听接收弹窗信息和弹窗组件,然后在动态组件中:is来弹出弹框,
- ConfirmDialog.vue组件中,通过App.vue父组件传入弹窗相关信息,点击确认或取消按钮{触发js方法的resolve或reject,在Todoitem回调的.then函数中$emit("delete")传给父组件Todo.vue删除Todoitem,$emit触发done事件},App.vue中监听done并删除弹框
<template>
<div :class="{editing: isEdited }">
<div class="view">
<!-- 选择某条备忘 -->
<!-- v-model 绑定是否选中 -->
<input class="toggle" type="checkbox" @change="updateChecked($event.target.checked)">
<!-- 双击可操作备忘 -->
<label @dblclick="editTodo(todo)">{{ title }}</label>
<!-- 删除某条备忘 -->
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<!-- 修改备忘的数据,失焦或 Enter 键可更新数据,Esc键取消更新 ,注意双向绑定的是editingtitle-->
<input class="edit" type="text" v-model="editingTitle" v-autofocus v-if="isEdited"
@blur="doneEdit()"
@keyup.enter="doneEdit()"
@keyup.esc="cancelEdit()">
</div>
</template>
editTodo(){
this.editingTitle = this.title//一样是editingTitle与title同步
this.isEdited = true
},
list.css
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
autofocus: {
// 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
inserted: function (el) {
// el: 指令所绑定的元素,可以用来直接操作 DOM
el.focus()
}
}
1、动态改变样式
通过改变this.isEdited = true,来改变页面的样式,<div :class="{editing: isEdited }">,当双击label时,动态改变class的样式,在css中会把view设置为display:none(不显示),只显示输入框。
这样就实现了双击显示输入框的功能要求
2、给输入框实现自动聚焦
通过标签绑定v-autofocus v-if="isEdited",其中v-if实现dom的插入与否,在v-autofocus中通过钩子函数inserted在dom插入时调用el.focus即可实现聚焦
子组件
props: {
// 备忘内容
title: {
type: String,
default: ""
},
...省略
updateChecked(completed) {
// 更新绑定的 completed
this.$emit("update:completed", completed);
},
父子组件的双向绑定与通信
用this.$emit('update:title', newTitle)
方法表达对其赋新值的意图,父组件可以这样监听事件并根据更新一个对应的数据:
<todo-item
v-bind:title="todo.title"
v-on:update:title="todo.title = $event"
></todo-item>
Vue 中为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:
<todo-item :title.sync="todo.title"></todo-item>
1、v-bind绑定title
2、title.sync可看作为@update:title="(data)=>todo.title=data"。监听update:title事件
@update:title="(data)=>todo.title=data"简写成:title.sync="todo.title"
3、data为子组件传过来的数据,子组件设置props和this.$emit("update:completed", data)触发事件并传递参数
使用vue-router
当我们刷新页面的时候,就会丢失当前的页面状态
路由配置只需要配一个页面的路由,状态使用query
来传参就可以
import Todo from "pages/Todo.vue";
// 配置路由信息
const routes = [
{ path: "/todo", component: Todo, name: "Todo" },
// 通配符 * 会匹配所有路径
{ path: "*", redirect: { name: "Todo" } }
];
//同时,我们需要给几个 Tab 添加激活状态:
<li>
<router-link :to="{query: {state: ''}}" active-class="selected" exact
>All</router-link
>
</li>
<li>
<router-link :to="{query: {state: 'active'}}" active-class="selected" exact
>Active</router-link
>
</li>
当刷新页面,url不会变,还保存着#/todo?state=active的信息,则可以保存状态
使用缓存
如果说我们希望刷新页面之后,原本的备忘还在,这个时候我们就需要把内容写到缓存里。这里我们使用localStorage
来存储:
export default {
// 其他选项省略
data() {
return {
// 初始化的时候,获取下本地的缓存
todos: JSON.parse(localStorage.getItem("todos") || "[]") // 所有的备忘
};
},
watch: {
// 侦听 todos 的变化
todos(newVal) {
// 每次更新写入缓存
localStorage.setItem("todos", JSON.stringify(newVal));
}
}
};
Promise与异步组件
通过 Promise 来进行弹窗,要实现使用 Promise,需要监听用户的确认或取消的,然后通过reject
或是resolve
来继续处理后续逻辑。
简略总结:
Todoitem.vue 点击删除按钮,通过vm实例触发事件,通过promise实现--> App.vue,监听vm实例中的事件,接收弹框参数和组件 --> ConfirmDialog.vue,点击确定或否定按钮,执行resolve或reject(reject会在Todoitem.vue中触发delete事件,在Todo.vue中监听并删除Todoitem。执行reject或resolove都会触发$emit(done)事件并在App.vue中监听来删除弹出框),进入下一个状态--> Todoitem.vue 中的promise.then回调函数根据状态来执行删除Todoitem组件。
通过在最外层导出根实例:
// main.js
// 默认 export 该 Vue 实例
export default new Vue({
el: "#app",
router, // 传入路由能力
render: h => h(App)
});
然后在需要的组件或者公共函数方法里引入,同时直接使用$emit触发事件、$on监听事件:
// confirm.js
// 获取该实例
import vm from "../main";
// 传入标题、内容、确认按钮和取消按钮的文案
export function confirmDialog({ title, text, cancelText, confirmText }) {
return new Promise((resolve, reject) => {
// 把 reject 和 resolve 通过 $emit 事件传参带过去,方便进行 Promise 状态扭转
vm.$emit("setDialog", {
title,
text,
cancelText,
confirmText,
resolve,
reject
});
});
}
// App.vue
import vm from "../main";
export default {
// 其他选型省略
mounted() {
this.$nextTick(() => {
vm.$on("setDialog", dialogInfo => {
// 将弹窗相关信息、弹窗组件添加进 component 数组中
this.items.push({ dialogInfo, component: ConfirmDialog });
});
});
}
};
通过这样的方式,我们可以在每个需要的组件里,引入根实例,然后再在需要的时候分别进行事件的监听和触发就可以了。但是这个方法的缺点在本章开头也有提到,我们在新增一个事件监听和触发的时候,很容易和已有的事件名冲突,同时我们想要查看到底有哪些地方进行了某个事件的监听,也只能通过全局搜索的方式来查找,多人协作的情况下很容易出现问题。
更简便的方法 用Vuex实现
步骤流程总结:
Todoitem.vue 点击删除按钮,通过confirmDialog方法封装的Store.commit(’setdialog‘,弹框参数)添加Vuex状态,通过promise实现------> App.vue,导入Vuex和弹框组件,在computed属性中同步Vuex中的状态(含有弹框参数), -------> ConfirmDialog.vue,导入Vuex,props绑定传入的弹框数据和索引,点击确定或否定按钮,执行Sore.comit(’removeDialog‘,index)并调用resolve或reject进入下一状态----->执行reject的话会在Todoitem.vue中触发delete事件,在Todo.vue中监听并删除Todoitem。
以下是通过vm实例来传递:
resolve或reject(reject会在Todoitem.vue中触发delete事件,在Todo.vue中监听并删除Todoitem。执行reject或resolove都会触发$emit(done)事件并在App.vue中监听来删除弹出框),进入下一个状态--> Todoitem.vue 中的promise.then回调函数根据状态来执行删除Todoitem组件。
通过与vm实例的对比可以看到:
不必再通过emit和on去触发和监听弹出框的状态来进行删除,在Vuex中弹出框的状态与App.vue、ConfirmDialog.vue组件状态同步,共用一份数据,不必更新监听数据的变化。
详细代码:
为了实现每次调用confirmDialog事件的时候,都新增一个弹窗,我们可以使用一个弹窗列表的方式来管理:
// dialogStore.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const dialogStore = new Vuex.Store({
state: {
// 弹窗列表,用来保存可能弹窗的一系列弹窗
dialogList: []
},
mutations: {
removeDialog(state, index) {
// 移除弹窗
state.dialogList.splice(index, 1);
},
setDialog(
state,
{ title, text, cancelText, confirmText, resolve, reject }
) {
// 添加新的弹窗
state.dialogList.push({
title,
text,
cancelText,
confirmText,
resolve,
reject
});
}
}
});
export default dialogStore;
实现一个用来添加弹窗确认的方法,同时返回一个 Promise,当用户进行了操作之后,就更新 Promise 的状态:
// confirm.js
import dialogStore from "../components/ConfirmDialog/dialogStore";
// 传入标题、内容、确认按钮和取消按钮的文案
export function confirmDialog({ title, text, cancelText, confirmText }) {
return new Promise((resolve, reject) => {
// 调用 dialogStore.commit 提交 setDialog
// 把 reject 和 resolve 通过事件传参带过去,方便进行 Promise 状态扭转
dialogStore.commit("setDialog", {
title,
text,
cancelText,
confirmText,
resolve,
reject
});
});
}
export default confirmDialog;
<!-- App.vue -->
<template>
<div>
<!-- 使用 <router-view></router-view> 来渲染最高级路由匹配到的组件 -->
<router-view></router-view>
<!-- 动态组件由 vm 实例的 component 控制 -->
<!-- done 事件绑定用户操作完毕 -->
<component
v-for="(dialogInfo, index) in dialogList"
:key="index"
:is="ConfirmDialog"
:dialogInfo="dialogInfo"
:index="index"
></component>
</div>
</template>
<script>
// 弹窗组件
import ConfirmDialog from "./components/ConfirmDialog/ConfirmDialog.vue";
import dialogStore from "./components/ConfirmDialog/dialogStore";
export default {
name: "app",
computed: {
dialogList() {
// 绑定 dialogStore 的 dialogList 列表
return dialogStore.state.dialogList;
}
},
data() {
return {
// 用来绑定 component
ConfirmDialog
};
}
};
</script>
通过列表的方式来管理弹窗的 Store,同时我们在动态组件中使用 Prop 的方式传入弹窗的信息,所以我们可以从 Prop 中获取到弹窗的内容:
<!-- ConfirmDialog.vue -->
<template>
<!-- 强制出现 display: block -->
<div class="modal" tabindex="-1" role="dialog" style="display: block">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<!-- 弹窗标题 -->
<h4 class="modal-title">{{dialogInfo.title || '提示'}}</h4>
</div>
<div class="modal-body">
<!-- 弹窗内容 -->
<p>{{dialogInfo.text}}</p>
</div>
<div class="modal-footer">
<!-- 取消按钮,点击取消,cancelText 可设置按钮文案 -->
<button type="button" class="btn btn-default" @click="cancel()">
{{dialogInfo.cancelText || '取消'}}
</button>
<!-- 确认按钮,点击确认,confirmText 可设置按钮文案 -->
<button type="button" class="btn btn-primary" @click="confirm()">
{{dialogInfo.confirmText || '确认'}}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import dialogStore from "./dialogStore";
export default {
props: {
// 弹窗相关信息
dialogInfo: {
type: Object,
default: () => {}
},
// 弹窗的序号,移除的时候需要
index: {
type: Number
}
},
methods: {
// 点击取消
cancel() {
// 要先判断下 reject 方法在不在
if (this.dialogInfo.reject) {
// 取消就 reject 掉呀
this.dialogInfo.reject();
// 移除掉这个弹窗
dialogStore.commit("removeDialog", this.index);
}
},
// 点击确认
confirm() {
// 要先判断下 resolve 方法在不在
if (this.dialogInfo.resolve) {
// 确认就 resolve 掉
this.dialogInfo.resolve();
// 移除掉这个弹窗
dialogStore.commit("removeDialog", this.index);
}
}
}
};
</script>