详解 Vue & Vuex 实践翻译自Vue + Vuex — Getting started,承接自2017 Vue.js 2快速入门指南,从属于Web 前端入门与工程实践。css
随着应用复杂度的增长,咱们须要考虑如何进行应用的状态管理,将业务逻辑与界面交互相剥离,详细讨论参考笔者的2016-个人前端之路:工具化与工程化。Vue 为咱们提供了方便的组件内状态管理的机制,下面这个例子就是常见的获取列表数据而后渲染到界面中:html
import axios from 'axios' export default { name: 'projects', data: function () { return { projects: [] } }, methods: { loadProjects: function () { axios.get('/secured/projects').then((response) => { this.projects = response.data }, (err) => { console.log(err) }) } }, mounted: function () { this.loadProjects() } } </script>
在 template 中咱们能够方便的访问项目列表而且进行过滤、排序等操做,不过若是咱们在另外一个列表中也须要来展现相同的数据信息,继续按照这种方式实现的话咱们不得不从新加载一遍数据。更麻烦的是若是用户在本地修改了某个列表数据,那么如何同步两个组件中的列表信息会是个头疼的问题。Vue 官方推荐使用Vuex,相似于 Redux 的集中式状态管理工具来辅助解决这个问题。前端
根据 Vuex 文档中的描述,Vuex 是适用于 Vue.js 应用的状态管理库,为应用中的全部组件提供集中式的状态存储与操做,保证了全部状态以可预测的方式进行修改。vue
Vuex 中 Store 的模板化定义以下:ios
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { }, actions: { }, mutations: { }, getters: { }, modules: { } }) export default store
上述代码中包含了定义 Vuex Store 时关键的 5 个属性:git
state: state 定义了应用状态的数据结构,一样能够在这里设置默认的初始状态。github
state: { projects: [], userProfile: {} }
actions:Actions 便是定义提交触发更改信息的描述,常见的例子有从服务端获取数据,在数据获取完成后会调用store.commit()
来调用更改 Store 中的状态。能够在组件中使用dispatch
来发出 Actions。vuex
actions: { LOAD_PROJECT_LIST: function ({ commit }) { axios.get('/secured/projects').then((response) => { commit('SET_PROJECT_LIST', { list: response.data }) }, (err) => { console.log(err) }) } }
mutations: 调用 mutations 是惟一容许更新应用状态的地方。json
mutations: { SET_PROJECT_LIST: (state, { list }) => { state.projects = list } }
getters: Getters 容许组件从 Store 中获取数据,譬如咱们能够从 Store 中的 projectList 中筛选出已完成的项目列表:axios
getters: { completedProjects: state => { return state.projects.filter(project => project.completed).length } }
modules: modules 对象容许将单一的 Store 拆分为多个 Store 的同时保存在单一的状态树中。随着应用复杂度的增长,这种拆分可以更好地组织代码,更多细节参考这里。
在理解了 Vuex 的基础概念以后,咱们会建立一个真正的应用来熟悉整个使用流程。该应用承接自这个博客,在准备好基础项目以后,咱们须要将 vuex 引入项目中:
$ yarn add vuex
该步骤完成以后,咱们须要在 src 目录下建立名为 store 的目录来存放状态管理相关代码,首先建立 index.js:
import Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) const store = new Vuex.Store({ state: { }, actions: { }, mutations: { }, getters: { } }) export default store
而后在 main.js 文件中咱们须要将该 Store 实例添加到构造的 Vue 实例中:
import store from './store' /* eslint-disable no-new */ new Vue({ template: ` <div> <navbar /> <section class="section"> <div class="container is-fluid"> <router-view></router-view> </div> </section> </div> `, router, store, components: { navbar } }).$mount('#app')
而后,咱们须要去完善 Store 定义:
import Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) const store = new Vuex.Store({ state: { projects: [] }, actions: { LOAD_PROJECT_LIST: function ({ commit }) { axios.get('/secured/projects').then((response) => { commit('SET_PROJECT_LIST', { list: response.data }) }, (err) => { console.log(err) }) } }, mutations: { SET_PROJECT_LIST: (state, { list }) => { state.projects = list } }, getters: { openProjects: state => { return state.projects.filter(project => !project.completed) } } }) export default store
在本项目中,咱们将本来存放在组件内的项目数组移动到 Store 中,而且将全部关于状态的改变都经过 Action 进行而不是直接修改:
// /src/components/projectList.vue <template lang="html"> <div class=""> <table class="table"> <thead> <tr> <th>Project Name</th> <th>Assigned To</th> <th>Priority</th> <th>Completed</th> </tr> </thead> <tbody> <tr v-for="item in projects"> <td>{{item.name}}</td> <td>{{item.assignedTo}}</td> <td>{{item.priority}}</td> <td><i v-if="item.completed" class="fa fa-check"></i></td> </tr> </tbody> </table> </div> </template> <script> import { mapState } from 'vuex' export default { name: 'projectList', computed: mapState([ 'projects' ]) } </script> <style lang="css"> </style>
这个模板仍是十分直观,咱们经过computed
对象来访问 Store 中的状态信息。值得一提的是这里的mapState
函数,这里用的是简写,完整的话能够直接访问 Store 对象:
computed: { projects () { return this.$store.state.projects } }
mapState
是 Vuex 提供的简化数据访问的辅助函数。咱们视线回到 project.vue 容器组件,在该组件中调用this.$store.dispatch('LOAD_PROJECT_LIST)
来触发从服务端中加载项目列表:
<template lang="html"> <div id="projects"> <div class="columns"> <div class="column is-half"> <div class="notification"> Project List </div> <project-list /> </div> </div> </div> </template> <script> import projectList from '../components/projectList' export default { name: 'projects', components: { projectList }, mounted: function () { this.$store.dispatch('LOAD_PROJECT_LIST') } } </script>
当咱们启动应用时,Vuex 状态管理容器会自动在数据获取以后渲染整个项目列表。如今咱们须要添加新的 Action 与 Mutation 来建立新的项目:
// under actions: ADD_NEW_PROJECT: function ({ commit }) { axios.post('/secured/projects').then((response) => { commit('ADD_PROJECT', { project: response.data }) }, (err) => { console.log(err) }) } // under mutations: ADD_PROJECT: (state, { project }) => { state.projects.push(project) }
而后咱们建立一个简单的用于添加新的项目的组件 addProject.vue:
<template lang="html"> <button type="button" class="button" @click="addProject()">Add New Project</button> </template> <script> export default { name: 'addProject', methods: { addProject () { this.$store.dispatch('ADD_NEW_PROJECT') } } } </script>
该组件会派发某个 Action 来添加组件,咱们须要将该组件引入到 projects.vue 中:
<template lang="html"> <div id="projects"> <div class="columns"> <div class="column is-half"> <div class="notification"> Project List </div> <project-list /> <add-project /> </div> </div> </div> </template> <script> import projectList from '../components/projectList' import addProject from '../components/addProject' export default { name: 'projects', components: { projectList, addProject }, mounted: function () { this.$store.dispatch('LOAD_PROJECT_LIST') } } </script>
从新运行下该应用会看到服务端返回的建立成功的提示,如今咱们添加另外一个功能,就是容许用户将某个项目设置为已完成。咱们如今添加新的组件 completeToggle.vue:
<template lang="html"> <button type="button" class="button" @click="toggle(item)"> <i class="fa fa-undo" v-if="item.completed"></i> <i class="fa fa-check-circle" v-else></i> </button> </template> <script> export default { name: 'completeToggle', props: ['item'], methods: { toggle (item) { this.$store.dispatch('TOGGLE_COMPLETED', { item: item }) } } } </script>
该组件会展现一个用于切换项目是否完成的按钮,咱们经过 Props 传入具体的项目信息而后经过触发 TOGGLE_COMPLETED Action 来使服务端进行相对应的更新与相应:
// actions TOGGLE_COMPLETED: function ({ commit, state }, { item }) { axios.put('/secured/projects/' + item.id, item).then((response) => { commit('UPDATE_PROJECT', { item: response.data }) }, (err) => { console.log(err) }) } // mutations UPDATE_PROJECT: (state, { item }) => { let idx = state.projects.map(p => p.id).indexOf(item.id) state.projects.splice(idx, 1, item) }
UPDATE_PROJECT 会触发项目列表移除对应的项目而且将服务端返回的数据从新添加到数组中:
app.put('/secured/projects/:id', function (req, res) { let project = data.filter(function (p) { return p.id == req.params.id }) if (project.length > 0) { project[0].completed = !project[0].completed res.status(201).json(project[0]) } else { res.sendStatus(404) } })
最后一步就是将 completeToggle 组件引入到 projectList 组件中,而后将其添加到列表中:
// new column in table <td><complete-toggle :item="item" /></td> // be sure import and add to the components object
如今咱们的应用已经具有了基本的特性,这里咱们再度回顾下文首的讨论,为何咱们须要大费周章的引入外部状态管理,将业务逻辑切分到组件外。譬如这里咱们须要另外一个组件来展现项目的统计信息,譬如项目的总数或者已完成项目的数目。咱们确定要避免重复地从服务端抓取数据,而是所谓的 Single Source Of Truth。这里咱们添加新的 projectStatus.vue 组件来展现项目的统计信息:
<template lang="html"> <article class="message"> <div class="message-header"> <p>Project Status:</p> </div> <div class="message-body"> <div class="control"> <span class="tag is-info">Number of projects: {{projectCount}}</span> </div> <div class="control"> <span class="tag is-success">Completed: {{completedProjects}}</span> </div> </div> </article> </template> <script> import { mapGetters } from 'vuex' export default { name: 'projectStatus', computed: { ...mapGetters([ 'completedProjects', 'projectCount' ]) } } </script>
该组件会展现项目的总数与已完成项目的总数,上面咱们使用了maoGetters
辅助函数来减小冗余代码:
getters: { completedProjects: state => { return state.projects.filter(project =>project.completed).length }, projectCount: state => { return state.projects.length } }
最后咱们将该统计信息添加到项目列表中,效果图示以下: