web后台的实现离不开表格和表单,在表单中,数据联动是我们常常遇到的功能。当产生复杂的异步联动时,我们提交表单时,如何保证数据的完整性和准确性呢?相信很多人都遇到过这种问题,在这里记录一下我的探索之旅...核心实现逻辑可以从方案三开始看
问题的产生
如下表单,存在字段A,字段B,以及一个保存按钮,当A失去焦点之后异步获取B,点击保存按钮,为了方便看到效果,将要提交的数据以JSON字符串的形式展示出来。
js
<template>
<el-form label-width="80px" :model="formData">
<el-form-item label="字段A">
<el-input v-model="formData.fieldA" @change="handleAChange"></el-input>
</el-form-item>
<el-form-item label="字段B">
<el-input v-model="formData.fieldB"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存</el-button>
</el-form-item>
<el-form-item> 此时保存的数据:{{ submitDataJSON }} </el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
submitDataJSON: "",
formData: {
fieldA: "",
fieldB: "",
},
};
},
methods: {
handleAChange() {
this.getFieldB();
},
getFieldB() {
setTimeout(() => {
this.formData.fieldB = this.formData.fieldA + "fieldB";
}, 2000);
},
handleSave() {
this.submitDataJSON = JSON.stringify(this.formData);
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
由于字段B异步获取,所以当我们输入字段A,马上点保存,保存的数据就会是上一次的字段B,如下图1所示:
图1
显然,这不是我们想要的结果,我们希望,点保存可以等到字段B获取到新值之后再触发保存操作。
方案一:字段B为必填时,使用必填项校验拦截
考虑到字段A变化引发字段B联动变化,那么字段A每次变化时,我们可以先把字段B清空。如果字段B必填,那保存时,我们会进行必填项校验,使保存操作被中断,从而避免提交错误的数据到后台。代码如下:
js
<template>
<el-form label-width="80px" :model="formData">
<el-form-item label="字段A">
<el-input v-model="formData.fieldA" @change="handleAChange"></el-input>
</el-form-item>
<el-form-item label="字段B" required>
<el-input v-model="formData.fieldB"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存</el-button>
</el-form-item>
<el-form-item> 此时保存的数据:{{ submitDataJSON }} </el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
submitDataJSON: "",
formData: {
fieldA: "",
fieldB: "",
},
};
},
methods: {
handleAChange() {
this.formData.fieldB = "";
this.getFieldB();
},
getFieldB() {
setTimeout(() => {
this.formData.fieldB = this.formData.fieldA + "fieldB";
}, 2000);
},
handleSave() {
if (!this.formData.fieldB) {
this.$message.warning("字段B必填");
return;
}
this.submitDataJSON = JSON.stringify(this.formData);
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
但是如果字段B非必填呢?前端就无法通过必填项校验来拦截错误数据的提交了。而且,必填项校验的限制,会导致保存动作中断,需要等字段B获取到之后再次手动触发保存才可实现保存操作。
���案二:既然保存的数据不对,那就让保存按钮禁用
字段A的变化触发字段B的查询,在字段B查询的过程中,我们将保存按钮禁用,如下代码:
js
...
<el-button type="primary" :disabled="disabled" @click="handleSave">保存</el-button>
...
...
getFieldB() {
this.disabled = true;
setTimeout(() => {
this.formData.fieldB = this.formData.fieldA + "fieldB";
this.disabled = false;
}, 2000);
},
...
同方案一:输入A之后迅速点保存,保存动作不会生效,需要等到字段B获取到之后二次手动触发保存操作
方案三:引入Promise监听获取字段B的状态
上面两个方案都需要两步操作,即获取字段B和保存需要单独触发。那我们能不能输入字段A后点一次保存,等到最新的字段B获取到后主动提交 呢?问题的关键在于我们需要知道字段B的获取状态,如下代码,我们引入Promise实现:
js
...
<script>
let promiseStatus = Promise.resolve();
export default {
data() {
return {
submitDataJSON: "",
formData: {
fieldA: "",
fieldB: "",
},
};
},
methods: {
handleAChange() {
this.formData.fieldB = "";
this.getFieldB();
},
getFieldB() {
promiseStatus = new Promise((resolve) => {
setTimeout(() => {
this.formData.fieldB = this.formData.fieldA + "fieldB";
resolve();
}, 2000);
});
},
handleSave() {
promiseStatus.finally(() => {
this.submitDataJSON = JSON.stringify(this.formData);
});
},
},
};
</script>
如上:我们将获取字段B的操作包装成一个promise,并把它赋值给一个变量,那它的状态就会变的可跟踪。我们点保存之后就可以保证在获取到最新的B之后执行保存操作了。
将方案三通用化
上述案例,A字段变更后只会触发B的查询,在现实需求中,我们往往会有更复杂的联动逻辑,比如A字段变更后,触发B、C、D...字段的异步查询(B、C、D...无依赖关系);或者A字段变更后,触发B字段的异步查询,查到B字段后,再触发C字段的异步查询...(B、C、D...有依赖关系)。为了方便管理和使用这种涉及到多个Promise的场景,我们实现了一个异步队列,引入Promise.all,如下:
js
// requestQueue.js
import { Loading } from "element-ui";
export default class RequestQueue {
constructor() {
this.requests = [];
this.isProcessing = false;
// 保存在requests执行过程中添加进来的新Promise
this.newRequests = [];
this.loadingInstance = null;
}
// 默认添加进来的异步任务为Promise
addRequest(requestFn) {
if (!this.isProcessing) {
this.requests.push(requestFn);
return;
}
this.newRequests.push(requestFn);
}
clearQueue() {
this.requests = [];
this.newRequests = [];
}
requestsCompleted(saveFn) {
this.isProcessing = true;
if (!this.loadingInstance) {
this.loadingInstance = Loading.service({ fullscreen: true });
}
Promise.all(this.requests).finally(() => {
this.isProcessing = false;
// 处理暂存的新请求
if (this.newRequests.length > 0) {
this.requests = this.newRequests;
this.newRequests = [];
this.requestsCompleted(saveFn);
return;
}
this.loadingInstance.close();
this.loadingInstance = null;
saveFn();
this.clearQueue();
});
}
}
使用方式如下:
js
<template>
<el-form label-width="80px" :model="formData">
<el-form-item label="字段A">
<el-input v-model="formData.fieldA" @change="handleAChange"></el-input>
</el-form-item>
<el-form-item label="字段B">
<el-input v-model="formData.fieldB" @change="handleBChange"></el-input>
</el-form-item>
<el-form-item label="字段C">
<el-input v-model="formData.fieldC"></el-input>
</el-form-item>
<el-form-item label="字段D">
<el-input v-model="formData.fieldD"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存</el-button>
</el-form-item>
<el-form-item> 此时保存的数据:{{ submitDataJSON }} </el-form-item>
</el-form>
</template>
<script>
import RequestQueue from "../utils/requestQueue.js";
const requestQueue = new RequestQueue();
export default {
data() {
return {
submitDataJSON: "",
formData: {
fieldA: "",
fieldB: "",
fieldC: "",
fieldD: "",
},
};
},
methods: {
handleAChange() {
this.getFieldB();
this.getFieldC();
},
handleBChange() {
this.getFieldD();
},
getFieldB() {
const promise = new Promise((resolve) => {
setTimeout(() => {
this.formData.fieldB = this.formData.fieldA + ":fieldB";
this.getFieldD();
resolve();
}, 2000);
});
requestQueue.addRequest(promise);
},
getFieldC() {
const promise = new Promise((resolve) => {
setTimeout(() => {
this.formData.fieldC = this.formData.fieldA + ":fieldC";
resolve();
}, 2000);
});
requestQueue.addRequest(promise);
},
getFieldD() {
const promise = new Promise((resolve) => {
setTimeout(() => {
this.formData.fieldD = this.formData.fieldB + ":fieldD";
resolve();
}, 2000);
});
requestQueue.addRequest(promise);
},
handleSave() {
requestQueue.requestsCompleted(this.handleRequest);
},
handleRequest() {
this.submitDataJSON = JSON.stringify(this.formData);
// 保存数据
},
},
created() {
requestQueue.clearQueue();
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
如下图2,输入A,马上点保存,B、C、D都获取到,才会执行保存操作,将查询到的B、C、D数据展示在页面上:
图2