学习vuex之写一个todo
03 Jan 20172017年的第一篇博客诞生了,虽然又是一个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。
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,是有点复杂了,遇到了主要两个问题:
-
state赋值时,为了避免物理存储的操作和state的操作互相影响,初始化赋值时需要给state做拷贝赋值,但过程中,我只做了数组slice拷贝 ,没留意到数组元素是object的情况,导致object还是共用的影响,一些莫名其妙的bug。
-
添加多个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