Vue--Vuex

weixin_33807284發表於2018-03-19

Vuex 就是前端為了方便資料的操作而建立的一個” 前端資料庫“。模組間是不共享作用域的,那麼B 模組想要拿到 A 模組的資料,我們會怎麼做?我們會定義一個全域性變數,叫aaa 吧,就是window.aaa。然後把A 模組要共享的資料作為屬性掛到 B 模組上。這樣我們在 B 模組中通過window.aaa 就可以拿到這個資料了。但是問題來了,B 模組拿到了共享的資料,就叫他xxx 吧?得了,名字太混亂了,我們先給它們都改下名字。那個全域性變數既然是存東西的,就叫store 吧,共享的資料就叫state 吧,B 模組拿到了 A 模組的資料state,但是這個資料不是一成不變的呀,A 要操作這個資料的。那麼我們是不是要在這個資料——state 改變的時候通知一下 B?那寫個自定義事件吧。首先,你得能取吧?那麼得有一套取資料的 API,我們給他們集中起個名字吧?既然是獲取,那就叫getter 吧。我們還得存資料呀,是吧。存資料就是對資料庫的修改,這些 API,我們也得給它起個名字,就叫mutation。vuex 生成的倉庫也就這麼出來了,所以我說 vuex 就是” 前端的資料庫“。State 就是資料庫。Mutations 就是我們把資料存入資料庫的 API,用來修改state 的。getters 是我們從資料庫裡取資料的 API,既然是取,那麼你肯定不能把資料庫給改了吧?所以getters 得是一個”純函式“,就是不會對原資料造成影響的函式。拿到了資料,總要做個處理吧,處理完了再存到資料庫中。其實這就是action的過程。當然你也可以不做處理,直接丟到資料庫。來看看 vuex 的資料流,通過action處理資料,然後通過mutation 把處理後的資料放入資料庫(state)中,誰要用就通過getter從資料庫(state)中取。

安裝

npm install --save vuex

引入

import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)

例項化生成store的過程是

//store為例項化生成的
import store from './store'

new Vue({
  el: '#app',
  store,
  render: h => h(App)
})
const mutations = {...};
const actions = {...};
const state = {...};

Vuex.Store({
  state,
  actions,
  mutation
});

State

儲存整個應用的狀態資料.
想要獲取對應的狀態你就可以直接使用this.$store.state獲取,也可以利用vuex提供的mapState輔助函式將state對映到計算屬性中去

//我是元件
import {mapState} from 'vuex'

export default {
  computed: mapState({
    count: state => state.count
  })
}

Mutations

更改狀態,本質就是用來處理資料的函式, 其接收唯一引數值state
store.commit(mutationName)是用來觸發一個mutation的方法。需要記住的是,定義的mutation必須是同步函式

const mutations = {
  mutationName(state) {
    //在這裡改變state中的資料
  }
}

在元件中觸發:

//我是一個元件
export default {
  methods: {
    handleClick() {
      this.$store.commit('mutationName')
    }
  }
}

或者使用輔助函式mapMutations直接將觸發函式對映到methods上,這樣就能在元素事件繫結上直接使用了

import {mapMutations} from 'vuex'

//我是一個元件
export default {
  methods: mapMutations([
    'mutationName'
  ])
}

Actions

Actions也可以用於改變狀態,不過是通過觸發mutation實現的,重要的是可以包含非同步操作。其輔助函式是mapActionsmapMutations類似,也是繫結在元件的methods上的。如果選擇直接觸發的話,使用this.$store.dispatch(actionName)方法。

//定義Actions
const actions = {
  actionName({ commit }) {
    //dosomething
    commit('mutationName')
  }
}

在元件中使用

import {mapActions} from 'vuex'

//我是一個元件
export default {
  methods: mapActions([
    'actionName',
  ])
}

Getters

有些狀態需要做二次處理,就可以使用getters。通過this.$store.getters.valueName對派生出來的狀態進行訪問。或者直接使用輔助函式mapGetters將其對映到本地計算屬性中去。

const getters = {
  strLength: state => state.aString.length
}
//上面的程式碼根據aString狀態派生出了一個strLength狀態

在元件中使用

import {mapGetters} from 'vuex'

//我是一個元件
export default {
  computed: mapGetters([
    'strLength'
  ])
}

Plugins

外掛就是一個鉤子函式,在初始化store的時候引入即可。比較常用的是內建的logger外掛,用於作為除錯使用。

import createLogger from 'vuex/dist/logger'
const store = Vuex.Store({
  ...
  plugins: [createLogger()]
})

Example

npm install --save 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

整理

基本用途:

  • 將某些data變成元件間公用的狀態,元件隨時都可以進行訪問和響應,解決了props傳值的鏈式響應的程式碼冗餘
  • 給狀態配以公用方法,將狀態的變更及時響應並處理

基本用法:

/store/store.js

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

Vue.use(Vuex);

export const store = new Vuex.Store({
    state: {
        sideBarOpened: false
        //放置公用狀態
    },
    getters: {
        changeState: state => {
            //相當於vue例項中的methods,用於處理公用data
            //自vuex 面向元件的資料處理
        }
    },
    mutations: {
        //寫法與getters相類似
        //元件想要對於vuex 中的資料進行的處理
        //元件中採用this.$store.commit('方法名') 的方式呼叫,實現充分解耦
        //內部操作必須在此刻完成(同步)
    },
    actions: {
        //使得mutations能夠實現非同步呼叫,實現例如延遲呼叫
        increment ({ commit }, payload) {
            commit('突變方法名')
            //payload應該是一個物件,可通過模板方法呼叫傳入物件的方式將資料從元件傳入vuex
        },
        asyncIncrement({commit}) => {
            setTimeout(() => {
                commit('increment')
            }, 1000)
        }
    },
    modules: {
        //引入某一個state的以上集合的模組,會自動分別填充到上面,使得結構更加清晰
    }
});

main.js

import { store } from './store/store'
*
*
new Vue({
  el: '#app',
  store,    //注入根元件
  ...
})

訪問vuex中的資料和方法

this.$store.state.資料名
this.$store.getters.方法名

受影響元件區域性定義計算屬性響應變化資料

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

將 store 中的 getters/mutations 對映到區域性(計算屬性/方法)使用mapGetters/mapMutations輔助函式

import { mapGetters } from 'vuex'

computed: {
  // 使用物件展開運算子將 getters 混入 computed 物件中
    ...mapGetters([
        //對映 this.doneTodosCount 為 store.getters.doneTodosCount
      'doneTodosCount',
      //'getter名稱',

      // 對映 this.doneCount 為 store.getters.doneTodosCount
        doneCount: 'doneTodosCount'
      // 三個點表示將內部拿出生成鍵值對,這樣使得元件本身的計算屬性不受影響
      // 此語法依賴babel-preset-stage-2
    ])
  }

注意事項:

mutation 必須是同步函式 — devtool要儲存快照,方便追蹤狀態變化
使用 v-model 繫結 vuex 計算屬性的時候要設定get 和 set 才能雙向繫結

computed: {
    value: {
        get () {
            return this.$store.getters.value;
        },
        set (event) {
            this.$store.dispatch('updateValue', event.target.value);
        }
    }
}

注意:

  1. 當 Vue 元件從 store 中讀取狀態的時候,若 store中的狀態發生變化,那麼相應的元件也會相應地得到高效更新。
  2. 不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交mutations
// 如果在模組化構建系統中,請確保在開頭呼叫了 Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

現在,你可以通過 store.state 來獲取狀態物件,以及通過 store.commit 方法觸發狀態變更:

store.commit('increment')
console.log(store.state.count) // -> 1

每次對資料物件進行操作的時候,都需要進行commit

state

Vuex 通過 store 選項,提供了一種機制將狀態從根元件『注入』到每一個子元件中(需呼叫 Vue.use(Vuex)):

const app = new Vue({
  el: '#app',
  // 把 store 物件提供給 “store” 選項,這可以把 store 的例項注入所有的子元件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})

通過在根例項中註冊 store 選項,該 store 例項會注入到根元件下的所有子元件中,且子元件能通過 this.$store訪問到。讓我們更新下 Counter 的實現:

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

mapState 輔助函式
輔助函式幫助我們生成計算屬性
mapState函式返回的是一個物件。我們如何將它與區域性計算屬性混合使用呢?通常,我們需要使用一個工具函式將多個物件合併為一個,以使我們可以將最終物件傳給 computed 屬性。但是自從有了物件展開運算子(現處於 ECMASCript 提案 stage-3 階段),我們可以極大地簡化寫法。

Getters

可以認為是 store 的計算屬性

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

會暴露為 store.getters 物件:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

也可以接受其他 getters 作為第二個引數
mapGetters 輔助函式
將 store 中的 getters 對映到區域性計算屬性

import { mapGetters } from 'vuex'
export default {
  // ...
  computed: {
  // 使用物件展開運算子將 getters 混入 computed 物件中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

你想將一個 getter 屬性另取一個名字,使用物件形式:

mapGetters({
  // 對映 this.doneCount 為 store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})