作者注:[2016.11 更新]這篇文章是基於一個非常舊的 vuex api 版本而寫的,代碼來自於2015年12月。
但是,它仍能針對下面幾個問題深入探討:
- vuex 為什么重要
- vuex 如何工作
- vuex 如何使你的應用更容易維護
vuex 是 vue.js 作者開發的一個原型庫,它幫助你創建更大、維護性更強的應用,類似於 Facebook 的 flux 庫(以及由社區維護的 redux 庫)。
這篇文章不直接跳到 vuex 教你如何使用它,而是從背后的故事開始說起,逐步解釋它為什么是優雅的替代方法,以及將如何幫助你。
譯者注:a git repo of vuex-tutorial use vue2.0
你想要創建什么應用?
一個擁有按鈕
和計數器
的簡單應用,點擊按鈕計數器加1。這聽起來非常容易理解和完成。
我們假設這個應用有兩個組件:
- 按鈕 (它是事件的來源)
- 計數器 (它必須按照事件來反映更新)
這兩個組件不知道彼此的存在,也不能相互通信。即使是在最小的 web 應用中,這也是一種非常常見的模式。在更大點兒的應用中,十幾個組件相互通信,並時刻關注對方的變化。不相信我?這里是一個基礎的 TODOlist 應用的交互清單:
這篇文章的目標
我們將討論解決同一個問題的3種方法:
- 組件之間使用事件廣播來通信
- 使用一個共享的狀態對象通信
- 使用 vuex 通信
讀完這篇文章,希望你能理解:
- 在你的項目中使用 vuex 的一個基本工作流程
- 它解決了哪些問題
- 相對其他方法,為什么它是更好的(盡管有些冗長和嚴格)
准備工作
我們將使用3種不同的方法來解決同一個問題。在這之前,需要做一些共同的准備工作。如果你打算跟着我做,我建議你為這個教程創建一個 git repo,這一小節結束后提交一次代碼,然后為不同的方法創建不同的分支。
1
2
3
4
5
6
|
|
現在你應該能看到 vue 的腳手架頁面了,下面來為我們要做的事來修改一些文件。
首先,在文件 src/components/IncrementButton.vue
中創建 IncrementButton
組件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<template>
<button @click.prevent="activate">+1</button>
</template>
<script>
export default {
methods: {
activate () {
console.log('+1 Pressed')
}
}
}
</script>
<style>
</style>
|
下一步,在文件 src/components/CounterDisplay.vue
中創建 CounterDisplay
組件來展示計數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<template>
Count is
{{ count }}
</template>
<script>
export default {
data () {
return {
count: 0
}
}
}
</script>
<style>
</style>
|
使用下面的內容替換 App.vue
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<template>
<div id="app">
<h3>Increment:</h3>
<increment></increment>
<h3>Counter:</h3>
<counter></counter>
</div>
</template>
<script>
import Counter from './components/CounterDisplay.vue'
import Increment from './components/IncrementButton.vue'
export default {
components: {
Counter,
Increment
}
}
</script>
<style>
</style>
|
現在,重新運行 npm run dev
,在瀏覽器打開頁面,你應該看到一個 按鈕
和一個 計數器
。點擊按鈕,控制台將顯示一條信息,其它沒什么變化。
現在我們已經來到了起點,開始吧。
方法1:事件廣播
來修改組件的代碼。
首先在 IncrementButton.vue
中,在按鈕被點擊時使用 $dispatch
給父組件發送一個消息。
1
2
3
4
5
6
7
8
|
export default {
methods: {
activate () {
// Send an event upwards to be picked up by App
this.$
dispatch('button-pressed')
}
}
}
|
在 App.vue
中監聽來自子組件的這個消息事件,然后廣播一個新的事件 increment
給所有的子組件:
1
2
3
4
5
6
7
8
9
10
11
12
|
export default {
components: {
Counter,
Increment
},
events: {
'button-pressed': function () {
// Send a message to all children
this.$broadcast('increment')
}
}
}
|
在 CounterDisplay.vue
中,監聽 increment
事件,並增加狀態數據中的變量:
1
2
3
4
5
6
7
8
9
10
11
12
|
export default {
data () {
return {
count: 0
}
},
events: {
increment () {
this.count++
}
}
}
|
這個方法的缺點:
這個方法基本沒有什么技術上的錯誤。此外,在一個文件里實現整個應用的邏輯,專門使用 goto 來跳轉也沒有錯。這只與可維護性有關,這里會講一下為什么這個方法在可維護性上是糟糕的。
- 對於每一個操作,父組件都需要將事件分發給正確的組件;
- 在大型應用中,可能很難理解事件是從哪兒來的;
- 業務邏輯沒有明確的位置。
this.count++
是在CounterDisplay
中,但業務邏輯可能到處都是,這會導致難以維護。
讓我來舉例說明一下這個方法會怎樣導致bug:
- 你雇了兩個實習生: Alice 和 Bob。你告訴 Alice 你需要為另外一個組件實現另一個計數器,告訴 Bob 寫一個重置按鈕;
- Alice 寫了一個新的組件
FormattedCounterDisplay
,它能夠監聽增量,並增加自己的狀態數據。Alice 開心的提交了代碼; - Bob 寫了一個新的
Reset
組件,它向應用發出一個reset
事件,並重新分發它。他在CounterDisplay
中將 count 重置為0,但是他沒有意識到 Alice 的組件也訂閱了這個變化; - 你的用戶點擊
+1
按鈕后看到應用工作正常。但是當他點擊重置
按鈕,只有一個計數器被重置了。這看起來是一個非常簡單的例子,僅僅為了說明狀態和業務邏輯綁在一起可能會導致錯誤。
方法2: 共享狀態
撤銷方法1中的改動,創建一個新文件 src/store.js
:
1
2
3
4
5
|
export
default {
state: {
counter:
0
}
}
|
首先修改 CounterDisplay.vue
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<template>
Count is
{{ sharedState.counter }}
</template>
<script>
import store from '../store'
export default {
data () {
return {
sharedState: store.state
}
}
}
</script>
|
這里我們做了一些有趣的事情:
- 獲取到一個 store 對象,它僅僅是一個對象常量,但是在不同的文件中定義的;
- 在本地數據中,我們創建了一個叫
sharedState
的數據,它映射到store.state
; - vue 使用
store.state
作為當前組件的一部分數據,這意味着store.state
有任何變化,vue 都會自動更新sharedState
。
到目前為止它還不能工作,現在我們來修改 IncrementButton.vue
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import store from '../store'
export
default {
data () {
return {
sharedState: store.state
}
},
methods: {
activate () {
this.sharedState.counter += 1
}
}
}
|
- 在這里,我們引入
store
,並像之前的例子一樣監聽了數據的狀態變化; - 當
activate
方法被調用時,指向store.state
的sharedState
的計數器 counter 增加; - 監聽了計數器的所有組件和計算屬性都會被更新。
它為什么比方法1更好
我們來回顧一下兩個實習生 Alice 和 Bob 的問題:
- Alice 寫的用來監聽共享數據的
FormattedComponentDisplay
組件將會始終顯示最新的 counter 數據; - Bob 的重置按鈕組件將共享數據的 counter 置為0,這將同時影響
CounterDisplay
和 Alice 寫的FormattedCounterDisplay
; - 重置按鈕符合預期。
為什么這樣仍然不夠好
- 在 Alice 和 Bob 的實習期內,他們使用不同的格式寫了許多計數器、重置按鈕,以及增量按鈕,它們更新的是同一份共享的數據,生活很美好;
- 一旦他們回到學校,你需要維護他們的代碼;
- 新任經理 Carol 進來之后說:“我不想看到計數器的數字超過100”
你現在該做什么?
- 你去十幾個組件的代碼里找到所有更新數據的地方嗎?這讓人沮喪;
- 你找到顯示數據的地方然后添加一個
filter/formatter
來格式化數據嗎?這同樣讓人沮喪; - 這里就是這個問題,業務邏輯分散在應用的各個角落,原則上一個很簡單的問題,但是維護和調試起來卻特別痛苦。
稍好一點兒的方法
現在來重構你的代碼,重寫 store.js
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var store = {
state: {
counter: 0
},
increment: function () {
if (store.state.counter < 100) {
store.state.counter +=
1;
}
},
reset: function () {
store.state.counter =
0;
}
}
export default store
|
顯式調用 increment
並將所有業務邏輯都放進 store
后代碼看起來清晰了許多。然而,一個新實習生不知道這背后的理論,他發現在應用的其他部分直接寫入 store.state.counter
更容易,於是一切變得難於調試。
然后,你制定大量嚴格的規則和代碼審查,以確保沒有人在 store.js
中不使用函數的情況下修改狀態數據。如果這都不起作用,那你可以告訴hr結束他的實習了。
方法3:vuex
回滾方法2里的修改,原則上 vuex 的工作原理與方法2有些相似。給你看一張稍稍有些可怕的圖:
首先來創建 src/store.js
,這次用下面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import Vuex
from 'vuex'
import Vue
from 'vue'
Vue.use(Vuex)
var store = new Vuex.Store({
state: {
counter:
0
},
mutations: {
INCREMENT (
state) {
state.counter++
}
}
})
export
default store
|
現在來看看這段代碼做了什么:
- 獲取 Vuex 模塊,然后使用
Vue.use
安裝這個插件; store
不再是一個普通的 JSON 對象,而是Vuex.Store
的一個實例;- 在
state
中創建一個計數器counter
,設置為0; - 創建一個新的變異對象,包含
INCREMENT
方法:獲取一個狀態數據,然后改變它。
看看這段代碼里有哪些有趣的東東:
- 所有通過
require('../store.js')
或import store from '../store.js'
引入的store
將使用同一個 store 實例; - 我們不會修改
store.state.counter
,但是我們有一份state
的拷貝用來做修改,這在接下來會很重要。
現在我們已經改好了 store,來繼續修改 IncrementButton.vue
:
1
2
3
4
5
6
7
8
9
|
import store from '../store'
export
default {
methods: {
activate () {
store.dispatch(
'INCREMENT')
}
}
}
|
這個組件沒有任何數據,但是點擊的時候調用 store.dispatch('INCREMENT')
,一會兒再返回來看。
下面更新一下 CounterDisplay.vue
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<template>
Count is
{{ counter }}
</template>
<script>
import store from '../store'
export default {
computed: {
counter () {
return store.state.counter
}
}
}
</script>
|
事情從這兒才真正有趣!我們不再訂閱共享的狀態數據的變化,而是使用 vue 的計算屬性來給 counter 同步 store 中的數據。
Vue 足夠聰明來計算出基於 store.state.counter
的計算屬性 counter
,無論 store 何時被更新,它將更新所有的關聯項。That’s it!
如果你刷新這個頁面,你將看到計數器依然正確工作。下面將逐步解釋發生了什么:
- vue 的事件處理函數是
activate
,這個方法調用了store.dispatch('INCREMENT')
; - 在這里,
INCREMENT
是一個動作的名稱。它表示 “這是 state 應該做出的那種改變”。我們還可以傳遞額外的其他參數給分發函數; - vue 指明了分發事件時應該調用哪個函數。現在我們只有一個,但是我們可以為大型應用定制的更復雜;
- 這個函數接收狀態數據的拷貝,並對它進行更新。vue 保留一份舊數據的拷貝用於后續的高級功能;
- 當狀態更新之后,vue 自動更新所有組件;
- 這些使得你的代碼可測試性更強,如果你做了這些的話。
這里是比辦法2更好的原因
假如在開發過程中所有狀態的拷貝都被保存下來,vue 開發者建立起所謂的“時間旅行調試器”是非常有可能的。除了一個聽起來超酷的超級英雄的名字,它將允許你在應用中撤銷行為、改變邏輯,以及開發的更快。
只要狀態改變,你就可以構建中間件。例如,你可以創建一個 logger 來記錄用戶執行的所有操作。如果他們發現了一個bug,你可以獲取到用戶日志,重新播放所有的行為,並正確的重現他們的bug。
通過強制你在一個地方(store)進行所有的動作,這是一個很好的參考,你團隊中的每一個人都可以使用你應用中所有修改狀態數據的方法。
還有很長的路要走
這里僅僅接觸到了 vuex 表面可以做的事情,它自身仍然是一個早期版本,我相信這將成為未來許多年里最成熟的模式之一。
你可以去網上找到關於如何組織 store 以及 vuex 文檔的更多信息。你可能需要花一些時間來理解所有的概念,甚至可能需要一些嘗試和錯誤才能找出正確的方法。
結語:處理實習生的代碼
你將應用移植到 vue.js,你的實習生仍舊可以找到方法在自己的組件中重寫 store.state.counter
。你明白的,這是最后一根稻草。然后繼續在你的 store.js
中增加一行代碼:
1
2
3
4
5
6
7
8
9
10
11
|
var store = new Vuex.Store({
state: {
counter:
0
},
mutations: {
INCREMENT (
state) {
state.counter++
}
},
strict: true // Vuex's patent pending anti-intern device
})
|
現在無論何時何人直接修改 store,將會拋出一個錯誤。請注意這會減慢你的應用運行的時間,這個配置可以在生產環境移除,相關示例請查文檔。