一、前言
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-bind
和v-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当做入参存于数组,然后帮他生成get
、set
的函数。虽然方案已经有4年左右的时间了,但是在vue2
中使用vuex
还是很便捷。
优点
基本上算是vue2
中使用vuex
处理表单的最优解,而且包的体积也比较小,对各个浏览器的支持也比较良好。
缺点
因为是通过遍历和Object.defineProperty
的方式来劫持属性的,当监听的属性特别多时,会出现跟vue2
一样的性能问题。但是!!大部分项目中,vuex
的变量并没有那么庞大,这一点性能的问题基本是可以忽略的。
方案四:使用proxy对vuex对象进行代理
思路
受方案三的启发,突然想到可以利用proxy
天然支持劫持一整个对象中所有属性的特性,直接创建vuex
对象对应的代理。然后重写handler
中的get
、set
方法,用来获取和提交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>