在使用 Vue 進行表單處理時,我們通常會使用 v-model
來建立雙向綁定。但是,如果將表單數據交由 Vuex 管理,這時的雙向綁定就會引發問題,因爲在 嚴格模式 下,Vuex 是不允許在 Mutation 之外的地方修改狀態數據的。以下用一個簡單的項目舉例說明,完整代碼可在 GitHub(鏈接) 查看。
src/store/table.js
export default {
state: {
namespaced: true,
table: {
table_name: ''
}
}
}
src/components/NonStrict.vue
<b-form-group label="表名:">
<b-form-input v-model="table.table_name" />
</b-form-group>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('table', [
'table'
])
}
}
</script>
當我們在“表名”字段輸入文字時,瀏覽器會報以下錯誤:
錯誤:[vuex] 禁止在 Mutation 之外修改 Vuex 狀態數據。
at assert (vuex.esm.js?358c:97)
at Vue.store._vm.$watch.deep (vuex.esm.js?358c:746)
at Watcher.run (vue.esm.js?efeb:3233)
當然,我們可以選擇不開啓嚴格模式,只是這樣就無法通過工具追蹤到每一次的狀態變動了。下面我將列舉幾種解決方案,描述如何在嚴格模式下進行表單處理。
將狀態複製到組件中
第一種方案是直接將 Vuex 中的表單數據複製到本地的組件狀態中,並在表單和本地狀態間建立雙向綁定。當用戶提交表單時,再將本地數據提交到 Vuex 狀態庫中。
src/components/LocalCopy.vue
<b-form-input v-model="table.table_name" />
<script>
import _ from 'lodash'
export default {
data () {
return {
table: _.cloneDeep(this.$store.state.table.table)
}
},
methods: {
handleSubmit (event) {
this.$store.commit('table/setTable', this.table)
}
}
}
</script>
src/store/table.js
export default {
mutations: {
setTable (state, payload) {
state.table = payload
}
}
}
以上方式有兩個缺陷。其一,在提交狀態更新後,若繼續修改表單數據,同樣會得到“禁止修改”的錯誤提示。這是因爲 setTable
方法將本地狀態對象直接傳入了 Vuex,我們可以對該方法稍作修改:
setTable (state, payload) {
// 將對象屬性逐一賦值給 Vuex
_.assign(state.table, payload)
// 或者,克隆整個對象
state.table = _.cloneDeep(payload)
}
第二個問題在於如果其他組件也向 Vuex 提交了數據變動(如彈出的對話框中包含了一個子表單),當前表單的數據不會得到更新。這時,我們就需要用到 Vue 的監聽機制了:
<script>
export default {
data () {
return {
table: _.cloneDeep(this.$store.state.table.table)
}
},
computed: {
storeTable () {
return _.cloneDeep(this.$store.state.table.table)
}
},
watch: {
storeTable (newValue) {
this.table = newValue
}
}
}
</script>
這個方法還能同時規避第一個問題,因爲每當 Vuex 數據更新,本地組件都會重新克隆一份數據。
響應表單更新事件並提交數據
一種類似 ReactJS 的做法是,棄用 v-model
,轉而使用 :value
展示數據,再通過監聽 @input
或 @change
事件來提交數據變更。這樣就從雙向綁定轉換爲了單向數據流,Vuex 狀態庫自此成爲整個應用程序的唯一數據源(Single Source of Truth)。
src/components/ExplicitUpdate.vue
<b-form-input :value="table.table_name" @input="updateTableForm({ table_name: $event })" />
<script>
export default {
computed: {
...mapState('table', [
'table'
])
},
methods: {
...mapMutations('table', [
'updateTableForm'
])
}
}
</script>
src/store/table.js
export table {
mutations: {
updateTableForm (state, payload) {
_.assign(state.table, payload)
}
}
}
以上方法也是 Vuex 文檔 所推崇的。而根據 Vue 文檔 的介紹,v-model
本質上也是一個“監聽 - 修改”流程的語法糖而已。
使用 Vue 計算屬性
Vue 的計算屬性(Computed Property)可以配置雙向的訪問器(Getter / Setter),我們可以利用其建立起 Vuex 狀態庫和本地組件間的橋樑。其中一個限制在於計算屬性無法支持嵌套屬性(table.table_name
),因此我們需要爲這些屬性設置別名。
src/components/ComputedProperty.vue
<b-form-input v-model="tableName" />
<b-form-select v-model="tableCategory" />
<script>
export default {
computed: {
tableName: {
get () {
return this.$store.state.table.table.table_name
},
set (value) {
this.updateTableForm({ table_name: value })
}
},
tableCategory: {
get () {
return this.$store.state.table.table.category
},
set (value) {
this.updateTableForm({ category: value })
}
},
},
methods: {
...mapMutations('table', [
'updateTableForm'
])
}
}
</script>
如果表單字段數目過多,全部列出不免有些繁瑣,我們可以創建一些工具函數來實現。首先,在 Vuex 狀態庫中新增一個可修改任意屬性的 Mutation,它接收一個 Lodash 風格的屬性路徑。
mutations: {
myUpdateField (state, payload) {
const { path, value } = payload
_.set(state, path, value)
}
}
在組件中,我們將傳入的“別名 - 路徑”對轉換成相應的 Getter / Setter 訪問器。
const mapFields = (namespace, fields) => {
return _.mapValues(fields, path => {
return {
get () {
return _.get(this.$store.state[namespace], path)
},
set (value) {
this.$store.commit(`${namespace}/myUpdateField`, { path, value })
}
}
})
}
export default {
computed: {
...mapFields('table', {
tableName: 'table.table_name',
tableCategory: 'table.category',
})
}
}
開源社區中已經有人建立了一個名爲 vuex-map-fields 的項目,其 mapFields
方法就實現了上述功能。