通过todoMVC来学vue.js的使用

这是vue官网的一个例子,挺适合作为vue应用的入门的。
通过这个应用,我们能学到vue的【双向绑定】,【v-for】【事件】,【计算】,【指令】等的应用。

应用预览

这个应用开头是这样的


初始状态.png

后来是这样的

增加一件事情以后

任务分析

作为一个to do 应用,最基本的任务有三个

  1. 增加一个新的to do事项
  2. 标记完成to do的事项
  3. 显示to do的事项(未完成,完成,全部),能够在三者的状态下进行切换
  4. 删除to do的事项

针对任务,我们最基础的数据设计有几个,一个是最基础的to do事项存储,最直观的应该是一个数组,存储了所有的事项,但是每个事项应该有两种状态,一个状态是未完成,一个状态是完成,所以每个设计成

var todos = [
  {title:A,completed:true},
  {title:B,completed:false},
   ...
]

这样的结构,title表示事项的内容,completed记录事项的状态。
所有的数据的改变都和前端交互有关。用户每次添加一个新的事项,就要往数组里面加元素,我们用变量newTodo来表示用户新增加的事项的内容,而状态为completed:false,而用户每次完成一个事项,就会改变事项的状态,从completed:false到completed:true,而用户删除一个事项,则要从数组中删除事件。同时我们要可以显示三种不同的事项状态(All,Active,Completed),所以我们要用一个visibility来表示用户选择的状态。

html结构

首先是结构,主题是一个section标签,它分成三个主要部分,一个是header.header,一个是section.main,最后一个是footer.footer。

核心结构
对应的html

下面给出基础结构的html代码,第一部分header.header里面主要是一个input来让用户添加待办事项(todo)。第二部分主要是已经列出事项列表,用ul和li来罗列事项,同时在每个li里面增加div,每个div由三个部分组成,input来让用户标注已经完成,label标注事项本身,button用来删除事项。footer第一个span用来计数,ul里面让用户切换三种状态,button用来清除已完成的事项。

<section class="todoapp">
    <header class="header">
        <input class="new-todo"
            autofocus autocomplete="off"
            placeholder="What needs to be done?">
    </header>
    <section class="main" >
        <ul class="todo-list">
            <li class="todo">
                <div class="view">
                    <input class="toggle" type="checkbox">
                    <label> </label>
                    <button class="destroy"></button>
                </div>
                <input class="edit" type="text">
            </li>
        </ul>
    </section>
    <footer class="footer">
        <span class="todo-count">
            <strong></strong>left
        </span>
        <ul class="filters">
            <li><a href="#/all" >All</a></li>
            <li><a href="#/active" >Active</a></li>
            <li><a href="#/completed">Completed</a></li>
        </ul>
        <button class="clear-completed"> remaining">
            Clear completed
        </button>
    </footer>
</section>

Vue的使用

首先,绑定vue,绑定整个最外围的的section。

new Vue({
  // the root element that will be compiled
  el: '.todoapp',
})
  1. 添加事项
    首先,前面的html结构中,有一个Input结构,我们通过input来得到用户输入,然后用户在输入enter键的时候,触发事件,然后将用户输入的内容增加都数组里面。
<input class="new-todo" autofocus autocomplete="off" 
placeholder="What needs to be done?">

这个时候,我们涉及到一个知识点,就是双向绑定,双向绑定的概念就是,将input里面的value(用户输入事项内容)和一个变量(这里的newTodo)绑定起来,input里面的事项内容改变,newTodo的内容也随之改变,反之也是一样。所以我们在input里面增加v-model属性

<input class="new-todo"
    autofocus autocomplete="off"
    placeholder="What needs to be done?"
    v-model="newTodo">

然后再vue里面加入数据

new Vue({
data: {
newTodo: ''
})

但是我们同时还要将用户输入的事项增加到todos数组中,需要在用户按下enter键的时候,触发事件。这里涉及到了[vue事件](http://cn.vuejs.org/guide/events.html),vue事件是绑定到元素上面的,

<input class="new-todo"
autofocus autocomplete="off"
placeholder="What needs to be done?"
v-model="newTodo"
@keyup.enter="addTodo">

同时我们也要增加事件触发以后的处理函数,vue模块的结果如下。

new Vue({
data: {
todos: [],
newTodo: ''
},
methods: {
addTodo: function () {
var value = this.newTodo && this.newTodo.trim();
if (!value) {
return;
}
this.todos.push({ title: value, completed: false });
this.newTodo = '';
}
})

至此,我们已经能够愉快地添加事件了。
2. 显示事项
显示事项其实要区分事项的状态,从概览里面我们已经看到,总共分成三类事项,一类是全部,一类是未完成,还有一类是已经完成。所以我们要对事项有一个分类的提取,作者写了一个filters来获取不同状态的事项列表。

var filters = {
all: function (todos) {
return todos;
},
active: function (todos) {
return todos.filter(function (todo) {
return !todo.completed;
});
},
completed: function (todos) {
return todos.filter(function (todo) {
return todo.completed;
});
}
};

注意到,我们只是将todos绑定在vue的data范围里面,而且三种状态的数据是根据data的数据来的,所以我们用到了computer[计算属性](http://cn.vuejs.org/guide/computed.html)。
计算属性用来解决依赖关系,比如用户事项的三种状态,都依赖于todos数组生成,所以我们需要用到计算属性来让todos变化的时候,三种状态的事项列表也能动态变化。然后我们用filterdTodos来表示最终要显示的某种状态的变量。

new Vue({
data:{
...
visibility: 'all'
},
...
computed: {
filteredTodos: function () {
return filtersthis.visibility;
}
})

section.main应该在todo的数据有元素的时候显示, 显然,我们应该在todos数组不为空的时候,显示,所以涉及到了[v-show](http://vuejs.org.cn/guide/conditional.html#v-show)

<section class="main" v-show="todos.length" v-cloak>

用来控制dom节点的display,当todos里面的元素大于0的时候,节点就显示,不然就隐藏。
另外还有一个v-cloak,可以配合`[v-cloak]{display:none}`来隐藏没有渲染的节点,不然你会看到很多的{{}}之类的文字。
当用户填写了事项的时候,也就是todos里面有内容了之后,下一步是显示事项,我们知道当前的状态,要显示的数据都在filteredTodos里面,所以利用[v-for](http://vuejs.org.cn/guide/list.html#值域-v-for)来循环生成列表.

<li class="todo" v-for="todo in filteredTodos"></li>

这样我们就能生成不同的li了。
针对每个事项,我们知道有两种状态,而我们想给已经完成的事项显示的时候,有删除线的样式,所以我们要根据事项的状态来绑定[css](http://vuejs.org.cn/guide/class-and-style.html),根据todo的状态来增加css。

<li class="todo"
v-for="todo in filteredTodos"
:class="{completed: todo.completed, editing: todo == editedTodo}">

>对于每个具体的事项,我们有三个操作,一个是转化状态,也就是利用左边的input来切换,另外一个是编辑事项,利用双击文本来实现,另外一个功能是删除事项,利用右边的X的button来完成。
1. 事项的状态转化
![具体事项](http://upload-images.jianshu.io/upload_images/2099962-4e1ace6fb72ee9ad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
li里面的结构如下所示。

<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>

切换状态用todo.completed来表示,然后再分别绑定事件。

new Vue({
data:{
editedTodo: null
}
...
methods: {
removeTodo: function (todo) {
this.todos.$remove(todo);
},

      editTodo: function (todo) {
        this.beforeEditCache = todo.title;
        this.editedTodo = todo;
      },
     cancelEdit: function (todo) {
    this.editedTodo = null;
    todo.title = this.beforeEditCache;
}
  }

})

1. 事项的编辑
针对Input的可编辑状态,这里设置了一个能编辑的Input,也就是设置成text.绑定了 todo.title的数据。

<input class="edit" type="text"
v-model="todo.title"
v-todo-focus="todo == editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)">

其他容易理解,我们在键盘输入enter的时候和焦点转移的时候,将todo的内容保存下来。

new Vue({
...
methods: {
doneEdit: function (todo) {
if (!this.editedTodo) {
return;
}
this.editedTodo = null;
todo.title = todo.title.trim();
if (!todo.title) {
this.removeTodo(todo);
}
}
})

这里编辑了一个[指令](http://vuejs.org/guide/custom-directive.html)。指令是数据变化自动转化为dom行为。如果editedTodo为todo,那么会触发这个Input的指令。

directives: {
'todo-focus': function (value) {
if (!value) {
return;
}
var el = this.el;
Vue.nextTick(function () {
el.focus();
});
}
}

指令现在的函数是update函数,也就是说在数据更新的时候调用,如果刚开始进入编辑状态,那么这时候value的值改成true,如果结束编辑,那么value的值为false,也就是只在进入编辑状态的时候指令出发,然后把焦点转移到当前的input上面。这里有一个[nextTick](http://vuejs.org.cn/guide/reactivity.html#异步更新队列),也就是在对dom直接进行改动的时候,因为异步更新队列的关系,所以我们要用nextTick不然可能会无效。
最后加一个取消编辑的命令,是用键盘的Esc来实现。
1. 显示状态切换
最后还有footer这部分,可以看到有三个部分,其他的不难,我们来想一下All,Active,Completed这三种状态切换的问题吧。最简单的,就是保存一个变量,这个变量可以决定要显示的状态,然后通过点击三个选项来切换状态。然而作者选用了链接路由的方式。
![](http://upload-images.jianshu.io/upload_images/2099962-c208dc6c5cb1feb3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
html的结构

<footer class="footer" v-show="todos.length" v-cloak>
<span class="todo-count">
<strong v-text="remaining"></strong> {{remaining | pluralize 'item'}} left
</span>
<ul class="filters">
<li><a href="#/all" :class="{selected: visibility == 'all'}">All</a></li>
<li><a href="#/active" :class="{selected: visibility == 'active'}">Active</a></li>
<li><a href="#/completed" :class="{selected: visibility == 'completed'}">Completed</a></li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
Clear completed
</button>
</footer>

我们可以看到ul.filters的下面有三个li代表三种状态,每个都是一个链接,链接是 `#/url`这样的格式。首先要了解routes.js

var router = new Router();
['all', 'active', 'completed'].forEach(function (visibility) {
router.on(visibility, function () {
app.visibility = visibility;
});
});
router.configure({
notfound: function () {
window.location.hash = '';
app.visibility = 'all';
}
});
router.init();

代码可以看到,每次点击超链接,都会切换visibility的状态。而vue的compute的属性可以为每次的visiblity状态切换改变前端的样式。
1. 数据存储
还记得我们刚开始的时候todos设置为空么,我们可以将数据存在localStorage来让用户下次访问的时候,也能访问到相应的数据,所以设置了一个
`store.js`

/*jshint unused:false */
(function (exports) {
'use strict';
var STORAGE_KEY = 'todos-vuejs';
exports.todoStorage = {
fetch: function () {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
},
save: function (todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
};
})(window);

然后我们在初始化todos的时候,调用fetch函数,而每次检测到todos变化的时候,调用watch函数。
new Vue({
    data: {
      todos:todoStorage.fetch(),
      ...
    }
  ...
  watch: {
            todos: {
                handler: function (todos) {
                  todoStorage.save(todos);
                },
                deep: true
            }
        }
})

关键代码

1.html结构

<section class="todoapp">
  <header class="header">
    <h1>todos</h1>
    <input class="new-todo"
        autofocus autocomplete="off"
        placeholder="What needs to be done?"
        v-model="newTodo"
        @keyup.enter="addTodo">
  </header>
  <section class="main" v-show="todos.length" v-cloak>
    <input class="toggle-all" type="checkbox" v-model="allDone">
    <ul class="todo-list">
        <li class="todo"
            v-for="todo in filteredTodos"
            :class="{completed: todo.completed, editing: todo == editedTodo}">
            <div class="view">
                <input class="toggle" type="checkbox" v-model="todo.completed">
                <label @dblclick="editTodo(todo)">{{todo.title}}</label>
                <button class="destroy" @click="removeTodo(todo)"></button>
            </div>
            <input class="edit" type="text"
                v-model="todo.title"
                v-todo-focus="todo == editedTodo"
                @blur="doneEdit(todo)"
                @keyup.enter="doneEdit(todo)"
                @keyup.esc="cancelEdit(todo)">
        </li>
    </ul>
  </section>
  <footer class="footer" v-show="todos.length" v-cloak>
    <span class="todo-count">
        <strong v-text="remaining"></strong> {{remaining | pluralize 'item'}} left
    </span>
    <ul class="filters">
        <li><a href="#/all" :class="{selected: visibility == 'all'}">All</a></li>
        <li><a href="#/active" :class="{selected: visibility == 'active'}">Active</a></li>
        <li><a href="#/completed" :class="{selected: visibility == 'completed'}">Completed</a></li>
    </ul>
    <button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
        Clear completed
    </button>
  </footer>
</section>
<script src="vue.js"></script>
<script src="js/director.js"></script>
<script src="js/store.js"></script>
<script src="js/app.js"></script>
<script src="js/routes.js"></script>

2.app.js

/*global Vue, todoStorage */
(function (exports) {
    'use strict';
    var filters = {
        all: function (todos) {
            return todos;
        },
        active: function (todos) {
            return todos.filter(function (todo) {
                return !todo.completed;
            });
        },
        completed: function (todos) {
            return todos.filter(function (todo) {
                return todo.completed;
            });
        }
    };
    exports.app = new Vue({
        // the root element that will be compiled
        el: '.todoapp',
        // app initial state
        data: {
            todos: todoStorage.fetch(),
            newTodo: '',
            editedTodo: null,
            visibility: 'all'
        },
        // watch todos change for localStorage persistence
        watch: {
            todos: {
                handler: function (todos) {
                  todoStorage.save(todos);
                },
                deep: true
            }
        },
        // computed properties
        // http://vuejs.org/guide/computed.html
        computed: {
            filteredTodos: function () {
                return filters[this.visibility](this.todos);
            },
            remaining: function () {
                return filters.active(this.todos).length;
            },
            allDone: {
                get: function () {
                    return this.remaining === 0;
                },
                set: function (value) {
                    this.todos.forEach(function (todo) {
                        todo.completed = value;
                    });
                }
            }
        },
        // methods that implement data logic.
        // note there's no DOM manipulation here at all.
        methods: {
            addTodo: function () {
                var value = this.newTodo && this.newTodo.trim();
                if (!value) {
                    return;
                }
                this.todos.push({ title: value, completed: false });
                this.newTodo = '';
            },
            removeTodo: function (todo) {
                this.todos.$remove(todo);
            },
            editTodo: function (todo) {
                this.beforeEditCache = todo.title;
                this.editedTodo = todo;
            },
            doneEdit: function (todo) {
                if (!this.editedTodo) {
                    return;
                }
                this.editedTodo = null;
                todo.title = todo.title.trim();
                if (!todo.title) {
                    this.removeTodo(todo);
                }
            },

            cancelEdit: function (todo) {
                this.editedTodo = null;
                todo.title = this.beforeEditCache;
            },
            removeCompleted: function () {
                this.todos = filters.active(this.todos);
            }
        },
        // a custom directive to wait for the DOM to be updated
        // before focusing on the input field.
        // http://vuejs.org/guide/custom-directive.html
        directives: {
            'todo-focus': function (value) {
                if (!value) {
                    return;
                }
                var el = this.el;
                Vue.nextTick(function () {
                    el.focus();
                });
            }
        }
    });
})(window);

3.数据的存储

(function (exports) {
    'use strict';
    var STORAGE_KEY = 'todos-vuejs';
    exports.todoStorage = {
        fetch: function () {
            return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
        },
        save: function (todos) {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
        }
    };
})(window);

webpack+vue模板
https://github.com/vuejs-templates/webpack

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容