在封装组件库或各种父子组件业务中,很多时候会需要既支持同步返回值,又支持通过 Promise 或 async/await 异步方式返回,最常见的比如 Modal 组件,既要支持点击 提交
按钮立刻关闭模态框,又要支持点击 提交
按钮后按钮进入 loading
状态且模态框不关闭,之后与后端进行交互,由后端返回结果来决定是否关闭模态框
不需要看场景模拟,可直接跳转到 四、重点
章节,复制逻辑判断部分代码即可
一、场景预设
1. 子组件模态框
假设我们封装了这么一个组件 a-model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <template> <model :visible="visible"> <slot> </slot>
<div class="flex-row justify-end"> <button>取消</button> <button type="primary" @click="handleSubmit">提交</button> </div> </model> </template>
<script setup> const props = defineProps<{ modelValue: boolean }>() const emit = defineEmits<{ ('submit'):void }>() const visible = ref(false)
const handleSubmit = ()=>{ emit('submit') } </script>
|
2. 父页面表单逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <a-model v-model="visible" @submit="handleOk"> <form> <!-- 此处模拟表单提交,点击模态框提交后,与后端交互,后端校验参数失败,模态框不应关闭而是继续让用户修改 --> </form> </a-model> </template>
<script setup> const visible = ref(false);
const handleOk = () => {}; </script>
|
二、改事件为变量
正常情况下已经能满足业务逻辑,即点击模态框按钮,外部能够触发submit
事件将数据传递给后端,但是这种做法比较粗糙
- 提交按钮并没有做防抖或节流,导致可能出现用户点击多次的情况
- 用户体验不好,例如立马关闭模态框,若请求失败用户得重新填一遍表单,若不立马关闭,则若有网络问题,用户多次点击的概率大大增加
- 开发者体验不好,代码处理不够优雅,用户得反复处理是否关闭模态框,一个项目中肯定很多地方都用类似逻辑,每次开发者都得手动处理
1. 修改模态框组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <template> <model :visible="visible"> <slot> </slot>
<div class="flex-row justify-end"> <button>取消</button> <!-- 新增loading --> <button type="primary" :loading="loading" @click="handleSubmit">提交</button> </div> </model> </template>
<script setup> const props = defineProps<{ modelValue: boolean // 新增ok属性,实际用于回调代替submit事件 ok: Function }>() const visible = ref(false) const loading = ref(false) const handleSubmit = ()=>{ if(loading.value){ return } loading.value = true props.ok((close=true)=>{ if(close){ visible.value = false } loading.value = false }) } </script>
|
2. 父页面表单逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <a-model v-model="visible" :ok="handleOk"> <form> <!-- 此处模拟表单提交,点击模态框提交后,与后端交互,后端校验参数失败,模态框不应关闭而是继续让用户修改 --> </form> </a-model> </template>
<script setup> const visible = ref(false);
const handleOk = (done: Function) => { axios .post("/....") .then(() => { done(); }) .catch(() => { done(false); }); }; </script>
|
到此处,我们已经将其改为异步方式,并引入 loading 解决了重复提交的情况(若重要业务应后端也要防重,因为攻击者可以绕过 ui 直接请求后端,但对于 99%以上场景,前端处理已经足够),并且能够控制当请求成功,模态框自动关闭,请求失败,模态框不关闭且 loading 取消。
三、支持 Promise/async
回调这种方式,若没有查看文档,很多人并不知道如何使用,且每次都得手动调用 done,无法做到自动判断,我们将其改为更符合直觉的方式
1. 修改模态框组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <template> <model :visible="visible"> <slot> </slot>
<div class="flex-row justify-end"> <button>取消</button> <button type="primary" :loading="loading" @click="handleSubmit">提交</button> </div> </model> </template>
<script setup> const props = defineProps<{ modelValue: boolean ok: ()=>boolean | ()=>Promise<boolean> }>() const visible = ref(false) const loading = ref(false) const handleSubmit = ()=>{ if(loading.value){ return } loading.value = true const cb =props.ok() // 此处模拟判断是Promise if(cb is Promise){ cb.then(()=>{ loading.value = false visible.value = false }) .catch(()=>{ loading.value = false }) } // 同步的情况 else{ loading.value = false if(cb!==false){ visible.value = false } } } </script>
|
2. 修改父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <template> <a-model v-model="visible" :ok="handleOk"> <form> <!-- 此处模拟表单提交,点击模态框提交后,与后端交互,后端校验参数失败,模态框不应关闭而是继续让用户修改 --> </form> </a-model> </template>
<script setup> const visible = ref(false);
// 支持不返回或直接返回的同步方式 const handleOk = () => { // 不想关闭,直接返回false // return false };
// 也支持异步情况 const handleOk = () => { return new Promise((RES, REJ) => { axios .post("/....") .then(() => { RES(); }) .catch(() => { REJ(); }); }); }; </script>
|
四、重点
实际可以发现,其实只是对 ok
这个 props 变量的类型判断,Promise
还是 Function
等进行不同逻辑处理,若有封装过组件库的开发者应该很容易就能联想到,这种情况其实非常常见,例如封装 select
下拉框组件,我们的 dicData
变量允许是固定常量数组,也允许是从后端接口获取的动态数组等等,也一样需要类似的逻辑,所以我们可以抽离一下这块代码
1. 封装方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const obj = Object.prototype.toString;
export function isFunction(val: any): val is Function { return obj.call(val) === "[object Function]" || obj.call(val) === "[object AsyncFunction]"; }
export function isPromise(val: any): val is Promise<any> { return obj.call(val) === "[object Promise]"; }
export const run = async (val: any, ...args: any): Promise<any> => { if (isFunction(val)) { const result = val(...args); return run(result); } if (isPromise(val)) { return val.then((resolvedResult: any) => run(resolvedResult)); } return Promise.resolve(val); };
|
2. 测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| run(5).then((res) => { console.log("case 1", res); });
const func2 = new Promise((RES) => { setTimeout(() => { RES(20); }, 1000); }); run(func2).then((res) => { console.log("case 2", res); });
const func3 = (x) => { return x * 2; }; run(func3, 5).then((res) => { console.log("case 3", res); });
const func4 = (x, y) => { return new Promise<number>((RES) => { setTimeout(() => { RES(x * y); }, 1500); }); }; run(func4, 3, 5).then((res) => { console.log("case 4", res); });
const func5 = async (x, y) => { const res = await func4(x, y); return res + 100; }; run(func5, 4, 5).then((res) => { console.log("case 5", res); });
|
输出结果为
1 2 3 4 5
| case 1 5 case 3 10 case 2 20 case 4 15 case 5 120
|