通過todoMVC來學(xué)vue.js的使用

這是vue官網(wǎng)的一個例子,挺適合作為vue應(yīng)用的入門的。
通過這個應(yīng)用,我們能學(xué)到vue的【雙向綁定】,【v-for】【事件】,【計(jì)算】,【指令】等的應(yīng)用。

應(yīng)用預(yù)覽

這個應(yīng)用開頭是這樣的


初始狀態(tài).png

后來是這樣的

增加一件事情以后

任務(wù)分析

作為一個to do 應(yīng)用,最基本的任務(wù)有三個

  1. 增加一個新的to do事項(xiàng)
  2. 標(biāo)記完成to do的事項(xiàng)
  3. 顯示to do的事項(xiàng)(未完成,完成,全部),能夠在三者的狀態(tài)下進(jìn)行切換
  4. 刪除to do的事項(xiàng)

針對任務(wù),我們最基礎(chǔ)的數(shù)據(jù)設(shè)計(jì)有幾個,一個是最基礎(chǔ)的to do事項(xiàng)存儲,最直觀的應(yīng)該是一個數(shù)組,存儲了所有的事項(xiàng),但是每個事項(xiàng)應(yīng)該有兩種狀態(tài),一個狀態(tài)是未完成,一個狀態(tài)是完成,所以每個設(shè)計(jì)成

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

這樣的結(jié)構(gòu),title表示事項(xiàng)的內(nèi)容,completed記錄事項(xiàng)的狀態(tài)。
所有的數(shù)據(jù)的改變都和前端交互有關(guān)。用戶每次添加一個新的事項(xiàng),就要往數(shù)組里面加元素,我們用變量newTodo來表示用戶新增加的事項(xiàng)的內(nèi)容,而狀態(tài)為completed:false,而用戶每次完成一個事項(xiàng),就會改變事項(xiàng)的狀態(tài),從completed:false到completed:true,而用戶刪除一個事項(xiàng),則要從數(shù)組中刪除事件。同時(shí)我們要可以顯示三種不同的事項(xiàng)狀態(tài)(All,Active,Completed),所以我們要用一個visibility來表示用戶選擇的狀態(tài)。

html結(jié)構(gòu)

首先是結(jié)構(gòu),主題是一個section標(biāo)簽,它分成三個主要部分,一個是header.header,一個是section.main,最后一個是footer.footer。

核心結(jié)構(gòu)
對應(yīng)的html

下面給出基礎(chǔ)結(jié)構(gòu)的html代碼,第一部分header.header里面主要是一個input來讓用戶添加待辦事項(xiàng)(todo)。第二部分主要是已經(jīng)列出事項(xiàng)列表,用ul和li來羅列事項(xiàng),同時(shí)在每個li里面增加div,每個div由三個部分組成,input來讓用戶標(biāo)注已經(jīng)完成,label標(biāo)注事項(xiàng)本身,button用來刪除事項(xiàng)。footer第一個span用來計(jì)數(shù),ul里面讓用戶切換三種狀態(tài),button用來清除已完成的事項(xiàng)。

<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. 添加事項(xiàng)
    首先,前面的html結(jié)構(gòu)中,有一個Input結(jié)構(gòu),我們通過input來得到用戶輸入,然后用戶在輸入enter鍵的時(shí)候,觸發(fā)事件,然后將用戶輸入的內(nèi)容增加都數(shù)組里面。
<input class="new-todo" autofocus autocomplete="off" 
placeholder="What needs to be done?">

這個時(shí)候,我們涉及到一個知識點(diǎn),就是雙向綁定,雙向綁定的概念就是,將input里面的value(用戶輸入事項(xiàng)內(nèi)容)和一個變量(這里的newTodo)綁定起來,input里面的事項(xiàng)內(nèi)容改變,newTodo的內(nèi)容也隨之改變,反之也是一樣。所以我們在input里面增加v-model屬性

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

然后再vue里面加入數(shù)據(jù)

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

但是我們同時(shí)還要將用戶輸入的事項(xiàng)增加到todos數(shù)組中,需要在用戶按下enter鍵的時(shí)候,觸發(fā)事件。這里涉及到了[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">

同時(shí)我們也要增加事件觸發(fā)以后的處理函數(shù),vue模塊的結(jié)果如下。

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 = '';
}
})

至此,我們已經(jīng)能夠愉快地添加事件了。
2. 顯示事項(xiàng)
顯示事項(xiàng)其實(shí)要區(qū)分事項(xiàng)的狀態(tài),從概覽里面我們已經(jīng)看到,總共分成三類事項(xiàng),一類是全部,一類是未完成,還有一類是已經(jīng)完成。所以我們要對事項(xiàng)有一個分類的提取,作者寫了一個filters來獲取不同狀態(tài)的事項(xiàng)列表。

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范圍里面,而且三種狀態(tài)的數(shù)據(jù)是根據(jù)data的數(shù)據(jù)來的,所以我們用到了computer[計(jì)算屬性](http://cn.vuejs.org/guide/computed.html)。
計(jì)算屬性用來解決依賴關(guān)系,比如用戶事項(xiàng)的三種狀態(tài),都依賴于todos數(shù)組生成,所以我們需要用到計(jì)算屬性來讓todos變化的時(shí)候,三種狀態(tài)的事項(xiàng)列表也能動態(tài)變化。然后我們用filterdTodos來表示最終要顯示的某種狀態(tài)的變量。

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

section.main應(yīng)該在todo的數(shù)據(jù)有元素的時(shí)候顯示, 顯然,我們應(yīng)該在todos數(shù)組不為空的時(shí)候,顯示,所以涉及到了[v-show](http://vuejs.org.cn/guide/conditional.html#v-show)

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

用來控制dom節(jié)點(diǎn)的display,當(dāng)todos里面的元素大于0的時(shí)候,節(jié)點(diǎn)就顯示,不然就隱藏。
另外還有一個v-cloak,可以配合`[v-cloak]{display:none}`來隱藏沒有渲染的節(jié)點(diǎn),不然你會看到很多的{{}}之類的文字。
當(dāng)用戶填寫了事項(xiàng)的時(shí)候,也就是todos里面有內(nèi)容了之后,下一步是顯示事項(xiàng),我們知道當(dāng)前的狀態(tài),要顯示的數(shù)據(jù)都在filteredTodos里面,所以利用[v-for](http://vuejs.org.cn/guide/list.html#值域-v-for)來循環(huán)生成列表.

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

這樣我們就能生成不同的li了。
針對每個事項(xiàng),我們知道有兩種狀態(tài),而我們想給已經(jīng)完成的事項(xiàng)顯示的時(shí)候,有刪除線的樣式,所以我們要根據(jù)事項(xiàng)的狀態(tài)來綁定[css](http://vuejs.org.cn/guide/class-and-style.html),根據(jù)todo的狀態(tài)來增加css。

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

>對于每個具體的事項(xiàng),我們有三個操作,一個是轉(zhuǎn)化狀態(tài),也就是利用左邊的input來切換,另外一個是編輯事項(xiàng),利用雙擊文本來實(shí)現(xiàn),另外一個功能是刪除事項(xiàng),利用右邊的X的button來完成。
1. 事項(xiàng)的狀態(tài)轉(zhuǎn)化
![具體事項(xiàng)](http://upload-images.jianshu.io/upload_images/2099962-4e1ace6fb72ee9ad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
li里面的結(jié)構(gòu)如下所示。

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

切換狀態(tài)用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. 事項(xiàng)的編輯
針對Input的可編輯狀態(tài),這里設(shè)置了一個能編輯的Input,也就是設(shè)置成text.綁定了 todo.title的數(shù)據(jù)。

<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的時(shí)候和焦點(diǎn)轉(zhuǎn)移的時(shí)候,將todo的內(nèi)容保存下來。

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)。指令是數(shù)據(jù)變化自動轉(zhuǎn)化為dom行為。如果editedTodo為todo,那么會觸發(fā)這個Input的指令。

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

指令現(xiàn)在的函數(shù)是update函數(shù),也就是說在數(shù)據(jù)更新的時(shí)候調(diào)用,如果剛開始進(jìn)入編輯狀態(tài),那么這時(shí)候value的值改成true,如果結(jié)束編輯,那么value的值為false,也就是只在進(jìn)入編輯狀態(tài)的時(shí)候指令出發(fā),然后把焦點(diǎn)轉(zhuǎn)移到當(dāng)前的input上面。這里有一個[nextTick](http://vuejs.org.cn/guide/reactivity.html#異步更新隊(duì)列),也就是在對dom直接進(jìn)行改動的時(shí)候,因?yàn)楫惒礁玛?duì)列的關(guān)系,所以我們要用nextTick不然可能會無效。
最后加一個取消編輯的命令,是用鍵盤的Esc來實(shí)現(xiàn)。
1. 顯示狀態(tài)切換
最后還有footer這部分,可以看到有三個部分,其他的不難,我們來想一下All,Active,Completed這三種狀態(tài)切換的問題吧。最簡單的,就是保存一個變量,這個變量可以決定要顯示的狀態(tài),然后通過點(diǎn)擊三個選項(xiàng)來切換狀態(tài)。然而作者選用了鏈接路由的方式。
![](http://upload-images.jianshu.io/upload_images/2099962-c208dc6c5cb1feb3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
html的結(jié)構(gòu)

<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代表三種狀態(tài),每個都是一個鏈接,鏈接是 `#/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();

代碼可以看到,每次點(diǎn)擊超鏈接,都會切換visibility的狀態(tài)。而vue的compute的屬性可以為每次的visiblity狀態(tài)切換改變前端的樣式。
1. 數(shù)據(jù)存儲
還記得我們剛開始的時(shí)候todos設(shè)置為空么,我們可以將數(shù)據(jù)存在localStorage來讓用戶下次訪問的時(shí)候,也能訪問到相應(yīng)的數(shù)據(jù),所以設(shè)置了一個
`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的時(shí)候,調(diào)用fetch函數(shù),而每次檢測到todos變化的時(shí)候,調(diào)用watch函數(shù)。
new Vue({
    data: {
      todos:todoStorage.fetch(),
      ...
    }
  ...
  watch: {
            todos: {
                handler: function (todos) {
                  todoStorage.save(todos);
                },
                deep: true
            }
        }
})

關(guān)鍵代碼

1.html結(jié)構(gòu)

<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.數(shù)據(jù)的存儲

(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容