Vuex 嚴格模式下的表單處理

在使用 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 方法就實現了上述功能。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章