学习vuex之写一个todo

2017年的第一篇博客诞生了,虽然又是一个demo,最近学习了下vuex,试着实现一下todo功能。

首先过一遍vuex的文档

可能看第一遍还是有点懵,一边写代码一边领悟效果更好。

vuex的主要几个概念:state、mutations、actions、getters、modules

另外vuex封装了几个辅助函数主要用于各个业务子组件的方便调用 mapXXX系列

而那几个概念,都封在store对象里,而store则注入到Vue之中。

vue页面用到的数据绑定的数据,可以间接的利用store的state而不再是用自己声明的data对象了,这样达到对数据的一个统一。

比如,我们在state里定义了数据todos来存储所有待办事项内容。

那么我们在组件里就可以通过$store.state.todos来访问到数据todos,如果仅仅是要对todos做一些过滤数据处理获取特定数据(如未完成事项),可以在store里定义getters方法来返回todos的处理数据,通过$store.getters.xxx来调用。

涉及到需要调整state数据的内容,利用mutations和actions来处理,mutations类似事件,通过store.commit(‘type’)来提交type的mutations,此时mutations就会回调 名为type的mutations,执行逻辑内对state进行数据操作,mutations是同步的。

所以如果涉及异步接口,比如需要先上传数据到后台,由后台提示成功后才能做处理时就不能直接操作mutations了,定义actions,commit的操作交由actions内实现的异步回调来,组件通过store this.$store.dispatch('fetchTodos')来分发actions。

image 查看原图

todo我分了三个子组件 AddTodo\TodoItem\TodoList(一个新增表单、一个事项组件、一个事项列表)

todo主要以本地存储为主,所以用localstorage来存储数据即可。

以下为store的主要内容: 包含localstorage的操作、以及store定义

import Vue from 'vue';
import Vuex from 'vuex';
import VueResource from 'vue-resource';

Vue.use(Vuex);
Vue.use(VueResource);
let idIndex = 0;

const gtoDos = (function(){
  var todos = [];
  var obj = function(){
    this.init();
    return this;
  };
  //因为我这里用的一个闭包来作为后台存储数据,与vue的state用到的数据相对独立
  //赋值state时得做好数组拷贝,注意元素为对象时的拷贝处理
  function copyArr(arr){
    return arr.map((e)=>{
      if(typeof e === 'object'){
        return Object.assign({},e)
      }else{
        return e
      }
    })
  }
  obj.prototype = {
    init(){

      var stores = window.localStorage.getItem('vtd-todos');
      if(stores){
        todos = JSON.parse(stores);
      }
    },
    getTodos() {
      return copyArr(todos)//.slice();
    },
    addTodo(todo) {
      todos.push(todo);
      this.refreshTodos();
    },
    deleteTodo(id) {
      todos = todos.filter(e=>{
        if(e.id==id){
          return false;
        }
        return true;
      });
      this.refreshTodos();
    },
    toggleTodo(id) {
      todos = todos.map(e=>{
        if(e.id==id){
          e.done = !e.done;
        }
        return e;
      });
      this.refreshTodos();
    },
    editTodo(item) {
      todos = todos.map(e=>{
        if(e.id==item.id){
          e = item;
        }
        return e;
      });
      this.refreshTodos();
    },
    refreshTodos() {
      window.localStorage.setItem('vtd-todos', JSON.stringify(todos));
    }
  };
  return new obj();
})();

export default new Vuex.Store({
  state: {
    todos:[
    
    ]
  },
  //view model的操作,针对state的操作
  mutations: {
    getTodos (state, list){
      var list = list.sort((a,b)=>a.id<b.id);
      idIndex = list.length && list[0].id || 0;
      state.todos = list;
    },
    addTodo (state, one){
      state.todos.unshift(Object.assign({}, one));
    },
    editTodo (state, one){
      let index=-1;
      state.todos.forEach((e,i)=>{
        if(e.id==one.id){
          index = i
        }
      })
      index>-1?state.todos[index].desc = one.desc:''
    },
    toggleTodo (state, item){
      let index=-1,
        status;
      state.todos.forEach((e,i)=>{
        if(e.id==item.id){
          index = i
          status = e.done
        }
      })
      //console.log('mutations ',index ,status, item.done)
      index>-1?state.todos[index].done = !status:''
    },
    deleteTodo (state, item){
      let index = -1;
      state.todos.filter((e,i)=>{
        if(e.id==item.id){
          index = i
        }
      })
      index>-1?state.todos.splice(index,1):''
    }
  },
  getters:{
    doneTodos: state=>{
      //console.log('get dones')
      let done = state.todos.filter(todo => todo.done)
      //console.log(done);
      return done
    },
    undoneTodos: state=>{
      return state.todos.filter(todo => !todo.done)
    }
  },

  actions:{
    fetchTodos ({commit}){
       const res = gtoDos.getTodos();
       commit('getTodos',res);
    },
    newTodo ({commit}, todo){
      idIndex++;
      todo.id = idIndex;
      gtoDos.addTodo(todo);
      commit('addTodo', todo);
    },
    toggleTodo ({commit}, item){
      gtoDos.toggleTodo(item.id);
      commit('toggleTodo', item);
    },
    deleteTodo  ({commit}, todo){
      gtoDos.deleteTodo(todo.id);
      commit('deleteTodo', todo);
    },
    editTodo ({commit}, todo){
      gtoDos.editTodo(todo);
      commit('editTodo', todo);
    }
  }
})

在main.js里注入store到根组件即可使用store

import Vue from 'vue'
import App from './App'
import store from './store/index'

new Vue({
  el: '#app',
  template: '<App/>',
  store,
  components: { App }
})

新增表单

新增表单,相对简单,无需返回state数据,在data定义input来做双向绑定处理,提交数据时直接用input来构造新数据并分发actions

<template>
  <div class="add-to-do">
    <h1><i class="glyphicon glyphicon-time"></i> To Do </h1>
    <form v-on:submit.prevent="onSubmit" role="form" class="form-horizontal" >
      <div class="form-group">
        <div class="col-sm-10">
          <input type="text" class="form-control" v-model="input" placeholder="输入事项~">
        </div>
          <button type="submit" class="btn btn-info col-sm-2">提交</button>
      </div>
    </form>
  </div>
</template>

<script>

import { mapActions } from 'vuex'

export default {
  name: 'AddToDo',
  data: function(){
    return {
      input:''
    }
  },
  created:function(){
    
  },
  methods:{
    onSubmit:function(){
      const todo = {
        done : false,
        desc : '',
        time : (new Date())
      };
      if(this.input==''){
        alert('不能为空');
        return;
      }
      todo.desc = this.input;
      //this.input = '';
      //通过dispatch分发actions,actions来处理数据,actions可以返回promise,然后由业务逻辑这边做相应处理
      this.$store.dispatch('newTodo', todo).then(()=>{
        this.input = '';
      }, ()=>{
        alert('出错');
      });

    }
  }
}
</script>

由于数据绑定的缘故 ,我们在列表组件那做好state的引用,当state发生变化时,页面显示自然也跟着变化。

具体事项

事项是列表的细化单位,一般具备显示跟删除功能,但todo还包含设置完成和修改事项的功能,所以实现的内容不会比新增数据简单。

这里还涉及到双击事项启动编辑功能以及自动聚焦的功能。

<template>
  <li class="todo-item" :class='{editing: editable}'>
    <div class="view">
      <input type="checkbox" class="cb" v-detect="item.done" @change="toggleTodo(item)">
      <label v-on:dblclick="toEdit()"></label>
      <a class="delete" @click="deleteItem">×</a>
    </div>
    <div class="col-sm-10 edit-input">
      <input type="text" class="form-control" v-auto-focus="editable" :value="item.desc"
      @keyup.enter="doneEdit"
      @keyup.esc="cancelEdit"
      @blur="doneEdit">
    </div>
  </li>
</template>

<script>
import Vue from 'vue';

export default {
  name: 'TodoItem',
  //读取父组件传入的item
  props: ['item'],
  data: function(){
    return {
      input:'',
      //标识是否进入编辑
      editable:false
    }
  },
  directives:{
    //定义指令: 监听数据设置checked值,回避一些奇怪的问题
    detect:function(el, binding){
       // console.log(el.checked, binding.value)
        el.checked = binding.value
    },
    'auto-focus': function(el, binding){
      //console.log(binding.value);
      if(binding.value){
        el.focus();
      }
    }
  },
  created:function(){
    
  },
  methods:{
    doneEdit (e) {
      const value = e.target.value.trim();
      const { item } = this;
      if (!value) {
        this.deleteItem();
      } else if (this.editable) {
        item.desc = value;
        //分发编辑处理
        this.$store.dispatch('editTodo', item);
        this.editable = false
      }
    },
    cancelEdit (e) {
      e.target.value = this.item.desc
      this.editable = false
    },
    toEdit(){
      this.editable = true;
    },
    deleteItem (){
      const todo = this.item;
     //分发删除操作
      this.$store.dispatch('deleteTodo', todo);

    },
    toggleTodo (){
      const todo = this.item;
     /*console.log('组件点击',todo.done);*/
     //分发切换事项状态操作
      this.$store.dispatch('toggleTodo', todo);
    }
  }
}
</script>

列表组件

列表的功能,相对较少,处理传递子组件数据外,多了个切换显示事项功能。

显示完成和未完成的事项,可调用store的getters来实现,不需要发动到数据处理。

<template>
  <div class="to-do-list">
    <ul class="todo-types">
      <li v-for="(obj, key) in filters" class="btn btn-default"
       :class="{'btn-success': key==visiableType}"
        role="button" @click="visiableType=key">
        </li>
    </ul>
    <p v-show="filterTodos.length==0" style="text-align: center;">暂无对应信息</p>
    <ul class="todo-list">
      <TodoItem v-for='todo in filterTodos' :item="todo"></TodoItem>
    </ul>
  </div>
</template>

<script>
import TodoItem from './TodoItem'
import {mapGetters} from 'vuex'

const filters = {
  'all': { 
    type:'all',
    desc:'所有'  
  },
  'done':{
    type:'done',
    desc:'已完成'
  },
  'undone':{
    type:'undone',
    desc:'待完成'
  }
};

export default {
  name: 'TodoList',
  data:function(){
    return {
      visiableType:'all',
      filters:filters
    }
  },
  created:function(){
    this.$store.dispatch('fetchTodos');
  },
  components: {TodoItem},
  computed:{
    filterTodos (){
      return this[this.filters[this.visiableType]['type']];
    },
    all (){
      return this.$store.state.todos;
    },
    ...mapGetters({
      done: 'doneTodos',
      undone:'undoneTodos'
    })
  }
}
</script>

总结:

对vuex有了进一步的理解后,很快掌握其开发关键点,而且起初我实现的是异步接口与后台的对接实现,后来改成本地存储,结果发现,只需要修改store的actions实现即可,分工明确,相当不错,期间也补充了自己对vue一些认识的不足,比如computed是属性而不是方法等理解,自定义指令的使用。

更新:

一开始实现的不是很好, 当然现在的实现跟vuex官方实现例子也是不大一样的,官方基本就用state来存储数据,然后监听store操作及时更新localstorage,而我这里用了一个对象来存储数据和更新localstorage,是有点复杂了,遇到了主要两个问题:

  1. state赋值时,为了避免物理存储的操作和state的操作互相影响,初始化赋值时需要给state做拷贝赋值,但过程中,我只做了数组slice拷贝 ,没留意到数组元素是object的情况,导致object还是共用的影响,一些莫名其妙的bug。

  2. 添加多个todo后,将其标识为完成,切换到已完成状态下,依次按新到旧的顺序重置已完成项时,checked值并没有及时更新,怀疑是dom复用的问题,现在还是没有分析出真正原因,产生显示效果是,旧的事项变成未完成状态,但实际还是已完成状态,为此我改为用自定义指令来监听数据,并调整checked。

demo地址:http://shellphon.wang/demo-codes/vuetodo/index.html

源码地址: https://github.com/shellphon/demo-codes/tree/master/mvm/vue-to-do