我们来用vue写一个简单的todo list。

需求和组件分析

一个todo应用其实没有很多需求,能满足输入、添加事件、标注事件完成、分组展示这几点即可。因此我们的面板中需要:

  • 输入框
  • 事件列表
  • 分组标签 其实上面三项写在一个组件中即可完成所有工作,但是为了方便了解组件之间的交互,我们会把事件列表单独拆出一个组件来。

另外我们这里会用localStorage来在本机存储事件列表。当然如果你想也可以通过服务器来存取数据。

跟技术栈有关的一些东西

使用vue-cli创建项目时,会帮我们自动配置好webpack打包工具和热部署工具,这一方面就不多赘述了。

在写style的时候,我们会使用stylus这个动态css语言来代替css。在项目文件夹中,我们要安装一下stylus相应的包:

1
$ npm install stylus stylus-loader --save-dev

安装完后,我们在vue中的<style>标签里要讲语言声明为stylus:

1
2
<style lang="stylus">
</style>

如果你使用webstorm来编写vue的话,为了让IDE能顺利识别出语法,还要标注一句:

1
2
<style lang="stylus" rel="stylesheet/stylus" >
</style>

关于stylus的介绍可以移步 Stylus

另外,各大IDE应该都会有vue相关的插件,推荐安装一下,便于标注代码高亮和这确识别语法。

例如在WS中,可以打开设置,在plugn选项中选择Browse respositories,在窗口中搜索vue即可找到。

除此之外,推荐下载Chrome的vue dev-tools,方便查看和分析状态和内容。

app.vue定义必要的数据和方法

第一步我们要在app.vue文件中把我们的一些必要的数据和方法给写出来。

打开app.vue,在script标签中添加data对象并向里面添加内容。

按照需求,我们先定义出一个todo对象的结构,用label来记录事件内容,用ifFinished来标注事情是否已经完成。

Todo: {label: String, isFinished: Boolean }

在这个应用中,我们还需要这几项内容:

  • newTodo: Todo 用来与输入框中新建的事件进行绑定;
  • todos: Array 用来存放Todo的数组;
  • tab: String 用来标注当前页的标签,有三个可选值:all, active, finished。 因此我们在data中这么输入
1
2
3
4
5
6
7
8
9
10
11
12
exprot default {
data() {
return {
newTodo: ''
todos: [],
tab: ‘All’,
tabList:[
'All', 'Active', 'Finished'
]
}
}
}

完成输入的功能

我们先来完成录入内容的功能。

我们需要一个输入框,并将它与newTodo.label中的内容绑定。在填完内容按下return键时,我们将newTodo的内容添加到todos数组中。

template里添加一个输入框,将它绑定数据并定义方法:

1
2
3
4
5
<template>
<div id="todo">
<input v-model="newTodo" @keyup.enter="handleAddNew"/>
</div>
</template>

然后我们在methods中定义方法来接收事件:

1
2
3
4
5
6
methods: {
handleAddNew(){
this.newTodo.length !== 0 ? this.todos.push({label: this.newTodo, isFinished: false}) : void(0);
this.newTodo = '';
}
}

特别注意

这里要特别注意的是,在data中定义newTodo时不要直接定义整个对象(如: newTodo: {label:'', isFinished: false }),否则传入todos数组时,只会传入newTodo对象的引用。

输入内容,按下return,我们打开调试工具就可以看到todos数组中加入的内容了。

添加列表功能

完成了输入功能,我们现在需要对todos中的内容进行展示。 新建一个list.vue组件。

在这个组件中,我们需要定义父组件app.vue传来的参数,因此在script标签中,我们定义一下参数,再添加一个handleFinish方法,让事件在被点击的时候标记成已完成。

1
2
3
4
5
6
7
8
9
export default{
name: 'list',
props: ['todos'],
methods: {
handleFinished(item){
this.$emit('handleFinished', item);
}
}
}

然后我们在template中循环输出父组件发过来的todos数组。

1
2
3
4
5
<template>
<ul>
<li v-for="item in todos" @click="handleFinished(item)">{{ item.label }}</li>
</ul>
</template>

我们还要对已完成的项目进行一下标注。因此我们在该项目已完成时为li标签绑定一个类。 在li标签里这么写

1
<li v-for="item in todos" @click="handleFinished(item)" :class="{finished: item.isFinished}">{{ item.label }}</li>

style标签里定义一下这个类:

1
2
3
4
5
6
<style lang="stylus" rel="stylesheet/stylus" scoped>
.finished {
color #ccc
text-decoration line-through
}
</style>

这里解释一下scoped: 在style标签中加上scoped属性后,当前组件中所对应的样式,只会在当前组件中被渲染,而不会影响其他外部组件。如图所示:

特别注意

这里有一点是需要特别注意的: 在当前组件的·handleFinished·方法中,我们如果在这里直接修改item中的值(例如: handleFinished(item){ item.isFinished = !item.isFinished }),其实是有效可行的,修改后父组件中todos数组内的相应对象相应值也会改变。但是为了保证应用中的数据单向流动,推荐把动作一步一步冒泡传输给父组件,并在父组件里进行想应改动,并传递给子组件。

在父组件中添加相应的事件接收方法

完成了子组件的操作,我们现在在父组件中添加相对应的接收方法。首先我们要引入子组件,并在template中子组件标签的中添加传参和接事件的属性:

首先在script标签中引入子组件,并在components对象里添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div id="todo">
<input v-model="newTodo" @keyup.enter="handleAddNew"/>
<list :todos="todos" @handleFinished="handleFinished"></list>
</div>
</template>
<script>
import List from './components/list'

export default{
//...
components: {
List
}
//...
}
</script>

然后我们往methods对象里添加handleFinished方法:

1
2
3
4
5
6
7
8
9
10
export default{
//...
methods:{
//...
handleFinished(item){
item.isFinished = !item.isFinished
}
}
//...
}

这里要解释的一点是,我们在<list></list>标签中写上的@handleFinished="handleFinished"的意思是:

当子组件向我们发送’handleFinished’事件时(对应 v-on: handleFinished),我们去调用当前组件中的”handleFinished”方法(对应 =“handleFinished”)

到这一步写完后,当前的todo应用就基本上是已经动态可用了。

实现localStorage存储数据

要使用localStorage来存储数据,我们首先要定义Store对象,并添加两个方法:

  • 保存数据 save(todos: Array): void
  • 读取数据 fetch(): Array 为了简便我们直接把Store对象写在app.vue里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
import ...
const STORAGE_KEY = 'vue-todo';
let Store = {
save(items){
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
},
fetch(){
return JSON.parse(window.localStorage.getItem(STORAGE_KEY)) || []
}
};
export default {
//...
}
</script>

然后,我们给data中的todos设定当组件生成时从localStorage中取值:

1
2
3
4
5
6
7
8
9
10
export default {
//...
data(){
return {
//...
todos: Store.fetch() || [],
//...
}
}
}

再添加一个todos的监听对象来帮助我们在todos的值发生改变时自动保存:

1
2
3
4
5
6
7
8
9
10
11
export default {
//...
watch: {
todos: {
handler: function(items) {
Store.save(items)
},
deep: true
}
}
}

到此我们就完成了数据的加载和保存。

数据的处理:标签切换

在data中我们预留了一个tab值用来标志当前的标签。现在我们来完成标签切换这一部分的内容。

首先在template中加入我们的标签代码块:

1
2
3
<div class="tabs">
<span v-for="item in tabList" :class="['tab' , {'active': isTabActive(item)}]" @click="toggleTab(item)">{{ item }}</span>
</div>

为了方便切换标签和标注标签的active属性,我们在methods里面会定义两个方法:

1
2
3
4
5
6
7
8
methods{
isTabActive(tabName){
return tabName === this.tab
},
toggleTab(tabName){
this.tab = tabName;
}
}

我们在style中最好也加上一些相应的样式。

1
2
3
4
5
6
7
8
.tab {
margin 10px
color #777
&.active {
color #2c3e50
font-weight 700
}
}

添加之后,你点击屏幕上的标签时,就可以查看到标签的状态能够正常切换了。如果这一步没有问题,我们就来进行最后一步数据处理。

和数据存储一样,我们还是要新建一个Filters对象来帮我们处理数据。 我们在Store对象底下添加Filters对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let Filters = {
All(items){
return items
},
Active(items){
return items.filter(item => {
return !item.isFinished
})
},
Finished(items){
return items.filter(item => {
return item.isFinished
})
}
};

然后我们向computed对象中添加根据当前tab输出相应todo数组的方法:

1
2
3
4
5
computed: {
filters(){
return Filters[this.tab](this.todos)
}
}

template里把相应的todos替换成computed对象中的相应计算方法:

1
<list :todos="filters" @handleFinished="handleFinished"></list>

现在当你点击不同的tab标签时,也会根据相应的标签展示出不同的事件。

到此,一个简单的todo应用就写完了。 当前项目的源码已经扔到github上:vue-todo-list at demo-version