在输入框中使用vuex字段的4种方案

1,113 阅读1分钟

一、前言

1.1 为什么要写这篇文章

表单中的输入框是每个前端都会接触到的场景,vuex在很多vue项目中也会使用到,所以在表单的输入框中使用vuex字段的场景,是大多数vue项目都会遇到的。所以,正确地在输入框中使用vuex的字段是非常有必要的!!比如下面这种场景:

1.2 v-model的限制

在输入框中使用v-model这个指令时,v-model中的字段值会随着输入值而改变。平常用在data中声明的字段时非常方便,但是使用vuex中的字段时,在vuex声明严格模式的时候,会直接报错。这个是因为没有通过commit的方式直接改变了vuex对象中属性的值,虽然实际上这个值会被改变,但是通过非commit的方式,会让这种改变难以追踪。

二、解决方案

方案一:使用:value和@input代替v-model

思路:

其实vue的官方文档也有说到,v-model这个指令不过是v-bindv-on的语法糖而已。那按照这种思路的话,那么其实就可以在:value中使用vuex的变量,在@input或者@change的回调中使用commit

实现

<input :value="message" @input="updateMessage">
// ...
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}

缺点:

使用:value@change来处理输入框不是很方便,而且字段多的时候,要写很多类似的updateMessage的函数,代码看起来比较冗余。

方案二:重写computed中vuex字段的get、set方法

思路

一般我们在computed中使用vuex的字段时,会返回store中对应的值。这种方式就相当于把返回的值放到get函数中,然后新增一个set方法用于处理commit的操作。这样既能在v-model中改变computed的值,又能正确地执行commit操作。

实现

<input v-model="message">
// ...
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

优点

保留了v-model的用法,比方案一用起来要方便一些

缺点

这两个建议都非常简单,但在处理大量表单字段时,它们很快就会变得很麻烦。比如会出现下面这种要重写一堆get set方法的情况:

<template>
  <input v-model="firstName">
  <input v-model="lastName">
  <input v-model="message">
  <!-- ... -->
</template>
computed: {
    firstName: {
      get() {
        return this.$store.state.form.firstName;
      },
      set(value) {
        this.$store.commit('updateFirstName', value);
      },
    },
    lastName: {
      get() {
        return this.$store.state.form.lastName;
      },
      set(value) {
        this.$store.commit('updateLastName', value);
      },
    },
    message: {
      get() {
        return this.$store.state.form.message;
      },
      set(value) {
        this.$store.commit('updateMessage', value);
      },
    },
    // ...
  },

方案三:github的vuex-map-fields

简介

团队有小伙伴分享了一个npm包叫vuex-map-fields,专门对多字段情况进行了优化。

社区方案使用示例

<template>
  <input v-model="firstName">
  <input v-model="lastName">
  <input v-model="message">
  <!-- ... -->
</template>

<script>
import { mapFields } from 'vuex-map-fields';

export default {
  computed: {
    ...mapFields([
      'form.firstName',
      'form.lastName',
      'form.message',
      // ...
    ]),
  },
};
</script>

社区方案原理

其实不难看出之前代码中,多个vuex字段在computed定义时,结构基本都是一样的,只是最后key不一样而已。这里就相当于只把key当做入参存于数组,然后帮他生成getset的函数。虽然方案已经有4年左右的时间了,但是在vue2中使用vuex还是很便捷。

优点

基本上算是vue2中使用vuex处理表单的最优解,而且包的体积也比较小,对各个浏览器的支持也比较良好。

缺点

因为是通过遍历和Object.defineProperty的方式来劫持属性的,当监听的属性特别多时,会出现跟vue2一样的性能问题。但是!!大部分项目中,vuex的变量并没有那么庞大,这一点性能的问题基本是可以忽略的。

方案四:使用proxy对vuex对象进行代理

思路

受方案三的启发,突然想到可以利用proxy天然支持劫持一整个对象中所有属性的特性,直接创建vuex对象对应的代理。然后重写handler中的getset方法,用来获取和提交vuex对应的属性。

使用示例

这里提供一个最简单的示例,共分为三个部分。
可以直接点击codepen链接查看效果

  • vue实例/组件
  • vuex仓库
  • template

demo代码展示

import store from '../store/index.js'
// Vue.use(Vuex) //  vuex或vue-router在检测到 Vue 是可访问的全局变量时会自动调用 Vue.use()

/**
 * 这个handler是针对vuex中用户信息更改的
 * @author waldon
 * @date 2022-01-01
 */
const userInfoHandler = {
  get(target, prop) {
    return store.state.userInfo[prop]
  },
  set(target, prop, value) {
    console.log(`设置属性-${prop}:`, value)
    store.commit('setUserInfo', {
      prop,
      value,
    })
    return true
  },
}

new Vue({
  store,
  computed: {
    stateProxy() {
      return new Proxy(store.state.userInfo, userInfoHandler)
    },
  },
}).$mount('#app')

const store = new Vuex.Store({
  strict: true,
  state: {
    userInfo: {
      name: 'waldon',
      age: 18,
      sex: 'man',
    },
  },
  mutations: {
    setUserInfo(state, { prop, value }) {
      state.userInfo[prop] = value
    },
  },
})
export default store

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>v-model在vuex中使用</title>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.common.dev.js"></script>
  <style>
    .wrapper{
      border: 1px solid #eeeeee;
      box-sizing: border-box;
      padding: 20px;
      margin-bottom: 20px;
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="wrapper">
      使用代理:
      <p>
        名字:
        <input type="text" v-model="stateProxy.name">
      </p>
      <p>
        年龄:
        <input type="text" v-model="stateProxy.age">
      </p>
      <p>
        其他字段(主要是测试未定义的字段也可以被监听的特性):
        <input type="text" v-model="stateProxy.otherFiled">
      </p>
    </div>
    <div class="wrapper">
      使用vuex原本的字段(严格模式报错):
      <p>
        名字:
        <input type="text" v-model="$store.state.userInfo.name">
      </p>
    </div>
    <div class="wrapper">
      结果展示:
      <p>
        名字:{{ $store.state.userInfo.name }}
      </p>
      <p>
        年龄:{{ $store.state.userInfo.age }}
      </p>
      <p>
        未定义的字段:{{ $store.state.userInfo.otherFiled }}
      </p>
    </div>
  </div>
  <script src="https://unpkg.com/vuex@3.6.2/dist/vuex.js"></script>
  <script src="./src/main.js" type="module"></script>
</body>
</html>

三、参考