使用Vue.js和Vuex 2.0构建一个简单的Todo应用程序

发布于 大漠

这篇文章将介绍使用最新版本的Vue.jsVuex构建应用程序的一些基本知识。

Vue.js(读音 /vjuː/,类似于 view) 是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与单文件组件Vue 生态系统支持的库结合使用时,Vue 也完全能够为复杂的单页应用程序提供驱动。

新版本的Vue中已经添加了许多优秀的特性,比如virtual-DOM。

**译者注:**如果你和我一样,对Virtual-DOM不熟悉,建议移步阅读下面有关于这方面的文档:

Vuex也更新了。在这篇文章中,我们将讨论如何基于Vuex的最新版本来创建一个Todo应用程序,允许用户对Todo应用程序进行添加、编辑、完成和删除等任务。

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它的灵感来自于FluxRedux。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

执行完上面的命令之后,你的命令终端可以看到像下图这样的信息:

vue simple todo

接下来需要安装项目所需的依赖关系:

npm i

在大天朝使用npm i经常会碰到一些资源被墙,你可以采用cnpm i来做,我这里采用的是tnpm i来替代npm i安装一些依赖关系:

vue simple todo

安装好项目的依赖关系之外,在命令终端执行

npm run dev

你的浏览器会自动启用http://localhost:8080/#/地址,访问Todo应用程序。你在浏览器中将看到的效果如下:

vue simple todo

安装过程很简单,但是由于这个设置与Vuex没有关系,我们将不得不将它导入我们的项目中:

npm install vuex@next

执行上面的命令你将安装Vuex的最新版本。

vue simple todo

**译者注:**最终换成了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.vueexport表达式的内容了清理干净,清理后的代码看上去像这样:

<!-- 修改App.vue文件 -->
<template>
    <div id="app">

    </div>
</template>

<script>
    export default {
    
    }
</script>

<style>
    #app {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;        
    }
</style>

此时浏览器看到的是一个空白页,什么内容都没有:

vue simple todo

但不用紧张,随着后面的内容完善,我们的页面效果会出来的。

Vuex Store

让我们从创建我们的Vuex store(仓库)开始。在src目录下创建一个store的文件夹,并在这个文件夹中新建一个store.js文件。在这个文件中将包含我们所需要的store。

vue simple todo

每一个 Vuex 应用的核心就是 store(仓库)。"store" 基本上就是一个容器,它包含着你的应用中大部分的状态(state)。Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交(commit) mutations。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

在早期的Vuex版本中,每一个操作需要在store中创建单独的文件。Vuex的最新特点之一是能够在一个store文件中定义actionsgetter。这样做,使用一个模块加载更加便捷,更易组合。这是store的基本结构:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
    
    },
    mutations: {

    },
    actions: {

    }
})

如果您和我一样是Vuex的新手,那么有关于statemutationsactions这些术语可能会让人感到困惑。然而,背后的概念非常简单。我们创建一个state对象,当应用程序启动时,它保持初始状态。接下来,我们创建一个对象来存储我们的mutations。在Vuex中mutations基本上是事件。最后,我们创建一个对象来存储我们的actionsactions是用来分派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.newTodocompleted开始被设置为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的分发对应一个mutationscommit。由于我们的操作是在Vuex存储中定义的,所以可以在多个模块中使用单个调用来分发actions

<button @click=”$store.dispatch(‘addTodo’)”>Add Todo</button>

addTodo被分发时,它将触发名叫ADD_TODOmutation,并将一个新的todo推送(push())到todos列表。其余的动作,当被触发时将以类似的方式表现,因此没有必要对每一个动作进行检查。

有关于Vuex2更多的资料可以阅读下面文章:

回到我们项的store中来。剩下的就是在main.jsVue实例中注册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应用程序将由四个不同的组件组成:AppGetTodoCurrentTodosCompletedTodos

组件是Vue.js最强大的特性之一。它能帮助你扩展基本的HTML元素,将其封装起来可重用。更高级的是,组件也是Vue.js自定义的元素,编译器可以附加指定行为。

对于一个简单的Todo应用程序,从单个组件构建整个应用程序是可以的。然而,我们这里想要演示的是Vuex是如何用来在各种不相关的组件之间进行通信的。

GetTodo

在创建组件之前,先在src目录下创建一个components子目录用来存储Todo应用程序所需要的组件(我这里创建Vue项目的时候,src下就有components文件夹)。而App.vue还是依旧放在src下。目录结构看上去像这样:

vue simple todo

为了让我们的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>

这个时候你在浏览器看到的效果像这样:

vue simple todo

到这一步,虽然在输入框中输入内容,点击Todo按钮,在页面上看不到任何的变化,事实上,程序已经有相应的变化,如上图中所示。

CurrentTodos

CurrentTodos组件主要用来负责显示所有完成值为falsetodos。除了显示每个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里,看上去像下图这样:

vue simple todo

现在在浏览器看到的效果包含了GetTodoCurrentTodos两个组件:

vue simple todo

CurrentTodos组件中,我们有几个按钮(编辑、完成和删除)可以对todo进行操作。当用户点击Edit按钮,对应的todo将被删除,而且对应的todo的值也将被删除,todo.body的值同时出现在input输入框中。然后用户可以编辑todo.body,并将编辑过的文本添加到当前todo的列表中。用户点击Remove按钮可以删除对应的todo。点击Complete按钮,可以将对应的todocompleted的值改为true。因为我们只是映射给todoscompleted的值为false,只有completed的值为false时对应的todo才会显示在todos里,反之为true时,todo不会在todos里显示。

vue simple todo

现在来看一下整体的操作过程:

vue simple todo

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

看上去不太美,但样子和功能都出来了,对于样式的美化不再花时间去纠结了,感兴趣的同学,自己可以去修复。

总结

我们还可以添加很多其他功能来增强这个应用程序的功能,但我希望这个简短的介绍能帮助你更好地理解如何使用Vue和Vuex 2.0来编写应用程序。

我们可以在这里找到我们文章中演示的示例代码。

本文根据@paadams的《》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://medium.com/@paadams/build-a-simple-todo-app-with-vue-js-1778ae175514

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/vue/build-a-simple-todo-app-with-vue-js.htmlJordan Jumpman Pro AJ12.5