使用Vue.js和Vuex 2.0构建一个简单的Todo应用程序
这篇文章将介绍使用最新版本的Vue.js和Vuex构建应用程序的一些基本知识。
Vue.js(读音
/vjuː/
,类似于 view) 是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与单文件组件和 Vue 生态系统支持的库结合使用时,Vue 也完全能够为复杂的单页应用程序提供驱动。
新版本的Vue中已经添加了许多优秀的特性,比如virtual-DOM。
**译者注:**如果你和我一样,对Virtual-DOM不熟悉,建议移步阅读下面有关于这方面的文档:
Vuex也更新了。在这篇文章中,我们将讨论如何基于Vuex的最新版本来创建一个Todo应用程序,允许用户对Todo应用程序进行添加、编辑、完成和删除等任务。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它的灵感来自于Flux和Redux。Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
开始
Vue已经简化了使用Vue-cli的过程。Vue社区的许多人也通过它创建项目,并为此做出很多贡献。在这篇文章中,我们将使用一个名为webpack-simeple-2.0
的模板来创建项目。
**译者注:**我在跟着教程做Todo应用程序的时候,采用的也是Vue-CLI来创建项目,但我采用的是
webpack
模板。操作非常简单。在这个模板中,我安装了vue-router
,但放弃了ESLint和单元测试等功能。
vue init webpack simple-todo
执行完上面的命令之后,你的命令终端可以看到像下图这样的信息:
接下来需要安装项目所需的依赖关系:
npm i
在大天朝使用npm i
经常会碰到一些资源被墙,你可以采用cnpm i
来做,我这里采用的是tnpm i
来替代npm i
安装一些依赖关系:
安装好项目的依赖关系之外,在命令终端执行
npm run dev
你的浏览器会自动启用http://localhost:8080/#/
地址,访问Todo应用程序。你在浏览器中将看到的效果如下:
安装过程很简单,但是由于这个设置与Vuex没有关系,我们将不得不将它导入我们的项目中:
npm install vuex@next
执行上面的命令你将安装Vuex的最新版本。
**译者注:**最终换成了
tnpm i vuex --save
安装的vuex。但这样安装的不是最新版本的vuex
在我们的项目中,我们已经有了一个组件和Vue
实例。让我们把里面的内容删除掉(删除src/components
中的Hello.vue
组件,清理src/App.vue
文件中的代码):
<!-- 修改App.vue文件 -->
<div id=”app”>
</div>
修改src/router/index.js
文件:
<!-- 修改router/index.js文件 -->
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [{
path: '/'
}]
})
同样把App.vue
中export
表达式的内容了清理干净,清理后的代码看上去像这样:
<!-- 修改App.vue文件 -->
<template>
<div id="app">
</div>
</template>
<script>
export default {
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
}
</style>
此时浏览器看到的是一个空白页,什么内容都没有:
但不用紧张,随着后面的内容完善,我们的页面效果会出来的。
Vuex Store
让我们从创建我们的Vuex store(仓库)开始。在src
目录下创建一个store
的文件夹,并在这个文件夹中新建一个store.js
文件。在这个文件中将包含我们所需要的store。
每一个 Vuex 应用的核心就是 store(仓库)。"store" 基本上就是一个容器,它包含着你的应用中大部分的状态(state)。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交(commit) mutations。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
在早期的Vuex版本中,每一个操作需要在store
中创建单独的文件。Vuex的最新特点之一是能够在一个store
文件中定义actions
和getter
。这样做,使用一个模块加载更加便捷,更易组合。这是store
的基本结构:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
}
})
如果您和我一样是Vuex的新手,那么有关于state、mutations和actions这些术语可能会让人感到困惑。然而,背后的概念非常简单。我们创建一个state
对象,当应用程序启动时,它保持初始状态。接下来,我们创建一个对象来存储我们的mutations
。在Vuex中mutations
基本上是事件。最后,我们创建一个对象来存储我们的actions
。actions
是用来分派mutations
的函数。我们完成的store.js
看上去像这样:
// 修改src/store/store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todos: [],
newTodo: ''
},
mutations: {
GET_TODO(state, todo) {
state.newTodo = todo
},
ADD_TODO(state) {
state.todos.push({
body: state.newTodo,
completed: false
})
},
EDIT_TODO(state, todo) {
var todos = state.todos
todos.splice(todos.indexOf(todo), 1)
state.todos = todos
state.newTodo = todo.body
},
REMOVE_TODO(state, todo) {
todo.completed = !todo.completed
},
CLEAR_TODO(state) {
state.newTodo = ''
}
},
actions: {
getTodo({ commit }, todo) {
commit('GET_TODO', todo)
},
addTodo({ commit }) {
commit('ADD_TODO')
},
editTodo({ commit }, todo) {
commit('EDIT_TODO', todo)
},
removeTodo({ commit }, todo) {
commit('REMOVE_TODO', todo)
},
completeTodo({ commit }, todo) {
commit('COMPLETE_TODO', todo)
},
clearTodo({ commit }) {
commit('CLEAR_TODO')
}
}
})
State
当我们的应用程序启动时,初始状态是todos
的空列表([]
),它将存储我们新添加的todos
和一个空的字符串,该字符串将保留添加到todos
。
Mutations
每个mutation
都有一个字符串的事件类型(type
)和一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state
作为第一个参数。习惯上把mutation
的每一个回调函数的命名都用大写的字母,主要是方便它们与普通的函数区分开来。GET_STATE
接受用户输入的值并将其赋值给state.newTodo
。当触发ADD_TODO
时,添加了一个新的todo
到我们的todos
列表 —— 将body
设置为state.newTodo
和completed
开始被设置为false
。这些mutations
很简单,所以我们不会花太多时间来详细解释它们。
Actions
Vuex的actions
期望一个存储作为必需的参数,然后是一个可选的参数。如果使用参数遭到破坏,可以使用以下语法传递额外的参数:
addTodo({commit}){
commit(‘ADD_TODO’)
},
没有参数的结构:
addTodo = function(store){
var commit = store.commit
commit('ADD_TODO')
}
分发怎么开始?
如果你使用过Vuex以前的版本,你可能会被新的版本语法抛弃。在Vuex2中,分发用于触发actions
,而commit
用于触发mutations
。一个actions
的分发对应一个mutations
的commit
。由于我们的操作是在Vuex存储中定义的,所以可以在多个模块中使用单个调用来分发actions
:
<button @click=”$store.dispatch(‘addTodo’)”>Add Todo</button>
当addTodo
被分发时,它将触发名叫ADD_TODO
的mutation
,并将一个新的todo
推送(push()
)到todos
列表。其余的动作,当被触发时将以类似的方式表现,因此没有必要对每一个动作进行检查。
有关于Vuex2更多的资料可以阅读下面文章:
回到我们项的store中来。剩下的就是在main.js
的Vue
实例中注册store
和导入store
:
// 修改src/main.js
import Vue from 'vue'
import App from './App'
import store from './store/store'
new Vue({
el: '#app',
store,
render: h => h(App)
})
创建组件
我们要做的Todo应用程序将由四个不同的组件组成:App
、GetTodo
、CurrentTodos
和CompletedTodos
。
组件是Vue.js最强大的特性之一。它能帮助你扩展基本的HTML元素,将其封装起来可重用。更高级的是,组件也是Vue.js自定义的元素,编译器可以附加指定行为。
对于一个简单的Todo应用程序,从单个组件构建整个应用程序是可以的。然而,我们这里想要演示的是Vuex是如何用来在各种不相关的组件之间进行通信的。
GetTodo
在创建组件之前,先在src
目录下创建一个components
子目录用来存储Todo应用程序所需要的组件(我这里创建Vue项目的时候,src
下就有components
文件夹)。而App.vue
还是依旧放在src
下。目录结构看上去像这样:
为了让我们的Todo应用程序好看一点,可以在index.html
引入BootStrap的样式文件。
截止到整理这篇文章的时候,BootStrap已经发布了Beta4版本,感兴趣的话,也可以将最新版本的样式引入到
index.html
中替换以前的版本。
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
接下来给GetTodo
组件添加代码:
<template>
<div id="get-todo" class="container">
<input class="form-control" :value="newTodo" @change="getTodo" placeholder="I need to ..." />
<button class="btn btn-primary" @click="addTodo">Todo</button>
</div>
</template>
<script>
export default {
methods: {
getTodo(e) {
this.$store.dispatch('getTodo', e.target.value)
},
addTodo() {
this.$store.dispatch('addTodo')
this.$store.dispatch('clearTodo')
}
},
computed: {
newTodo() {
return this.$store.getters.newTodo
}
}
}
</script>
如果你熟悉Vuex的话,你应该注意到了Vuex选项(option)的变化。在V2.0中,Vuex选项(option)已经被弃用,转而支持computed
属性和方法。作者认为,使用computed
的属性和方法可以减少在任何地方需要导入store
的需求。另一个你可能注意到的变是没有getters
。在V2.0中,引入了getters
:
store.getters
Vuex 允许我们在
store
中定义**getters
**(可以认为是store
的计算属性)。就像计算属性一样,getters
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
在我们的store.js
文件中,在actions
对象下面添加一个getters
对象。
// 修改src/store/store.js
// 在actions对象下添加下面代码
getters: {
newTodo: state => state.newTodo,
todos: state => state.todos.filter((todo) => {
return !todo.completed
})
}
newTodo
返回用户输入的新的todo
值,这个值绑定到input
元素上。todos
返回一个包含所有已完成值的todos
的数组。由于我们还希望在CompletedTodos
组件内显示所有已完成的todos
,我们不仿现在添加:
// 修改src/store/store.js
getters: {
newTodo: state => state.newTodo,
todos: state => state.todos.filter((todo) => {
return !todo.completed
}),
completedTodos: state => state.todos.filter((todo) => {
return todo.completed
})
}
接下来,我们应该在App.vue
组件内部注册我们的组件。我们使用注册的组件作为自定义元素,看上去像这样:
<template>
<div id="app">
<GetTodo></GetTodo>
</div>
</template>
<script>
import GetTodo from './components/GetTodo.vue'
export default {
components: {
GetTodo
}
}
</script>
这个时候你在浏览器看到的效果像这样:
到这一步,虽然在输入框中输入内容,点击
Todo
按钮,在页面上看不到任何的变化,事实上,程序已经有相应的变化,如上图中所示。
CurrentTodos
CurrentTodos
组件主要用来负责显示所有完成值为false
的todos
。除了显示每个todo
的主体之外,该组件还提供用于编辑、完成和删除todo
列表中的todo
操作(有相应的操作按钮)。该组件还显示了正在进行的todos
的数量。CurrentTodos
的代码如下:
<template>
<div id="current-todos" class="container">
<h3 v-if="todos.length > 0">Current({{ todos.length }})</h3>
<ul class="list-group">
<li class="list-group-item" v-for="todo in todos">
{{ todo.body }}
<div class="btn-group">
<button type="button" @click="edit(todo)" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-edit"></span> Edit
</button>
<button type="button" @click="complete(todo)" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-ok-circle"></span> Complete
</button>
<button type="button" @click="remove(todo)" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-remove-circle"></span> Remove
</button>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
methods: {
edit(todo) {
this.$store.dispatch('editTodo', todo)
},
complete(todo) {
this.$store.dispatch('completeTodo', todo)
},
remove(todo) {
this.$store.dispatch('removeTodo', todo)
}
},
computed: {
todos() {
return this.$store.getters.todos
}
}
}
</script>
<style>
.btn-group{
float: right;
}
</style>
像前面的GetTodo
组件一样,把CurrentTodos
组件添加到App.vue
组件中:
<template>
<div id="app">
<CurrentTodos></CurrentTodos>
<GetTodo></GetTodo>
</div>
</template>
<script>
import GetTodo from './components/GetTodo.vue'
import CurrentTodos from './components/CurrentTodos.vue'
export default {
components: {
GetTodo,
CurrentTodos
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
}
</style>
这个时候你在input
输入框输入你需要的todo
,然后点击todo
按钮,就可以把你想要的todo
添加到todos
里,看上去像下图这样:
现在在浏览器看到的效果包含了GetTodo
和CurrentTodos
两个组件:
在CurrentTodos
组件中,我们有几个按钮(编辑、完成和删除)可以对todo
进行操作。当用户点击Edit按钮,对应的todo
将被删除,而且对应的todo
的值也将被删除,todo.body
的值同时出现在input
输入框中。然后用户可以编辑todo.body
,并将编辑过的文本添加到当前todo
的列表中。用户点击Remove按钮可以删除对应的todo
。点击Complete按钮,可以将对应的todo
的completed
的值改为true
。因为我们只是映射给todos
的completed
的值为false
,只有completed
的值为false
时对应的todo
才会显示在todos
里,反之为true
时,todo
不会在todos
里显示。
现在来看一下整体的操作过程:
CompletedTodos
看最后一个组件CompletedTodos
,代码如下:
<template>
<div id="completed-todos" class="container">
<h3 v-if="completed.length > 0">Completed({{ completed.length }})</h3>
<ul class="list-group">
<li class="list-group-item" v-for="todo in completed">
{{todo.body}}
<button type="button" @click="remove(todo)" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-remove-circle"></span> Remove
</button>
</li>
</ul>
</div>
</template>
<script>
export default{
methods: {
remove(todo){
this.$store.dispatch('removeTodo', todo)
}
},
computed: {
completed(){
return this.$store.getters.completedTodos
}
}
}
</script>
跟前面的组件一样的方法,把该组件添加到App.vue
组件中:
<template>
<div id="app">
<CompletedTodos></CompletedTodos>
<GetTodo></GetTodo>
<CurrentTodos></CurrentTodos>
</div>
</template>
<script>
import GetTodo from './components/GetTodo.vue'
import CurrentTodos from './components/CurrentTodos.vue'
import CompletedTodos from './components/CompletedTodos.vue'
export default {
components: {
GetTodo,
CurrentTodos,
CompletedTodos
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
}
</style>
CompletedTodos
组件主要用来显示用户已完成的当前todos
。在这个示例中只显示完成的todo
数量、todo.body
以及将已完成的todos
从列表中删除的选项。
看上去不太美,但样子和功能都出来了,对于样式的美化不再花时间去纠结了,感兴趣的同学,自己可以去修复。
总结
我们还可以添加很多其他功能来增强这个应用程序的功能,但我希望这个简短的介绍能帮助你更好地理解如何使用Vue和Vuex 2.0来编写应用程序。
我们可以在这里找到我们文章中演示的示例代码。
本文根据@paadams的《》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://medium.com/@paadams/build-a-simple-todo-app-with-vue-js-1778ae175514。
如需转载,烦请注明出处:https://www.fedev.cn/vue/build-a-simple-todo-app-with-vue-js.htmlJordan Jumpman Pro AJ12.5