在使用 Vue 进行表单处理时,我们通常会使用 v-model
来建立双向绑定。但是,如果将表单数据交由 Vuex 管理,这时的双向绑定就会引发问题,因为在 严格模式 下,Vuex 是不允许在 Mutation 之外的地方修改状态数据的。以下用一个简单的项目举例说明,完整代码可在 GitHub(链接 ) 查看。
src/store/table.js
1 2 3 4 5 6 7 8 export default { state : { namespaced : true , table : { table_name : '' } } }
src/components/NonStrict.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <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 >
当我们在“表名”字段输入文字时,浏览器会报以下错误:
1 2 3 4 错误:[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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <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
1 2 3 4 5 6 7 export default { mutations : { setTable (state, payload) { state.table = payload } } }
以上方式有两个缺陷。其一,在提交状态更新后,若继续修改表单数据,同样会得到“禁止修改”的错误提示。这是因为 setTable
方法将本地状态对象直接传入了 Vuex,我们可以对该方法稍作修改:
1 2 3 4 5 6 setTable (state, payload) { _.assign (state.table , payload) state.table = _.cloneDeep (payload) }
第二个问题在于如果其他组件也向 Vuex 提交了数据变动(如弹出的对话框中包含了一个子表单),当前表单的数据不会得到更新。这时,我们就需要用到 Vue 的监听机制了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <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
1 2 3 4 5 6 7 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <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 风格的属性路径。
1 2 3 4 5 6 mutations : { myUpdateField (state, payload) { const { path, value } = payload _.set (state, path, value) } }
在组件中,我们将传入的“别名 - 路径”对转换成相应的 Getter / Setter 访问器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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
方法就实现了上述功能。
参考资料