用脚本来写函数式弹窗,更快更爽
前言
在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方式来向弹窗内更新props和处理弹窗的emit事件。
<template>
<n-button @click="open">open</n-button>
</template>
<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>
传统vue编写弹窗
通过变量来直接控制弹窗的开启和关闭。
<template>
<n-button @click="showModal = true">
来吧
</n-button>
<n-modal v-model:show="showModal" preset="dialog" title="Dialog">
<template #header>
<div>标题</div>
</template>
<div>内容</div>
<template #action>
<div>操作</div>
</template>
</n-modal>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
showModal: ref(false)
}
}
})
</script>
痛点
- 深层次的传props让人有很大的心理负担,污染组件props
- 要关注弹窗show变量的true,false
函数式弹窗
在主页面用Provider包裹一下
// RootPage.vue
<ModalProvider>
<ChildPage></ChildPage>
</ModalProvider>
<script setup lang="ts">
import ModalProvider from "./ModalProvider.vue"
import ChildPage from "./ChildPage.vue"
</script>
在页面内的某个子组件中,直接通过oepn方法打开弹窗
// ChidPage.vue
<template>
<n-button @click="open">open</n-button>
</template>
<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>
优势
- 对于使用者来说简单,没有控制show的心理负担
- 弹窗内容和其他业务代码分离,不会污染其他组件props和结构
- 能够充分复用同一个弹窗,通过函数参数的方式来更新弹窗内容,在不同地方打开同一个弹窗
劣势
- 对于一些简单需求的弹窗,用这种函数式的弹窗会有些臃肿
- 在使用函数式弹窗前需要做一些操作(Provider和Inject),会在后面提到以及解决方案。
如何使用这种函数式的弹窗
原理
通过在根页面将我们的Modal组件挂载上去,然后通过使用hook去管理这个modal组件的状态值(props,ref),然后通过provide将props和event和我们的modal组件联系起来,再在我们需要使用弹窗的地方使用inject更新props来启动弹窗。
步骤1(❌):编写Modal
这里我使用的是Naive Ui的Modal组件,按喜好选择就行。按照你的需求编写弹窗的内容。定义好props和emits,写好他们的类型申明。
// TestModal.vue
<template>
<n-modal
v-model:show="isShowModal"
preset="dialog"
@after-leave="handleClose"
>
...你的弹窗内容
</n-modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"
import { useVModel } from "@vueuse/core"
import { FormRules } from "naive-ui"
interface ModalProps {
show: boolean
testValue: string
}
// 中间区域不要修改
const props = defineProps<ModalProps>()
const formRef = ref()
const loading = ref(false)
const isShowModal = useVModel(props, "show")
// 中间区域不要修改
const rules: FormRules = []
const formData = reactive({
testValue: props.testValue,
})
const callBackData = computed(() => {
return {
formData,
}
})
watch(
() => props.show,
() => {
if (props.show) {
formData.testValue = props.testValue
} else {
formData.testValue = ""
}
}
)
const emits = defineEmits<{
(e: "update:show", value: boolean): void
(e: "close", param: typeof callBackData.value): void
(
e: "confirm",
param: typeof callBackData.value,
close: () => void,
endLoading: () => void
): void
(e: "cancel", param: typeof callBackData.value): void
}>()
function handleCancel() {
// 中间区域不要修改
emits("cancel", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}
function handleClose() {
// 中间区域不要修改
emits("close", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}
function handleConfirm() {
// 中间区域不要修改
loading.value = true
emits(
"confirm",
callBackData.value,
() => {
loading.value = false
isShowModal.value = false
},
() => {
loading.value = false
}
)
// 中间区域不要修改
}
</script>
步骤2(❌):编写hook来管理弹窗的状态
在这个文件里面,使用hook管理 TestModal
弹窗需要的props和event事件,然后我们通过ts类型体操来获取TestModal的props类型和event类型,我们向外inject一个 open
函数,这个函数可以更新 TestModal
的props和event,同时打开弹窗,他的参数有完整的类型提示,可以让使用者更加明确的使用我们的弹窗。
// use-test-modal.ts
import {
ref,
provide,
InjectionKey,
inject,
VNodeProps,
AllowedComponentProps,
reactive,
} from "vue";
import Modal from "./TestModal.vue";
/**
* 通过引入弹窗组件来获取组件的除show,update:show以外的props和emits来作为open函数的
*/
type ModalInstance = InstanceType<
typeof Modal extends abstract new (...args: any) => any ? typeof Modal : any
>["$props"];
type OpenParam = Omit<
{
readonly [K in keyof Omit<
ModalInstance,
keyof VNodeProps | keyof AllowedComponentProps
>]: ModalInstance[K];
},
"show" | "onUpdate:show"
>;
interface AnyFileChangeModal {
open: (param?: OpenParam) => Promise<void>;
}
/**
* 通过弹窗实例来获取弹窗组件内需要哪些props
*/
type AllProps = Omit<
OpenParam,
"onClose" | "onCancel" | "onConfirm" | "onUpdate:show"
> & { show: boolean };
const anyModalKey: InjectionKey<AnyFileChangeModal> = Symbol("ModalKey");
export function provideTestModal() {
const allProps: AllProps = reactive({
show: false,
} as AllProps);
const closeCallback = ref();
const cancelCallback = ref();
const confirmCallback = ref();
const handleUpdateShow = (value: boolean) => {
allProps.show = value;
};
/**
* @param param 通过函数来更新props
*/
function updateAllProps(param: OpenParam) {
const excludeKey = ["show", "onClose", "onConfirm", "onCancel"];
for (const [key, value] of Object.entries(param)) {
if (!excludeKey.includes(key)) {
allProps[key] = value;
}
}
}
function clearAllProps() {
for (const [key] of Object.entries(allProps)) {
allProps[key] = undefined;
}
}
async function open(param: OpenParam) {
clearAllProps();
updateAllProps(param);
allProps.show = true;
param.onClose && (closeCallback.value = param.onClose);
param.onConfirm && (confirmCallback.value = param.onConfirm);
param.onCancel && (cancelCallback.value = param.onCancel);
}
provide(anyModalKey, { open });
return {
allProps,
closeCallback,
confirmCallback,
cancelCallback,
handleUpdateShow,
};
}
export function injectTestModal() {
return inject(anyModalKey)
}
Ï
步骤3(❌):提供Provider
在这个文件里,我将TestModal放在了根页面,然后将hook返回的props和event绑定给TestModal
// ModalProvider.vue
<template>
<slot />
<TestModal
v-bind="allTestModalProps"
@update:show="handleTestModalUpdateShow"
@close="closeTestModalCallback"
@confirm="confirmTestModalCallback"
@cancel="cancelTestModalCallback"
/>
<!-- 新增Modal -->
</template>
<script setup lang="ts">
import TestModal from "./test-modal/TestModal.vue";
import { provideTestModal } from "./test-modal/use-test-modal";
/** 新增import */
const {
allProps: allTestModalProps,
handleUpdateShow: handleTestModalUpdateShow,
closeCallback: closeTestModalCallback,
confirmCallback: confirmTestModalCallback,
cancelCallback: cancelTestModalCallback,
} = provideTestModal();
/** 新增provide */
</script>
步骤4(❌):通过函数打开弹窗
<template>
<n-button @click="open">open</n-button>
</template>
<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>
到这里也就结束了。如果这样写一个弹窗,确实可以达到函数式的打开弹窗,但是他实在是太繁琐了,要写这么一堆东西,不如直接修改弹窗的show来的快。如果看过上面的use-test-modal.ts
的话,会发现里面有AllProps,这是为了减少方便使用脚本工具来写通用化的代码,可以大大减少人工的重复代码编写,接下来我们使用一个工具来减少这些繁琐的操作。
步骤1(✅):初始化Provider
通过使用工具生成根页面ModalProvder组件。
具体步骤:在你想放置当前业务版本的弹窗文件夹路径下使用终端执行脚本,选择initProvider
步骤2(✅):生成弹窗模板
通过继续使用脚本工具,生成弹窗组件以及hook文件。
具体步骤:在你想放该弹窗的文件夹路径下使用终端,使用脚本工具,选择genModal,然后跟着指令操作就行。比如例子中,我们生产名为test的弹窗,然后告诉脚本我们的ModalProvider组件的绝对路径,脚本帮我生产test-modal
的文件夹,里面放着testModal.vue
和use-test-modal.ts
,这里的use-test-modal.ts
文件已经是成品了,不需要你去修改,ModalProvider.vue
也不需要你去修改,里面的路径关系也帮你处理好了。
步骤3(✅):修改弹窗内容
上一步操作中,脚本帮我写好了TestModal.vue组件,我们可以在他的基础上完善我们的业务需求。
步骤4(✅):调用弹窗
我们找到生成的use-test-modal.ts,里面有一个injectXXXModal的方法,我们在哪个地方用到弹窗就引用执行他,返回的对象中有open方法,通过open方法去开启弹窗并且更新弹窗props。
Demo
脚本工具
仓库地址
这个是我写的脚本工具,用来减少一些重复代码的工作。用法可看readme
总结
本文先通过比较函数式的弹窗和直接传统编写弹窗的优劣,然后引出本文的主旨(如何编写函数式弹窗),再考虑我这一版函数式弹窗的问题,最后通过脚本工具来解决这一版函数式弹窗的劣势,完整的配合脚本工具,可以极大的加快我们的工作效率。
大家不要看这么写要好几个文件夹,实际使用的时候其实就是脚本生成代码后,我们只需要去修改弹窗主题的内容。有什么疑惑或者建议评论区聊。
来源:juejin.cn/post/7267418473401057321