Vuex & Axios

weixin_33860553發表於2018-05-31

Vuex

Vuex是一個專門為Vue.js應用所設計的集中式狀態管理架構,它借鑑了Flux和Redux的設計思想,但簡化了概念,採用了一種為能更好地發揮Vue.js資料相應機制而專門設計的實現。

Vuex是一個專為Vue.js應用開發的狀態管理模式,採用集中式儲存管理應用元件狀態,並以響應規則保證狀態以一種可預測的方式發生變化。

# 安裝Vuex
$ npm i vuex --S

# 引入
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

狀態管理模式

# 計數器
new Vue({
  // state 驅動應用的資料來源
  data () {
    return {
      count:0
    }
  },
  // view 以宣告方式將資料來源對映到檢視
  template:`<span>{{count}}</span>`,
  //actions 響應在檢視上的使用者輸入導致的中狀態變化
  methods:{
    increment() {
      this.count ++
    }
  }
})

狀態state的概念,簡單來說可視為專案中使用的資料的集合。Vuex使元件本地狀態和應用層級狀態有了一定的差異。

  • 元件本地狀態:表示僅僅在元件內部使用的狀態,類似於通過配置選項傳入Vue元件內部。
  • 應用層級狀態:表示同時被多個元件共享的狀態層級

單向資料流

假如有一個父元件同時包含兩個子元件,父元件可以很容易的使用prop屬性向子元件傳遞資料。兩個子元件如何和物件互相通訊呢?子元件如何傳遞資料給父元件呢?專案規模很小時,可通過事件派發和監聽來完成父元件和子元件的通訊。隨著專案規模增長,遇到的問題是:

  • 保持對所有事件追蹤變得越來越困難,到底哪個事件是哪個元件派發的,哪個元件該監聽哪個事件呢?
  • 專案邏輯分散在各個元件當中,很容易導致邏輯混亂,不利於專案維護。
  • 父元件變得和子元件耦合度越來越嚴重,因為它需要明確的派發和監聽子元件的某些事件。
4933701-c6946fdef5d709cd.png
單向資料流

當應用遇到多個元件共享狀態時,單向資料流的間接性很容易被破壞:

  • 多個檢視依賴於同一個狀態
    傳參的方法對於多層巢狀的元件將會非常繁瑣,對於兄弟元件間的狀態傳遞無能為力。
  • 來自不同檢視的行為需要變更同一狀態
    採用父子元件直接飲用或通過事件來變更和同步狀態的多份拷貝

為什麼不把元件的共享狀態抽取出來以一個全域性單例模式管理呢?在這種模式下,元件樹構成一個巨大的檢視,不管在樹的哪個位置,任何元件都能獲取狀態或觸發行為。

另外,通過定義和隔離狀態管理中各種概念並強制遵守一定的規則,程式碼將會變得更加結構化且易於維護。

核心概念

  1. 單一狀態樹
    使用一個物件包含全部應用層級狀態,它作為唯一資料來源存在,這意味著,每個應用將緊緊包含一個倉庫store的例項。單一狀態樹讓我們能夠直接地定位任意特定的狀態片段,在呼叫過程中也能輕易地取得整個當前應用狀態的快照。
  2. 獲取狀態
    派生狀態getters用來從倉庫store中獲取Vue元件資料
  3. 狀態變更
    狀態變更mutators的事件處理器可用來驅動狀態的變化
  4. 非同步操作
    非同步操作actions可給元件使用的函式,以此用來驅動事件處理器mutations
4933701-066aaf208bff9f98.png
Vuex應用中的資料流向

Vuex規定,屬於應用層級的狀態只能通過狀態變更mutation中的方法來修改,而派發mutation中的事件只能通過action

從左至右,從元件觸發,元件中呼叫action,在action層可以和後臺資料互動,比如獲取初始化資料來源,或者中間資料的過濾等。然後在action中去派發mutationmutation去觸發狀態的改變,從而觸發檢視的更新。

注意

  • 資料流都是單向的
  • 元件能夠呼叫action
  • action用來派發mutation
  • 只有mutation可以改變狀態
  • store是響應式的,無論state什麼時候更新,元件都將同步更新。

開始

每個Vuex應用的核心就是倉庫(store),store基本就是一個容器,包含著應用中大部分的狀態(state)。Vuex和單純的全域性物件由兩點不同:

  • Vuex的狀態儲存時響應式的,當Vue元件從store中讀取狀態時,若store中的狀態發生變化,相應的元件也會得到高效更新。
  • 不能直接改變store中的狀態,改變store中的狀態唯一途徑就是顯式地提交(commit)mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化。

倉庫

Vuex應用狀態state都存放在store中,Vue元件可從store中獲取狀態,可把store通俗的理解為一個全域性變數的倉庫。和單純的全域性變數的區別是當store中的狀態發生變化時,相應的Vue元件也會得到更新。

//倉庫
const store = new Vuex.Store({
  //狀態
  state:{
    count:0
  },
  //狀態變更
  mutations:{
    increment(state){
      state.count++
    }
  }
})
//獲取狀態物件
store.state.count
//觸發狀態變更
store.commit('increment')

狀態

單一狀態樹

Vuex使用單一狀態樹,用一個物件包含了全部應用層級狀態,並作為一個“唯一資料來源(SSOT)”而存在。這意味著,每個應用將僅僅包含一個狀態例項。單一狀態樹讓我們能夠直接地定位任意特定的狀態片段,在除錯過程中也能輕易地去的整個當前應用狀態的快照。

在Vue元件中獲得Vuex狀態

由於Vuex的狀態儲存的是響應式的,從store例項中讀取狀態最簡單是方式是在計算屬性中返回某個狀態。

// 建立元件
const Counter = {
  template:`<div>{{count}}</div>`,
  computed:{
    count () {
      // 每當store.state.count變化時會重新求取計算屬性並觸發更新相關聯的DOM
      return store.state.count
    }
  }
}

這種模式導致元件依賴全域性狀態單例,在模組化的構建系統中,在每個需要使用state的元件中需要頻繁地匯入,並在測試元件時需模擬狀態。

Vuex通過store選項,提供了一種機制將狀態從根元件注入到每個子元件中。

const app = new Vue({
  el:'#app',
  store,//將store物件提供給store選項,把store例項注入到所有的子元件中。
  componenets:{Counter},
  template:`<div class="app"><counter></counter></div>`
})

通過在根例項中註冊store選項,該store例項會注入到根元件下的所有子元件中,且子元件能通過this.$store訪問到。

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

當一個元件需要獲取多個狀態時,將這些狀態都宣告為計算屬性會有些重複和冗餘,為解決這個問題,使用mapState輔助函式幫助我們生成計算屬性。

// 單獨構建版本中輔助函式為Vuex.mapState
import {mapState} from 'vuex'

export default {
  computed:mapState({
    //箭頭函式使得程式碼更簡潔
    count:state=>state.count,
    //傳字串count等同於state=>state.count
    countAlias:'count',
    //為了能夠使用this獲取區域性狀態必須使用常規函式
    countPlusLocalState(state){
      return state.count + this.localCount
    }
  })
}

當對映的計算屬性的名稱和state的子節點名稱相同時,也可以給mapState傳入一個字串陣列。

// 對映this.count為store.state.count
computed:mapState(['count'])

mapState函式返回的是一個物件,如何將其與區域性計算屬性混合使用呢?通常需要使用一個工具函式將多個物件合併為一個,以便於將最終物件傳給computed屬性。自從有了物件展開運算子可極大地簡化寫法。

computed:{
  localComputed () {
    // 使用物件展開運算子對此物件混入到外部物件中
    ...mapState({
    //...
    })
  }
}

使用Vuex並不意味著需要將所有狀態放入Vuex,雖然將所有狀態放入Vuex會使狀態變化更顯式和易呼叫,但也會使程式碼變得冗長且不直觀。有些狀態 嚴格屬於單個元件,最好還是作為元件的區域性狀態。

getter

有時需從倉庫中的狀態中派生一些狀態

computed:{
  // 對列表進行過濾並計數
  doneTodosCount() {
    return this.$store.state.todos.filter(todo=>todo.done).length
  }
}

如果有多個元件需要用到此屬性,要麼複製函式或抽取到一個共享函式然後再多出匯入,但是無論哪種方式都不是很理想。

Vuex允許在倉庫中定義getter(可認為是store的計算屬性),就像計算屬性一樣,getter的返回值會根據它的依賴被快取起來,只有噹噹它的依賴發生變化了才會被重新計算。

const store = new Vuex.Store({
  state:{
    todos:[
      {id:1, text:'...', done:true},
      {id:2, text:'...', done:false}
    ]
  },
  // getter類似倉庫的計算屬性一樣,其返回值會根據其依賴被快取起來,只有當以來之發生改變才會被重新計算。
  getters:{
    doneTodos:state=>{
      return state.todos.filter(todo=>todo.done)
    }
  }
})

getter會暴露為store.getters物件,可以以屬性的形式訪問其值:

# getter會暴露為store.getters物件,可以以屬性的形式訪問其值:
store.getters.doneTodos

# getter也可以接受其他getter作為第二個引數
getters:{
  doneTodosCount:(state, getters)=>{
    return getters.doneTodos.length
  }
}

store.getters.doneTodosCount

# 可以很容易在任何元件中使用它
computed:{
  doneTodosCount(){
    return this.$store.getters.doneTodosCount
  }
}

注意:getter在通過屬性訪問時是作為Vue的響應式系統的一部分快取其中的。

可以通過讓getter返回一個函式來實現給getter傳參,在對store中的陣列進行查詢時非常有用。

getters:{
  //可以通過讓getter返回一個函式來實現給getter傳參
  getTodoById:(state)=>(id)=>{
    return state.todos.find(todo=>todo.id === id)
  }
}

store.getters.getTodoById(2)

注意,getter在通過方法訪問時,每次都會去進行呼叫,而不會快取結果。

mapGetters 輔助函式

# mapGetters輔助函式僅僅是將store中的getter對映到區域性計算屬性
import {mapGetters} from 'vuex'

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

# 若想將一個getter屬性另取一個名字,使用物件形式。
mapGetters({
  //將this.doneCount對映為this.$store.getters.doneTodosCount
  doneCount:'doneTodosCount'
})

mutation

更改Vuex倉庫中的狀態的唯一方式是提交mutation。vuex中的mutation非常類似於事件:每個mutation都有一個字串的事件型別(type)和一個回撥函式(handler)。這個回撥函式就是實際進行狀態更改的地方,並且它會接收state作為第一個引數。

const store = new Vuex.Store({
  //狀態
  state:{count:1},
  //狀態變更
  mutations:{
    //每個mutation都有一個字串的事件型別和一個回撥函式
    increment (state) {
      state.count++ //變更狀態
    }
  }
})

不能直接呼叫一個mutation handler,這個選項更像是事件註冊:“當觸發一個型別為increment的mutation時,呼叫此函式”。要喚醒一個mutation handler,你需要以相應的type呼叫store.commit方法。

store.commit('increment')

提交載荷

可以像store.commit傳入額外的引數,即mutation的載荷payload

mutations:{
  increment (state, n){
    state.count += n
  }
}
// 向store.commit傳入額外的引數及mutation的載荷
store.commit('increment', 10)

多數情況下,載荷是一個物件,可包含多個欄位並記錄mutation會更易讀。

mutations:{
  increment(state, payload) {
    state.count += payload.amount
  }
}
// 載荷為物件
store.commit('increment',{acount:10})

物件風格的提交方式

提交mutation的另一種方式是直接使用包含type屬性的物件

# 使用包含type屬性的物件提交mutation
store.commit({type:'increment', amount:10})

# 使用物件風格提交方式,整個物件都作為載荷傳給mutation函式,因此handler保持不變。
mutations:{
  increment(state, payload){
    state.count += payload.amount
  }
}

mutation需遵守vue的響應規則

既然vuex的store中的狀態是響應式的,當變更狀態時,監視狀態的vue元件會也自動更新,這也意味著vuex中的mutation也需要與使用vue一樣遵守一些事項:

  1. 最好提前在store中初始化好所有屬性
  2. 當需要在物件上新增新屬性時,應該
  • 使用Vue.set(obj, 'prop', 123)
  • 以新物件替換老物件
state.obj = {...state.obj, prop:123}

使用常量替代mutation事件型別

使用常量替代mutation事件型別在各種flux實現中最為常見,可使用linter之類的工具發揮作用,同時將常量放在單獨的檔案中可讓程式碼合作者對整個app包含的mutation一目瞭然。

// mutation-types.js
export const SOME_MUTATION = ''

//store.js
import Vuex from 'vuex'
import {SOME_MUTATION} from './mutation-types'

const store = new Vuex.Store({
  state:{...},
  mutations:{
    //使用ES2015風格的計算屬性命名功能來使用一個常量作為函式名稱
    [SOME_MUTATION](state){

    }
  }
})

mutation必須是同步函式

Axios

Axios是一個基於promise的HTTP庫,可用於瀏覽器和Node.js中。

  • 從瀏覽器中建立XMLHttpRequest
  • 從Node.js中建立HTTP請求
  • 支援Promise API
  • 攔截請求和響應
  • 轉換請求資料和響應資料
  • 取消請求
  • 自動轉換JSON資料
  • 客戶端支援防禦XSRF
# 安裝 axios
$ npm i axios -S

# 執行GET請求
axios.get('/user?id=1').then(function(response){
  console.log(response);
}).catch(function(error){
  console.log(error)
})

axios.get('/user', {params:{id:1}}).then(function(response){
  console.log(response);
}).catch(function(error){
  console.log(error)
})

# 執行POST請求
axios.post('/user',{username:'', password:''}).then(function(response){
  console.log(response);
}).catch(function(error){
  console.log(error)
})

# 執行多個併發請求
function getUsername(){
  return axios.get('/user/1')
}
function getPermission(){
  return axios.get('/user/1/permission')
}
axios.all([getUsername(), getPermission()]).then(axios.spread(function(username, permission){

}))

axios API

# 向axios傳遞相關配置來建立請求
axios(url[, config])

Vue中使用Axios

Axios並非Vue的外掛無法直接引入後使用,只能在每個需要傳送請求的元件中即時引入。為了解決這個問題,有兩種思路:

  1. 引入Axios修改原型鏈
# main.js
# 引入
import axios from 'axios`
# 原型鏈繫結
Vue.prototype.$ajax = axios
# 使用
methods:{
  submit(){
    this.$ajax({
      method:'post',
      url:'url',
      data:{username:'username', password:'password'}
    })
  }
}
  1. 結合Vuex封裝action

Vuex的mutation類似於事件,可用於提交Vuex中的狀態,action和mutation很類似,區別在於action包含非同步操作,還可通過action來提交mutation。最主要的區別在於

  • mutation 固有引數state用於接收Vuex中的state物件
  • action 固有引數context是state的父級,包含著state、getters。

相關文章