抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

在封装组件库或各种父子组件业务中,很多时候会需要既支持同步返回值,又支持通过 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;

/**
* 是否函数
* @param val 值
* @returns bool
*/
export function isFunction(val: any): val is Function {
return obj.call(val) === "[object Function]" || obj.call(val) === "[object AsyncFunction]";
}

/**
* 是否promise
* @param val 值
* @returns bool
*/
export function isPromise(val: any): val is Promise<any> {
return obj.call(val) === "[object Promise]";
}
/**
* 执行对象,获取真正的值
* @param val 值或函数或promise
* @param args 可传参
* @returns 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
// case 1 常量测试
run(5).then((res) => {
console.log("case 1", res); // res = 5
});

// case 2 Promise 类型
const func2 = new Promise((RES) => {
setTimeout(() => {
RES(20);
}, 1000);
});
run(func2).then((res) => {
console.log("case 2", res); // res = 20
});

// case 3 函数类型
const func3 = (x) => {
return x * 2;
};
run(func3, 5).then((res) => {
console.log("case 3", res); // res = 10
});

// case 4 函数Promise类型
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); // res = 15
});

// case 5 async 函数类型
const func5 = async (x, y) => {
const res = await func4(x, y); // 相乘
return res + 100; // 结果再+100
};
run(func5, 4, 5).then((res) => {
console.log("case 5", res); // res = 120
});

输出结果为

1
2
3
4
5
case 1 5
case 3 10
case 2 20
case 4 15
case 5 120

评论