表单处理 #

表单处理的挑战 #

在 Vuex 中处理表单时,需要考虑:

  • 严格模式下不能直接修改状态
  • 表单需要双向绑定
  • 表单验证和错误处理
  • 表单重置和取消

基本方法 #

双向绑定计算属性 #

vue
<template>
  <form @submit.prevent="submit">
    <input v-model="userName" placeholder="Name" />
    <input v-model="userEmail" placeholder="Email" />
    <button type="submit">Submit</button>
  </form>
</template>

<script>
export default {
  computed: {
    userName: {
      get() {
        return this.$store.state.user.name
      },
      set(value) {
        this.$store.commit('SET_USER_NAME', value)
      }
    },
    
    userEmail: {
      get() {
        return this.$store.state.user.email
      },
      set(value) {
        this.$store.commit('SET_USER_EMAIL', value)
      }
    }
  },
  
  methods: {
    submit() {
      this.$store.dispatch('user/saveProfile')
    }
  }
}
</script>

使用 mapState 和 mapMutations #

vue
<script>
import { mapState, mapMutations } from 'vuex'

export default {
  computed: {
    ...mapState('user', ['name', 'email'])
  },
  
  methods: {
    ...mapMutations('user', ['SET_NAME', 'SET_EMAIL']),
    
    updateName(e) {
      this.SET_NAME(e.target.value)
    },
    
    updateEmail(e) {
      this.SET_EMAIL(e.target.value)
    }
  }
}
</script>

局部副本模式 #

创建副本 #

vue
<template>
  <form @submit.prevent="submit">
    <input v-model="form.name" />
    <input v-model="form.email" />
    <button type="submit">Save</button>
    <button type="button" @click="reset">Cancel</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      form: {}
    }
  },
  
  created() {
    this.reset()
  },
  
  methods: {
    reset() {
      // 创建状态副本
      this.form = { ...this.$store.state.user }
    },
    
    submit() {
      this.$store.dispatch('user/updateProfile', this.form)
    }
  }
}
</script>

深拷贝 #

javascript
// 对于嵌套对象,使用深拷贝
created() {
  this.form = JSON.parse(JSON.stringify(this.$store.state.user))
}

表单状态管理模块 #

创建表单模块 #

javascript
// store/modules/form.js
export default {
  namespaced: true,
  
  state: () => ({
    values: {},
    errors: {},
    touched: {},
    dirty: false,
    submitting: false
  }),
  
  mutations: {
    SET_VALUE(state, { field, value }) {
      state.values[field] = value
      state.dirty = true
    },
    
    SET_ERROR(state, { field, error }) {
      state.errors[field] = error
    },
    
    CLEAR_ERROR(state, field) {
      delete state.errors[field]
    },
    
    SET_TOUCHED(state, field) {
      state.touched[field] = true
    },
    
    SET_SUBMITTING(state, submitting) {
      state.submitting = submitting
    },
    
    RESET(state) {
      state.values = {}
      state.errors = {}
      state.touched = {}
      state.dirty = false
      state.submitting = false
    }
  },
  
  actions: {
    async submit({ state, commit, dispatch }, { action, onSuccess }) {
      commit('SET_SUBMITTING', true)
      
      try {
        await dispatch(action, state.values, { root: true })
        onSuccess?.()
      } catch (error) {
        // 处理验证错误
        if (error.errors) {
          Object.entries(error.errors).forEach(([field, message]) => {
            commit('SET_ERROR', { field, error: message })
          })
        }
        throw error
      } finally {
        commit('SET_SUBMITTING', false)
      }
    }
  },
  
  getters: {
    isValid: state => Object.keys(state.errors).length === 0,
    hasChanges: state => state.dirty
  }
}

使用表单模块 #

vue
<template>
  <form @submit.prevent="submit">
    <div>
      <input 
        v-model="values.name"
        @blur="touch('name')"
        placeholder="Name"
      />
      <span v-if="errors.name" class="error">{{ errors.name }}</span>
    </div>
    
    <div>
      <input 
        v-model="values.email"
        @blur="touch('email')"
        placeholder="Email"
      />
      <span v-if="errors.email" class="error">{{ errors.email }}</span>
    </div>
    
    <button type="submit" :disabled="submitting || !isValid">
      {{ submitting ? 'Saving...' : 'Save' }}
    </button>
  </form>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('userForm', ['values', 'errors', 'submitting']),
    ...mapGetters('userForm', ['isValid', 'hasChanges'])
  },
  
  methods: {
    ...mapMutations('userForm', ['SET_VALUE', 'SET_TOUCHED', 'RESET']),
    ...mapActions('userForm', ['submit']),
    
    touch(field) {
      this.SET_TOUCHED(field)
    },
    
    async submit() {
      await this.submit({
        action: 'user/updateProfile',
        onSuccess: () => {
          this.$router.push('/profile')
        }
      })
    }
  },
  
  created() {
    // 初始化表单值
    const user = this.$store.state.user
    Object.entries(user).forEach(([key, value]) => {
      this.SET_VALUE({ field: key, value })
    })
  }
}
</script>

表单验证 #

验证器函数 #

javascript
// utils/validators.js
export const validators = {
  required: (value) => !!value || 'This field is required',
  
  email: (value) => {
    const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return pattern.test(value) || 'Invalid email format'
  },
  
  minLength: (min) => (value) => 
    value.length >= min || `Minimum ${min} characters`,
  
  maxLength: (max) => (value) => 
    value.length <= max || `Maximum ${max} characters`,
  
  match: (field) => (value, values) => 
    value === values[field] || 'Values do not match'
}

验证 Action #

javascript
// store/modules/form.js
import { validators } from '@/utils/validators'

actions: {
  validate({ state, commit }, rules) {
    let isValid = true
    
    Object.entries(rules).forEach(([field, fieldRules]) => {
      const value = state.values[field]
      
      for (const rule of fieldRules) {
        const result = rule(value, state.values)
        
        if (result !== true) {
          commit('SET_ERROR', { field, error: result })
          isValid = false
          break
        } else {
          commit('CLEAR_ERROR', field)
        }
      }
    })
    
    return isValid
  }
}

使用验证 #

vue
<script>
export default {
  methods: {
    async submit() {
      const rules = {
        name: [validators.required, validators.minLength(2)],
        email: [validators.required, validators.email],
        password: [validators.required, validators.minLength(8)],
        confirmPassword: [
          validators.required,
          validators.match('password')
        ]
      }
      
      const isValid = await this.$store.dispatch('form/validate', rules)
      
      if (isValid) {
        await this.$store.dispatch('user/register', this.values)
      }
    }
  }
}
</script>

动态表单 #

字段数组 #

javascript
// store/modules/form.js
state: () => ({
  values: {
    items: [{ name: '', quantity: 1 }]
  }
}),

mutations: {
  ADD_ITEM(state) {
    state.values.items.push({ name: '', quantity: 1 })
  },
  
  REMOVE_ITEM(state, index) {
    state.values.items.splice(index, 1)
  },
  
  UPDATE_ITEM(state, { index, field, value }) {
    state.values.items[index][field] = value
  }
}

动态表单组件 #

vue
<template>
  <form @submit.prevent="submit">
    <div v-for="(item, index) in items" :key="index">
      <input v-model="item.name" placeholder="Item name" />
      <input v-model.number="item.quantity" type="number" />
      <button type="button" @click="removeItem(index)">Remove</button>
    </div>
    
    <button type="button" @click="addItem">Add Item</button>
    <button type="submit">Submit</button>
  </form>
</template>

<script>
import { mapState, mapMutations } from 'vuex'

export default {
  computed: {
    ...mapState('orderForm', {
      items: state => state.values.items
    })
  },
  
  methods: {
    ...mapMutations('orderForm', ['ADD_ITEM', 'REMOVE_ITEM']),
    
    addItem() {
      this.ADD_ITEM()
    },
    
    removeItem(index) {
      this.REMOVE_ITEM(index)
    }
  }
}
</script>

最佳实践 #

1. 使用局部副本处理复杂表单 #

javascript
// 复杂表单使用局部副本
data() {
  return {
    form: JSON.parse(JSON.stringify(this.$store.state.user))
  }
}

2. 防抖输入 #

javascript
import { debounce } from 'lodash'

methods: {
  updateValue: debounce(function(value) {
    this.$store.commit('SET_VALUE', value)
  }, 300)
}

3. 表单重置 #

javascript
methods: {
  reset() {
    this.form = { ...this.$store.state.user }
    this.errors = {}
  }
}

总结 #

表单处理要点:

方法 适用场景
计算属性 简单表单、少量字段
局部副本 复杂表单、需要取消功能
表单模块 大型表单、需要验证
动态表单 字段数量可变的表单

继续学习 热更新,了解开发时热更新配置。

最后更新:2026-03-28