Vue 小demo巩固

185 阅读3分钟

一个基于vue的小练习,用来帮助迅速回顾vue的基础知识

下面是效果图

ezgif.com-video-to-gif.gif

具体分为了四个组件,Footer,Header,item,list,其中item与list为父子组件,footer与header,list为兄弟组件,通过这么一个小案例,迅速回顾vue基础知识包括但不限于指令,配置项,通讯方式等等

image.png

image.png

事件总线的建立

相信大家对组件通信这一方面的事件总线都不陌生,它用于兄弟组件,爷孙组件,叔侄组件之间的通讯比较多

image.png

可以看到,如果需要从跟组件往组件n和组件s传递数据,那么需要层层传递下来,传递给组件s的父组件,组件n的爷爷组件,或者组件s需要给组件n传递数据,一层层往上抛出,再一层层往下传递,但是这些数据未必是父组件和爷爷组件需要的,那么这样的传递就会浪费大量的资源,显得很臃肿,而事件总线的出现就是为了解决这一问题。 好,那么不墨迹了事件总线只需要再main组件中加入这么一段代码即可

import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  beforeCreate(){
    Vue.prototype.$bus=this
  }
}).$mount('#app')

只需要在实例创建后,注入前进行写入这样的,在vue的原型上加上。代表将当前实例作为事件总线 关于生命周期不在这里过多赘述,以一张图表示

image.png 事件总线挂在完之后我们先放一边,等到后续用到的时候再详细阐述

组件App

组件app呢是我们的跟组件,一切组件都需要在这里进行引入和展示

<template>
  <div id="root">
    <div class="container">
      <div class="todo-wrap">
        <MyHeader @addTodo="addTodo"></MyHeader>
        <MyList :todos="todos"></MyList>
        <MyFooter :todos="todos" @DelAll="DelAll" @Achieve="Achieve"></MyFooter>
      </div>
    </div>
  </div>
</template>

<script>
import MyFooter from "./components/MyFooter.vue";
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
export default {
  components: {
    MyHeader,
    MyFooter,
    MyList,
  },
  mounted() {
    this.$bus.$on("Del", this.Del);
    this.$bus.$on("check", this.cheke);
    this.$bus.$on("upd", this.upd);
  },
  beforeDestroy() {
    this.$bus.$off("Del", this.Del);
    this.$bus.$off("check", this.cheke);
    this.$bus.$off("upd", this.upd);
  },
  methods: {
    //更新
    upd(id, value) {
      this.todos.forEach((i) => {
        if (i.id === id) {
          i.title = value;
        }
      });
    },
    //添加
    addTodo(todoObj) {
      this.todos.unshift(todoObj);
    },
    //勾选
    cheke(id) {
      this.todos.forEach((item) => {
        if (item.id === id) {
          item.done = !item.done;
        }
      });
    },
    //删除
    Del(id) {
      this.todos = this.todos.filter((e) => e.id !== id);
    },
    //删除已完成的
    DelAll() {
      this.todos = this.todos.filter((e) => e.done === false);
    },
    //全选
    Achieve() {
      this.isAll = this.todos.every((e) => {
        return e.done === true;
      });
      if (this.isAll) {
        this.todos.forEach((e) => {
          e.done = false;
        });
      } else {
        this.todos.forEach((e) => {
          e.done = true;
        });
      }
    },
  },
  data() {
    return {
      todos: JSON.parse(localStorage.getItem("todos")) || [],
    };
  },
  watch: {
    todos: {
      deep: true,
      handler(value) {
        localStorage.setItem("todos", JSON.stringify(value));
      },
    },
  },
};
</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-change {
  color: #fff;
  background-color: #a6aeb3;
  border: 1px solid #a6aeb3;
  margin-right: 8px;
}
.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}
.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}
.btn:focus {
  outline: none;
}
.container {
  width: 600px;
  margin: 0 auto;
}
.container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>

大家先忽视里面的方法,再跟组件中,引入了其他三个组件(import),并且用components进行注册

页脚组件

这里是我们的页脚组件,初始样式很简单,只有一个input勾选框还有一些底部要展示的数据和一些按钮

<template>
  <div class="fotter" v-show="this.todos.length">
    <label>
      <input type="checkbox" @click="test" :checked="isAll" />
    </label>
    <span>
      <span @click="test">已完成{{ tests }}/总数{{ todos.length }}</span>
    </span>
    <button class="btn btn-danger" @click="DelAlls">清除已经完成的任务</button>
  </div>
</template>

<script>
export default {
  props: ["todos"],
  computed: {
    isAll() {
      if (this.todos.length == 0) {
        return false;
      } else {
        return this.tests === this.todos.length;
      }
    },
    tests() {
      return this.todos.reduce((pre, current) => {
        return pre + (current.done ? 1 : 0);
      }, 0);
    },
  },
  methods: {
    DelAlls() {
      this.$emit("DelAll");
    },
    test() {
      this.$emit("Achieve");
    },
  },
};
</script>

<style>
.footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}
.footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}
.footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
}
.footer label button {
  float: right;
  margin-top: 5px;
}
.btn-danger {
  float: right;
}
</style>

头部组件

头部组件只有一个输入框用来添加数据的

<template>
  <div class="header">
    <input
      type="text"
      placeholder="请输入你要添加的任务"
      v-model="title"
      @keyup.enter="add"
    />
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: "",
    };
  },
  methods: {
    add() {
      if (!this.title.trim()) return alert("Please enter");
      const todoObj = {
        id: Math.random() * 10,
        title: this.title,
        done: false,
      };
      this.$emit("addTodo", todoObj);
      this.title = "";
    },
  },
};
</script>

<style>
.header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid black;
  border-radius: 4px;
  padding: 4px 7px;
  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.6);
}
</style>

列表组件

列表的父组件

  <ul class="main">
    <transition-group name="todo" appear>
      <MyItem
        v-for="todoObj in todos"
        :key="todoObj.id"
        :todo="todoObj"
      ></MyItem>
    </transition-group>
  </ul>
</template>

<script>
import MyItem from "./MyItem.vue";
export default {
  components: { MyItem },
  props: ["todos"],
};
</script>

<style>
.main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}
.empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
.todo-enter-active {
  animation: todos 0.3s linear;
}
.todo-leave-active {
  animation: todos 0.3s linear reverse;
}
@keyframes todos {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}
</style>

列表子组件

<template>
  <li>
    <label>
      <input
        type="checkbox"
        :checked="todo.done"
        @click="handleCheck(todo.id)"
      />
      <span v-show="!todo.isUpdata">{{ todo.title }}</span>
      <input
        type="text"
        v-show="todo.isUpdata"
        ref="inputTitle"
        :value="todo.title"
        @blur="reve(todo, $event)"
      />
    </label>
    <button class="btn btn-danger" @click="Dels(todo.id)">删除</button>
    <button class="btn btn-change" @click="achieve(todo)">修改</button>
  </li>
</template>

<script>
import MyList from "./MyList.vue";
export default {
  comments: {
    MyList,
  },
  props: ["todo"],
  methods: {
    handleCheck(i) {
      this.$bus.$emit("check", i);
    },
    Dels(i) {
      this.$bus.$emit("Del", i);
    },
    //失去焦点时候更新数据
    reve(todo, e) {
      todo.isUpdata = false;
      if (!e.target.value) {
        alert("内容不能为空");
        return;
      }
      this.$bus.$emit("upd", todo.id, e.target.value);
    },
    achieve(todo) {
      if (Object.prototype.hasOwnProperty.call(todo, "isUpdata")) {
        todo.isUpdata = true;
      } else {
        this.$set(todo, "isUpdata", true);
      }
      this.$nextTick(function () {
        //一般用于,当数据改变后要基于更新的dom进行某些操作的时候,要在nextTick中执行
        //下一轮的时候在执行,nextTick会在dom节点更新完毕之后在执行
        //如果这个直接暴露在外面,则会执行完achieve之后才会渲染dom,但是由于inp还没有渲染到页面上,无法获取焦点
        this.$refs.inputTitle.focus();
      });
    },
  },
};
</script>

<style>
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>

至此我们的组件已经添加完毕,下面介绍组件里的功能

列表组件

在这个图中我们可以看到,list组件时父组件,item是里面每一个任务的子组件,再这个案例里,数据被存放在了app跟组件中,首先要解决的就是如何把跟组件的数据传递到子组件list和孙组件item中

image.png

我们回过头来看app组件,在app组件中data里

image.png

写入了这么一个数据样式,相信大家都不陌生,localStorage,这段的意思是,如果在本地存储存在数据,那么拿到本地存储里面的数据,如果不存在,那么这个todos就是一个空数组,等待用户往里面添加内容,这里大家可以先手写一些静态数据进行测试,这里呢我就插入头部的添加数据的输入框进来 在app组件中,存在一个方法名为addTdo,用来添加数据的

image.png

添加数据的输入框在头组件中,那么就涉及到一个问题,如何让子组件给父组件传递数据呢,这里我们采用自定义事件的形式,自定义事件想要获取子组件给父组件传递的数据,那么就需要父组件提前预留一个方法,在合适的时机调用,那么这个合适的时机是指什么时候呢,是指子组件何时通知父组件,什么时候就是何合适的时机,我们再看头部组件

image.png

可以看到再input框中存在一个v-model指令和键盘事件,这里是针对输入框进行的事件,vmodel是典型的双向绑定事件,用来拿到输入框中的内容并且与data里面的titel绑定@是v-on事件的缩写用来注册方法,在这里我们可以看到,下方的methods事件中写入了一个add方法,并且与键盘事件向链接,当键盘按下回车键的时候,触发事件

image.png

再看事件函数内部,这里我们首先进行了判断,输入框内的东西是否为空,不为空才会进入下一步,再todoObj里写入了一个随机注册id的方法,并且将输入框中的内容存到对象中,这个done设置为flase(后续介绍) 重点在这里,我们通过点击事件通过$emit抛出一个事件,通知父组件这个时候执行addtodo方法(app组件定义的方法),并且将数据todoObj作为参数传递给父组件

  methods: {
    add() {
      if (!this.title.trim()) return alert("Please enter");
      const todoObj = {
        id: Math.random() * 10,
        title: this.title,
        done: false,
      };
      this.$emit("addTodo", todoObj);
      this.title = "";
    },
  },
};

至此,父组件拿到了子组件传递的数据,并且调用了addTdo方法,往数据todos里添加一个数据,此时app组件里面的todos有了数据,那么光有数据是不行的,还需将数据传递给子组件list用来渲染到页面上

父组件给子组件传递 父组件给子组件传递相对简单,只需要加上:(v-bind),将需要的数据写入,再子组件的props中接受一下,就完成了传递。

image.png

image.png

随后通过vfor指令,渲染item组件,注意注意,vfor指令一定要绑定唯一的key值!!

image.png 这样就会根据todos的数量进行渲染到页面上来

回过头我们去看item组件

item组件针对每一个小的任务做了样式和一些交互

image.png

分别是勾选,全选,删除,修改,在这里呢就用到了我们之前提到的事件总线 大家思考一下,如何确定你选中的任务就是数据里存的哪个任务的? 答案是通过id,那么就涉及到了如何把id传给父组件告诉父组件我点击勾选的是哪个呢 那么就涉及到我们的事件总线上场, 事件总线的使用的和自定义组件类似,事件总线要分清谁是需要数据的,谁是发送数据的 发送数据的只需要写一个方法通知事件总线,然后事件总线再通知其他组件并且携带数据过去 以这个为例,

image.png 在这里写入了两个方法,分别是选中和删除,并且与按钮绑定 当点击按钮的时候,会触发选中事件,并且把当前的id当作参数传入,事件总线的使用就是如此 this.bus.bus.emit(名称,参数),这样就是告诉事件总线,我给你传入了一个名字为名称的事情,帮我监控,谁用了这个名字,就把数据传给谁 反过来看接收数据方接收方需要再挂载前,通知事件总线,我要用名字为xxx的事件,并且当通知到我的时候,我触发this.xxx的事件来对传来的数据做一些处理

image.png 在这里就是,我要绑定勾选和删除这个名字,并且当子组件通知我的时候执行this.del方法, 作为接收方,我们需要再接收方事先预留一个函数,用来处理传来的数据

image.png 这样当子组件点击并且抛出勾选和删除事件并且传递过来对应的id值通知到父组件,父组件会再挂在前收到通知,并且绑定勾选和删除事件,随后触发预留的函数对数据进行一个处理。 在这里通过传来的id对todo里面的done(是否勾选),做一个取反,还有通过filter过滤进行删除。

还有一个事件修改事件,点击修改事件会触发,注意注意,再vue里给数据平白无故添加上一个属性,需要使用¥set和get方法,使用get和set方法才会为属性匹配一套set和get方法,才会保证数据改变从而引起页面改变 这里要介绍到另一个指令,ref,用来获取真实dom,大家都知,vue是虚拟dom然后生成真实dom,那么通过ref指令,可以获取到对应的dom进行操作,下面就通过this.$refs.inputTitle.focus();进行了聚焦

image.png

至此只剩下全选和本地存储未说明

全选就比较简单啦

image.png 我们事先已经给数据添加了一个done属性,就是用来判断是否已经选中,通过一系列的方法,我们只需要得出是否选中的数量等于数据全部的数量即可。大家仔细看,不多阐述

本地存储

相信大家应该都了解localStorage这个东西,如果不了解赶紧去补课。这里采用了深度观察的方式对todos监视,往本地存储里面存入新的值,因为todos是一个对象,需要转换成json 的格数传入

image.png

至此小案例就完成啦

后续再加上vuex的写法

已上传git gitee.com/wu-canhua/n…

---2023年10月28日20点35分-----