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">&times;</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>




阅读剩余
THE END