詳解 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 } }
最後咱們將該統計信息添加到項目列表中,效果圖示以下: