当前位置:首页 » 《随便一记》 » 正文

Vue项目实战——实现一个任务清单(学以致用,两小时带你巩固和强化Vue知识点)

2 人参与  2022年09月27日 08:14  分类 : 《随便一记》  评论

点击全文阅读


Vue2.x 项目实战(一)


文章目录

Vue2.x 项目实战(一)Vue2.x 实现 todoList1、前言2、项目演示(一睹为快)3、涉及知识点4、项目详情(附源码及解析)5、写在最后的话


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";export default {  name: "App",  components: { MyHeader, MyList, MyFooter },  data() {    return {      // 由于 todos 是 MyHeader 组件 和 MyFooter 组件都在用,所以放在APP中(状态提升)      // 解析 JSON字符串 第一次使用时 null 身上没有 length 属性会报错,所以添加||,前面不能用时,置为空数组      // localStorage.getItem("xxx") 用于从本地存储中读取 todos      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    updateTodo(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;      });    },    // 清除所有已经完成的todo    clearAllTodo() {      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 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 {  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";export default {  name: "MyHeader",  data() {    return {      // 要输入的任务事项      title: "",    };  },  methods: {    add() {      // 校验数据      if (!this.title.trim()) return alert("输入不能为空");      // 将用户的输入包装成为一个 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),    0 0 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";export default {  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>export default {  name: "MyItem",  // 声明接收 todo 对象,checkTodo 是否勾选,deleteTodo 删除该 todo  props: ["todo", "checkTodo", "deleteTodo"],  methods: {    // 勾选 or 取消勾选    handleCheck(id) {      // 通知 APP 组件 将对应的 todo 对象的 done 值取反      this.checkTodo(id);    },    // 删除 todo    handleDelete(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()) return alert('输入内容不能为空!')      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>export default {  name: "MyFooter",  props: ["todos"],  computed: {    // 返回 todos 的总长度    total() {      return this.todos.length;    },    // 统计任务已经完成的个数    doneTotal() {      // reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值      // pre 必需:初始值;todo 必需:当前元素;0 可选:传递给函数的初始值      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);    },    // 是否全选,当被选个数和总个数相同,且总个数大于 0 时,checked 选中    isAll: {      get() {        return this.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 实现的,一起期待一下吧~


点击全文阅读


本文链接:http://m.zhangshiyu.com/post/45111.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1