- 原文作者:本文已獲得原作者 Benjamin Listwon 的授權
- 譯文出自:掘金翻譯計劃
- 譯者:linpu.li
- 校對者:malcolmyu,XatMassacrE
看起來在 Vue 里面困擾開發(fā)者的事情之一是如何在組件之間共享狀態(tài)社搅。對于剛剛接觸響應式編程的開發(fā)者來說吭净,像Vuex 這種庫添瓷,有著繁多的新名詞及其關注點分離的方式丧诺,往往令人望而生畏往产。特別是當你只希望分享一兩個數(shù)據(jù)片段時蝶防,(這一套邏輯的復雜性)就顯得有點過分了窄锅。
考慮到這一點的話,我想我應該把兩個簡短的演示放到一起展示出來木柬。第一個通過使用一個簡單的 JavaScript 對象皆串,在每個新組件當中引用來實現(xiàn)共享狀態(tài)。第二個做了和 Vuex 一樣的事情弄诲,當它運行成功的時候愚战,也是一個你絕對不應該做的事情的示例(我們將在最后看看為什么)。
你可以通過查看下面這些演示來開始:
或者獲取這個倉庫并在本地運行試試看齐遵!代碼里很多地方是2.0版本的特性寂玲,但我接下來想講的數(shù)據(jù)流概念在任何版本里都是相關的,并且它可以通過一些改變很輕易地向下兼容到1.0梗摇。
這些演示都是一樣的功能拓哟,只是實現(xiàn)的方法不同。應用程序由兩個獨立的聊天組件實例組成伶授。當用戶在一個實例里提交一個消息的時候断序,它應該在兩個聊天窗口都出現(xiàn),因為消息狀態(tài)是共享的糜烹,下面是一個截圖:
用一個對象共享狀態(tài)
開始前违诗,讓我們先來看看數(shù)據(jù)是如何在示例的應用程序當中流轉的。
在這個演示里疮蹦,我們將使用一個簡單的 JavaScript 對象:var store = {...}
诸迟,在Client.vue
組件的實例之間共享狀態(tài)。下面是關鍵文件的重要代碼部分:
index.html
<div id="app"></div>
<script>
var store = {
state: {
messages: []
},
newMessage (msg) {
this.state.messages.push(msg)
}
}
</script>
這里有兩個關鍵的地方:
- 我們通過把這個對象直接添加到
index.html
里來讓其對整個應用程序可用,也可以將它注入到應用程序里更下一層的作用鏈阵苇,但目前直接添加顯然更快捷簡單壁公。 - 我們在這里保存狀態(tài),但同時也提供了一個函數(shù)來調用它绅项。相比起分散在組件各處的函數(shù)紊册,我們更傾向于讓它們保持在一個地方(便于維護),并在任何需要它們的地方簡單使用快耿。
App.vue
<template>
<div id="app">
<div class="row">
<div class="col">
<client clientid="Client A"></client>
</div>
<div class="col">
<client clientid="Client B"></client>
</div>
</div>
</div>
</template>
<script>
import Client from './components/Client.vue'
export default {
components: {
Client
}
}
</script>
這里我們引入了 Client 組件囊陡,并創(chuàng)建了兩個它的實例,使用一個屬性:clientid
润努,來對每個實例進行區(qū)分关斜。事實上示括,你應該更動態(tài)地去實現(xiàn)這些铺浇,但別忘了,目前快捷簡單更重要垛膝。
注意一點鳍侣,到這里我們還完全沒有同步任何狀態(tài)。
Client.vue
<template>
<div>
<h1>{{ clientid }}</h1>
<div class="client">
<ul>
<li v-for="message in messages">
<label>{{ message.sender }}:</label> {{ message.text }}
</li>
</ul>
<div class="msgbox">
<input v-model="msg" placeholder="Enter a message, then hit [enter]" @keyup.enter="trySendMessage">
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
msg: '',
messages: store.state.messages
}
},
props: ['clientid'],
methods: {
trySendMessage() {
store.newMessage({
text: this.msg,
sender: this.clientid
})
this.resetMessage()
},
resetMessage() {
this.msg = ''
}
}
}
</script>
下面是應用程序的主要內容:
- 在該模板里吼拥,設置一個
v-for
循環(huán)去遍歷messages
集合倚聚。 - 綁定在文本輸入框上的
v-model
簡單地存儲了組件的本地數(shù)據(jù)對象msg
。 - 同樣在數(shù)據(jù)對象里凿可,我們創(chuàng)建了一個
store.state.messages
的引用惑折,它將觸發(fā)組件的更新。 - 最后枯跑,將 enter 鍵綁定到
trySendMessage
函數(shù)惨驶,這個函數(shù)包含了以下幾個功能:- 準備好需要存儲的數(shù)據(jù)(發(fā)送者和消息的一個字典對象)。
- 調用定義在共享存儲里的
newMessage
函數(shù)敛助。 - 調用一個清理函數(shù):
resetMessage
粗卜,重置輸入框。通常你更應該在一個promise
完成之后再調用它纳击。
這就是使用對象的方法续扔,來試一試。
用 Vuex 共享狀態(tài)
好了焕数,現(xiàn)在來試試看用 Vuex 實現(xiàn)纱昧。同樣的,先上圖堡赔,也便于我們將 Vuex 的術語(actions识脆,mutations等等)對應到我們剛剛完成的示例中。
正如你所看到的,Vuex 簡單地形式化了我們剛剛完成的過程存璃。使用它的時候仑荐,所做的事情其實和我們上面做過的非常像:
- 創(chuàng)建一個用來共享的存儲,在這個例子中它將通過 vue/vuex 注入到組件當中纵东。
- 定義組件可以調用的 actions粘招,它們仍然是集中定義的。
- 定義實際接觸存儲狀態(tài)的 mutations偎球。我們這么做洒扎,actions 就可以形成不止一個 mutation,或者執(zhí)行邏輯去決定調用哪一個 mutation衰絮。這意味著你再也不用擔心組件當中的業(yè)務邏輯了袍冷,成功!
- 當狀態(tài)更新時猫牡,任何擁有 getter胡诗,動態(tài)屬性和映射到 store 的組件都會被立即更新。
同樣再來看看代碼:
main.js
import store from './vuex/store'
new Vue({ // eslint-disable-line no-new
el: '#app',
render: (h) => h(App),
store: store
})
這次淌友,我們用 Vuex 創(chuàng)建了一個存儲并將其直接傳入應用程序當中煌恢,替代掉了之前index.html
中的 store
對象。在繼續(xù)之前震庭,先來看一下這個存儲:
store.js
export default new Vuex.Store({
state: {
messages: []
},
actions: {
newMessage ({commit}, msg) {
commit('NEW_MESSAGE', msg)
}
},
mutations: {
NEW_MESSAGE (state, msg) {
state.messages.push(msg)
}
},
strict: debug
})
和我們自己創(chuàng)建的對象非常相似瑰抵,但是多了一個mutations
對象。
Client.vue
<div class="row">
<div class="col">
<client clientid="Client A"></client>
</div>
<div class="col">
<client clientid="Client B"></client>
</div>
</div>
和上次一樣的配方器联。(驚人的相似二汛,對吧?)
Client.vue
<script>
import { mapState, mapActions } from 'vuex'
export default {
data() {
return {
msg: ''
}
},
props: ['clientid'],
computed: {
...mapState({
messages: state => state.messages
})
},
methods: {
trySendMessage() {
this.newMessage({
text: this.msg,
sender: this.clientid
})
this.resetMessage()
},
resetMessage() {
this.msg = ''
},
...mapActions(['newMessage'])
}
}
</script>
模板仍然剛好一樣拨拓,所以我甚至不需要費心怎么去引入它肴颊。最大的不同在于:
- 使用
mapState
來生成對共享消息集合的引用。 - 使用
mapActions
來生成創(chuàng)建一個新消息的動作(action)千元。
(注意:這些都是 Vuex 2.0特性苫昌。)
好的,做完啦幸海!也來看一下這個演示吧祟身。
結論
所以,正如你所希望看到的物独,自己進行簡單的狀態(tài)共享和使用 Vuex 進行共享并沒有多大區(qū)別袜硫。而 Vuex 最大的優(yōu)點在于它為你形式化了集中處理數(shù)據(jù)存儲的過程,并提供了所有功能方法去處理那些數(shù)據(jù)挡篓。
最初婉陷,當你閱讀 Vuex 的文檔和示例的時候帚称,它那些針對 mutations,actions 和 modules 的單獨文檔很容易讓人感覺困擾秽澳。但是如果你敢于跨出那一步闯睹,簡單地在store.js
文件里寫一些關于它們的代碼來開始學習。隨著這個文件的大小增加担神,你就將找到正確的時間移步到actions.js
里楼吃,或者是把它們更進一步地分離開來。
不要著急妄讯,慢慢來孩锡,一步一個臺階。當然也可以使用vue-cli從創(chuàng)建一個模板開始亥贸,我使用browserify模板躬窜,并把下面的代碼添加進我的package.json
文件。
"dependencies": {
"vue": "^2.0.0-rc.6",
"vuex": "^2.0.0-rc.5"
}
還在看嗎炕置?
我知道我還說過要再講一個“不好的”方式荣挨。再次,這個演示恰好也是一樣的讹俊。不好的地方在于我利用了 Vue 2.0 里單向綁定的特性來注入回調函數(shù)垦沉,從而允許了父子模板之間順序的雙向綁定。首先仍劈,來看一下2.0文檔中的這個部分,然后再來看看我這個不好的方法寡壮。
App.vue
<div class="row">
<div class="col">
<client clientid="Client A" :messages="messages" :callback="newMessage"></client>
</div>
<div class="col">
<client clientid="Client B" :messages="messages" :callback="newMessage"></client>
</div>
</div>
這里贩疙,我在組件上使用了一個屬性將一個動態(tài)綁定傳遞到messages
集合里。但是况既,我同時還傳遞了一個動作函數(shù)这溅,所以可以在子組件里調用它。
Client.vue
<script>
export default {
data() {
return {
msg: ''
}
},
props: ['clientid', 'messages', 'callback'],
methods: {
trySendMessage() {
this.callback({
text: this.msg,
sender: this.clientid
})
this.resetMessage()
},
resetMessage() {
this.msg = ''
}
}
}
</script>
這里就是不好的做法棒仍。
要問為什么有這么不好嗎悲靴?
- 我們正在破壞之前圖中所展示的單向循環(huán)。
- 我們創(chuàng)建了一個在組件及其父組件之間的緊密耦合莫其。
- 這將變得不可維護癞尚。如果你在組件里需要20個函數(shù),你就將添加20個屬性乱陡,管理它們的命名等等浇揩,然后,如果任何東西發(fā)生改變憨颠,呃胳徽!
所以為什么還要再展示這段积锅?因為我和其他人一樣很懶。有時我就會做這樣的事情养盗,僅僅想知道再繼續(xù)做下去會有多么糟糕缚陷,然后我就會咒罵自己的懶惰,因為我可能要花上一小時或者一天的時間去清理它們往核。鑒于這種情況蹬跃,我希望我可以幫助你盡早避免無謂的決定和錯誤,千萬不要傳遞任何你不需要的東西铆铆。99%的情況下蝶缀,一個單獨的共享狀態(tài)已經(jīng)足夠完美。(不久再詳細講講那1%的情況)