hxh

Vue + Vuex — 入门 – Matt Bradford – 中级

hxh · 2017-01-27翻译 · 1445阅读 原文链接

有时创建应用程序时,你可能需要考虑如何管理状态。Vue 提供了一种简单的方式来管理组件内局部数据的状态。下面的例子是从 api 加载数据并设置为局部变量 projects 的值:

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>

在视图模板中,你可以很容易的获得数组 projects 并处理它们。这很好也很简单,但是如果你有其它组件也需要展示这些信息,该怎么办?你可以从服务器中重新加载,但是如果一个组件更新了数据,你可能需要重载其它相关的组件。这种方法能够运行,但它确实不高效,而且在应用中创建了重复的代码。那么答案是什么?就像所有事情一样,视情况而定!其中一个解决办法是使用Vuex,一种与 vue 紧密集成的集中式状态管理工具。也有其他的解决方法,但这里我将着重讨论 vuex。

你可以在这里找到文中示例完整的代码。

文章太长不看?——Vuex 是一个很棒的用于管理 Vue 应用程序状态的工具。你可以跳到结尾处要点是什么中,阅读一个能够说明它很重要的例子。

什么是 Vuex?

以下来自 vuex 的文档:

> Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用中所有组件的状态,规则保证了它只能在可预料的方法中发生转变。

https://raw.githubusercontent.com/vuejs/vuex/dev/docs/en/images/vuex.png

定义一个 vuex 存储器:

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

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {

  },
  actions: {

  },
  mutations: {

  },
  getters: {

  },  
  modules: {
    ...
  }
})

export default store

store 由 5 个对象组成,让我们逐一探索它们。

state:

用于定义应用中数据的结构。也可以在这里设置默认值或者初始化状态。

state: {
  projects: [],
  userProfile: {}
}

actions:

Actions 用于定义了向存储器提交变化的回调方法。一个常见的例子是调用 api 获取数据,完成之后使用 store.commit() 调用 mutation 更新存储器中的数据。在组件中通过分发调用 Actions。

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 是 存储器中唯一能够更新数据的地方

mutations: {
    SET_PROJECT_LIST: (state, { list }) => {
      state.projects = list
    }
  }

getters:

getters 用于获取存储器中的运算数据。例如,你有一个项目列表,二组件可能只想展示项目的完成进度:

getters: {
 completedProjects: state => {
  return state.projects.filter(project => project.completed).length
 }
}

modules:

modules 对象提供了一种将多个存储区拆分存储的方法,但允许它们都保存在存储树中。随着应用的增长,这是一个管理代码库的好方法。关于 modules 的更多细节可以在 vuex 文档中查找。

例子

现在我们了解了 vuex 的基础知识,让我们使用它创建一个实际的应用程序。我会在前一篇帖子的基础上创建 app,如果你需要了解我们如何到达这点,可以在这里找到代码。为了安全起见,这个 app 使用了 Auth0,如果你不想安装,只需要删除 router 中对 beforeEnter 的调用以删除安全防护。还有,在 api 中,删除 routes 的 /secured 目录

为了能够运行起来,需要设置 vuex 存储器,添加 vuex 到项目中:

$ yarn add vuex

这一步完成之后,在根目录中创建一个名为 store 的文件夹,在里面添加 index.js 文件。可以进入主题了,只需加入 store 的基础定义项。

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 文件,导入存储器,将它添加到 vue 的根对象中:

import store from './store'
/* eslint-disable no-new */
new Vue({
  template: `
  <div>
    &lt;navbar /&gt;
    <section>
      <div>
        &lt;router-view&gt;&lt;/router-view&gt;
      </div>
    </section>
  </div>
  `,
  router,
  **store,**
  components: {
    navbar
  }
}).$mount('#app')

下一步,设置 vuex store(/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: {
    projects: []
  },
  actions: {
    LOAD_PROJECT_LIST: function ({ commit }) {
      axios.get('/secured/projects').then((response) =&gt; {
        commit('SET_PROJECT_LIST', { list: response.data })
      }, (err) =&gt; {
        console.log(err)
      })
    }
  },
  mutations: {
    SET_PROJECT_LIST: (state, { list }) =&gt; {
      state.projects = list
    }
  },
  getters: {
    openProjects: state =&gt; {
      return state.projects.filter(project =&gt; !project.completed)
    }
  }
})
export default store

我在 state 中定义了数组 projects,创建一个 action 方法用于获取项目列表,并将它们提交到 mutation,设置 projects 的值。我也设置了一个 getter 方法,用于返回已完成的项目。现在存储器设置好了,我会重构 projects 页面, 将其分成几个组件。首先创建一个 projectList 组件用于渲染列表。

/src/components/projectList.vue:

&lt;template lang="html"&gt;
  <div>
    <table>
      <thead>
        <tr>
          <th>Project Name</th>
          <th>Assigned To</th>
          <th>Priority</th>
          <th>Completed</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>{{item.name}}</td>
          <td>{{item.assignedTo}}</td>
          <td>{{item.priority}}</td>
          <td><i></i></td>
        </tr>
      </tbody>
    </table>
  </div>
&lt;/template&gt;
&lt;script&gt;
import { mapState } from 'vuex'
export default {
  name: 'projectList',
  computed: mapState([
    'projects'
  ])
}
&lt;/script&gt;
&lt;style lang="css"&gt;
&lt;/style&gt;

这个模板非常的直白,我们通过 computed 对象获取存储器中的信息。值得注意的一件事是,这里使用了辅助函数 mapState。当向存储器中的运算对象添加新对象时,你可以写出完整的路径:

computed: {
  projects () {
    return this.$state.store.projects
  }
}

而 mapState 是 vuex 提供的一个辅助函数,为了简化对象的创建。最终的结果是完全相同的,可以放心的使用。

回到 project.vue 容器,导入 projectList 组件,将它添加到定义项中。注意调用的 this.$store.dispatch('LOAD_PROJECT_LIST) 会从服务器加载 projects 到存储器中。当考虑到在哪里加载 state 时,总是希望尽可能将它提升。我们的案例中,在最高层的 project 容器中将 projects 加载到 store。

&lt;template lang="html"&gt;
  <div>
    <div>  
      <div>
        <div>
          Project List
        </div>
        &lt;project-list /&gt;
      </div>
    </div>
  </div>
&lt;/template&gt;
&lt;script&gt;
import projectList from '../components/projectList'
export default {
  name: 'projects',
  components: {
    projectList
  },
  mounted: function () {
    this.$store.dispatch('LOAD_PROJECT_LIST')
  }
}
&lt;/script&gt;

如果运行这个应用,项目列表将从 vuex 的 state 中渲染出来。现在 projects 列表展示出来了,是时候设置添加新 project 的方法了。

回到 vuex 存储器,添加新的 action 和 mutation 来处理获取新 project 并出入存储器中。

// under actions:
ADD_NEW_PROJECT: function ({ commit }) {
  axios.post('/secured/projects').then((response) =&gt; {
    commit('ADD_PROJECT', { project: response.data })
  }, (err) =&gt; {
    console.log(err)
  })
}
// under mutations:
ADD_PROJECT: (state, { project }) =&gt; {
  state.projects.push(project)
}

_*这个回调方法在服务器端中有一个注意事项,我只在服务器中创建了一个简单的数组,和一个 post 方法来添加新的 project 到数组中,并返回它。你可以在 project 的 /server/app.js 中查看细节_

接着,在 components 文件夹中创建一个名为 addProject.vue 的简单组件:

&lt;template lang="html"&gt;
  &lt;button type="button" class="button" [@click](http://twitter.com/click "Twitter profile for @click")="addProject()"&gt;Add New Project&lt;/button&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
  name: 'addProject',
  methods: {
    addProject () {
      this.$store.dispatch('ADD_NEW_PROJECT')
    }
  }
}
&lt;/script&gt;

这个组件派遣 action 添加一个新的 project 到列表中。导入 addProject 组件到 projects.vue 容器中,并将它添加到模板。

&lt;template lang="html"&gt;
  <div>
    <div>
      <div>
        <div>
          Project List
        </div>
        &lt;project-list /&gt;
        &lt;add-project /&gt;
      </div>
    </div>
  </div>
&lt;/template&gt;
&lt;script&gt;
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')
  }
}
&lt;/script&gt;

运行应用,你会看到当服务器成功返回新 project 时,它会提交到存储器并展示在屏幕上。我添加一个额外的组件来切换不同标志的项目。创建一个新的组件名为 completeToggle.vue:

&lt;template lang="html"&gt;
  &lt;button type="button" class="button"  [@click](http://twitter.com/click "Twitter profile for @click")="toggle(item)"&gt;
    <i></i>
    <i></i>
  &lt;/button&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
  name: 'completeToggle',
  props: ['item'],
  methods: {
    toggle (item) {
      this.$store.dispatch('TOGGLE_COMPLETED', { item: item })
    }
  }
}
&lt;/script&gt;

这个组件在每个项目中显示一个按钮,这个按钮会调用切换展示完成或未完成项目的函数。通过 props 传递 item, 切换函数会派遣 action 的 TOGGLE_COMPLETED,更新服务器中的值并将它返回。当更新完成时,会调用 commit 方法,使用新数据更新状态。这是存储器新方法。

// actions
TOGGLE_COMPLETED: function ({ commit, state }, { item }) {
  axios.put('/secured/projects/' + item.id, item).then((response) =&gt; {
    commit('UPDATE_PROJECT', { item: response.data })
  }, (err) =&gt; {
    console.log(err)
  })
}
// mutations
UPDATE_PROJECT: (state, { item }) =&gt; {
  let idx = state.projects.map(p =&gt; p.id).indexOf(item.id)
  state.projects.splice(idx, 1, item)
}

mutation 中的 UPDATE_PROJECT 会移除数组中的一行,重新加入来自服务器的新行。这样,如果另一个 actions 更新了对象中的不同部分,我任然可以使用这个 mutation 更新存储器。服务器调用的详细信息,可以查看 github repo 中的 server/app.js。这是 server 中的 put 调用:

app.put('/secured/projects/:id', function (req, res) {
  let project = data.filter(function (p) { return p.id == req.params.id })
  if (project.length &gt; 0) {
    project[0].completed = !project[0].completed
    res.status(201).json(project[0])
  } else {
    res.sendStatus(404)
  }
})

最后一步是导入 completeToggle 组件到 projectList 组件中,并将其添加到表,传递 item 给 completeToggle。

// new column in table
<td>&lt;complete-toggle :item="item" /&gt;</td>
// be sure import and add to the components object
// 确保导入和添加组件对象

现在运行应用,你可以添加新项目并完成它们。数据是保存在 vuex 存储器和服务器端(内存)中的。

要点是什么?

现在应用有了一些基本功能,但你可能会问和我第一次使用这个工具时一样的问题。。。这真的值得吗?对于实现添加行和切换字段项目列表这样简单的功能,它似乎过于复杂。如果现在就满足了你应用程序的所有需求,这就结束了,那么我同意。但如果你需要其它的组件展示项目列表的摘要信息呢?每次更改时从服务器获取数据呢?发出一些事件类型使数据保持最新状态?我们看看 vuex 如何为我们解决这个问题。添加另一个名为 projectStatus.vue 的组件。

&lt;template lang="html"&gt;
  <article>
    <div>
      <p>Project Status:</p>
    </div>
    <div>
      <div>
        <span>Number of projects: {{projectCount}}</span>
      </div>
      <div>
        <span>Completed: {{completedProjects}}</span>
      </div>
    </div>
  </article>
&lt;/template&gt;
&lt;script&gt;
import { mapGetters } from 'vuex'
export default {
  name: 'projectStatus',
  computed: {
    ...mapGetters([
      'completedProjects',
      'projectCount'
    ])
  }
}
&lt;/script&gt;

这个组件会展示项目总数和是否完成。我使用了 mapGetters 辅助函数缩短定义,这个方法定义在存储器中:

getters: {
  completedProjects: state =&gt; {
    return state.projects.filter(project =&gt;project.completed).length
  },
  projectCount: state =&gt; {
    return state.projects.length
  }
}

现在在 projectList 页面紧接着表添加这个组件,当你添加新项目或者完成它们时,你会看到,数字会自动更新。这个组件能够在 app 的任何地方使用,让你用户的数据保持最新状态。

总结

虽然这是一个简单的示例,但它能够帮助理解如果有效的管理状态。按常理使用它,有时组件很简单不需要使用 vuex,但随着你应用的增长,使用它管理状态会变得越来越合理。

相关文章