【譯】vuex 基礎:教程和說明

原文地址:Vuex basics: Tutorial and explanation

作者注:[2016.11 更新]這篇文章是基于一個非常舊的 vuex api 版本而寫的,代碼來自于2015年12月。
但是,它仍能針對下面幾個問題深入探討:

  1. vuex 為什么重要
  2. vuex 如何工作
  3. vuex 如何使你的應用更容易維護

vuex 是 vue.js 作者開發的一個原型庫,它幫助你創建更大、維護性更強的應用,類似于 Facebook 的 flux 庫(以及由社區維護的 redux 庫)。
這篇文章不直接跳到 vuex 教你如何使用它,而是從背后的故事開始說起,逐步解釋它為什么是優雅的替代方法,以及將如何幫助你。

譯者注:a git repo of vuex-tutorial use vue2.0

你想要創建什么應用?

target.png

一個擁有按鈕計數器的簡單應用,點擊按鈕計數器加1。這聽起來非常容易理解和完成。

problem.dot.png

我們假設這個應用有兩個組件:

  1. 按鈕 (它是事件的來源)
  2. 計數器 (它必須按照事件來反映更新)

這兩個組件不知道彼此的存在,也不能相互通信。即使是在最小的 web 應用中,這也是一種非常常見的模式。在更大點兒的應用中,十幾個組件相互通信,并時刻關注對方的變化。不相信我?這里是一個基礎的 TODOlist 應用的交互清單:

todo.dot.png

這篇文章的目標

我們將討論解決同一個問題的3種方法:

  1. 組件之間使用事件廣播來通信
  2. 使用一個共享的狀態對象通信
  3. 使用 vuex 通信

讀完這篇文章,希望你能理解:

  1. 在你的項目中使用 vuex 的一個基本工作流程
  2. 它解決了哪些問題
  3. 相對其他方法,為什么它是更好的(盡管有些冗長和嚴格)

準備工作

我們將使用3種不同的方法來解決同一個問題。在這之前,需要做一些共同的準備工作。如果你打算跟著我做,我建議你為這個教程創建一個 git repo,這一小節結束后提交一次代碼,然后為不同的方法創建不同的分支。

$ npm install -g vue-cli
$ vue init webpack vuex-tutorial
$ cd vuex-tutorial
$ npm install
$ npm install --save vuex
$ npm run dev

現在你應該能看到 vue 的腳手架頁面了,下面來為我們要做的事來修改一些文件。

首先,在文件 src/components/IncrementButton.vue 中創建 IncrementButton 組件:

<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 組件來展示計數:

<template>
    Count is {{ count }}
</template>

<script>
export default {
    data () {
        return {
            count: 0
        }
    }
}
</script>

<style>
</style>

使用下面的內容替換 App.vue

<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:事件廣播

solution1.dot.png

來修改組件的代碼。
首先在 IncrementButton.vue 中,在按鈕被點擊時使用 $dispatch 給父組件發送一個消息。

export default {
    methods: {
        activate () {
            // Send an event upwards to be picked up by App
            this.$dispatch('button-pressed')
        }
    }
}

App.vue 中監聽來自子組件的這個消息事件,然后廣播一個新的事件 increment 給所有的子組件:

export default {
    components: {
        Counter,
        Increment
    },
    events: {
        'button-pressed': function () {
            // Send a message to all children
            this.$broadcast('increment')
        }
    }
}

CounterDisplay.vue 中,監聽 increment 事件,并增加狀態數據中的變量:

export default {
    data () {
        return {
            count: 0
        }
    },
    events: {
        increment () {
            this.count++
        }
    }
}

這個方法的缺點:

這個方法基本沒有什么技術上的錯誤。此外,在一個文件里實現整個應用的邏輯,專門使用 goto 來跳轉也沒有錯。這只與可維護性有關,這里會講一下為什么這個方法在可維護性上是糟糕的。

  1. 對于每一個操作,父組件都需要將事件分發給正確的組件;
  2. 在大型應用中,可能很難理解事件是從哪兒來的;
  3. 業務邏輯沒有明確的位置。this.count++ 是在 CounterDisplay 中,但業務邏輯可能到處都是,這會導致難以維護。

讓我來舉例說明一下這個方法會怎樣導致bug:

  1. 你雇了兩個實習生: Alice 和 Bob。你告訴 Alice 你需要為另外一個組件實現另一個計數器,告訴 Bob 寫一個重置按鈕;
  2. Alice 寫了一個新的組件 FormattedCounterDisplay,它能夠監聽增量,并增加自己的狀態數據。Alice 開心的提交了代碼;
  3. Bob 寫了一個新的 Reset 組件,它向應用發出一個 reset 事件,并重新分發它。他在 CounterDisplay 中將 count 重置為0,但是他沒有意識到 Alice 的組件也訂閱了這個變化;
  4. 你的用戶點擊 +1 按鈕后看到應用工作正常。但是當他點擊 重置 按鈕,只有一個計數器被重置了。這看起來是一個非常簡單的例子,僅僅為了說明狀態和業務邏輯綁在一起可能會導致錯誤。

方法2: 共享狀態

撤銷方法1中的改動,創建一個新文件 src/store.js

export default {
    state: {
        counter: 0
    }
}

首先修改 CounterDisplay.vue

<template>
    Count is {{ sharedState.counter }}
</template>

<script>
import store from '../store'

export default {
    data () {
        return {
            sharedState: store.state
        }
    }
}
</script>

這里我們做了一些有趣的事情:

  1. 獲取到一個 store 對象,它僅僅是一個對象常量,但是在不同的文件中定義的;
  2. 在本地數據中,我們創建了一個叫 sharedState 的數據,它映射到 store.state
  3. vue 使用 store.state 作為當前組件的一部分數據,這意味著 store.state 有任何變化,vue 都會自動更新 sharedState。

到目前為止它還不能工作,現在我們來修改 IncrementButton.vue

import store from '../store'

export default {
    data () {
        return {
            sharedState: store.state
        }
    },
    methods: {
        activate () {
            this.sharedState.counter += 1
        }
    }
}
  1. 在這里,我們引入 store,并像之前的例子一樣監聽了數據的狀態變化;
  2. activate 方法被調用時,指向 store.statesharedState 的計數器 counter 增加;
  3. 監聽了計數器的所有組件和計算屬性都會被更新。

它為什么比方法1更好

我們來回顧一下兩個實習生 Alice 和 Bob 的問題:

  1. Alice 寫的用來監聽共享數據的 FormattedComponentDisplay 組件將會始終顯示最新的 counter 數據;
  2. Bob 的重置按鈕組件將共享數據的 counter 置為0,這將同時影響 CounterDisplay 和 Alice 寫的 FormattedCounterDisplay;
  3. 重置按鈕符合預期。

為什么這樣仍然不夠好

  1. 在 Alice 和 Bob 的實習期內,他們使用不同的格式寫了許多計數器、重置按鈕,以及增量按鈕,它們更新的是同一份共享的數據,生活很美好;
  2. 一旦他們回到學校,你需要維護他們的代碼;
  3. 新任經理 Carol 進來之后說:“我不想看到計數器的數字超過100”

你現在該做什么?

  1. 你去十幾個組件的代碼里找到所有更新數據的地方嗎?這讓人沮喪;
  2. 你找到顯示數據的地方然后添加一個 filter/formatter 來格式化數據嗎?這同樣讓人沮喪;
  3. 這里就是這個問題,業務邏輯分散在應用的各個角落,原則上一個很簡單的問題,但是維護和調試起來卻特別痛苦。

稍好一點兒的方法

現在來重構你的代碼,重寫 store.js 如下:

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有些相似。給你看一張稍稍有些可怕的圖:

solution3.dot.png

首先來創建 src/store.js,這次用下面的代碼:

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

現在來看看這段代碼做了什么:

  1. 獲取 Vuex 模塊,然后使用 Vue.use 安裝這個插件;
  2. store 不再是一個普通的 JSON 對象,而是 Vuex.Store 的一個實例;
  3. state 中創建一個計數器 counter,設置為0;
  4. 創建一個新的變異對象,包含 INCREMENT 方法:獲取一個狀態數據,然后改變它。

看看這段代碼里有哪些有趣的東東:

  1. 所有通過 require('../store.js')import store from '../store.js' 引入的 store 將使用同一個 store 實例;
  2. 我們不會修改 store.state.counter,但是我們有一份 state 的拷貝用來做修改,這在接下來會很重要。

現在我們已經改好了 store,來繼續修改 IncrementButton.vue

import store from '../store'

export default {
    methods: {
        activate () {
            store.dispatch('INCREMENT')
        }
    }
}

這個組件沒有任何數據,但是點擊的時候調用 store.dispatch('INCREMENT'),一會兒再返回來看。

下面更新一下 CounterDisplay.vue

<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!

如果你刷新這個頁面,你將看到計數器依然正確工作。下面將逐步解釋發生了什么:

  1. vue 的事件處理函數是 activate,這個方法調用了 store.dispatch('INCREMENT')
  2. 在這里,INCREMENT 是一個動作的名稱。它表示 “這是 state 應該做出的那種改變”。我們還可以傳遞額外的其他參數給分發函數;
  3. vue 指明了分發事件時應該調用哪個函數?,F在我們只有一個,但是我們可以為大型應用定制的更復雜;
  4. 這個函數接收狀態數據的拷貝,并對它進行更新。vue 保留一份舊數據的拷貝用于后續的高級功能;
  5. 當狀態更新之后,vue 自動更新所有組件;
  6. 這些使得你的代碼可測試性更強,如果你做了這些的話。

這里是比辦法2更好的原因

假如在開發過程中所有狀態的拷貝都被保存下來,vue 開發者建立起所謂的“時間旅行調試器”是非常有可能的。除了一個聽起來超酷的超級英雄的名字,它將允許你在應用中撤銷行為、改變邏輯,以及開發的更快。
只要狀態改變,你就可以構建中間件。例如,你可以創建一個 logger 來記錄用戶執行的所有操作。如果他們發現了一個bug,你可以獲取到用戶日志,重新播放所有的行為,并正確的重現他們的bug。
通過強制你在一個地方(store)進行所有的動作,這是一個很好的參考,你團隊中的每一個人都可以使用你應用中所有修改狀態數據的方法。

還有很長的路要走

這里僅僅接觸到了 vuex 表面可以做的事情,它自身仍然是一個早期版本,我相信這將成為未來許多年里最成熟的模式之一。
你可以去網上找到關于如何組織 store 以及 vuex 文檔的更多信息。你可能需要花一些時間來理解所有的概念,甚至可能需要一些嘗試和錯誤才能找出正確的方法。

結語:處理實習生的代碼

你將應用移植到 vue.js,你的實習生仍舊可以找到方法在自己的組件中重寫 store.state.counter。你明白的,這是最后一根稻草。然后繼續在你的 store.js 中增加一行代碼:

var store = new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
    INCREMENT (state) {
      state.counter++
    }
  },
  strict: true // Vuex's patent pending anti-intern device
})

現在無論何時何人直接修改 store,將會拋出一個錯誤。請注意這會減慢你的應用運行的時間,這個配置可以在生產環境移除,相關示例請查文檔。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容

  • 譯文地址Vuex basics: Tutorial and explanation 更新于2016年11月:這篇文...
    莫莫小熊閱讀 1,209評論 0 2
  • Vuex是什么? Vuex 是一個專為 Vue.js應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件...
    蕭玄辭閱讀 3,130評論 0 6
  • 安裝 npm npm install vuex --save 在一個模塊化的打包系統中,您必須顯式地通過Vue.u...
    蕭玄辭閱讀 2,959評論 0 7
  • Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,并以相應...
    白水螺絲閱讀 4,675評論 7 61
  • 死亡這個詞語,在我的感受里似乎沒有什么恐懼,和悲傷的成分在里面,更多的是司空見慣,不以為然了,從在實驗室里見到一個...
    奔跑的的兔子2016閱讀 215評論 0 0