Vue2 Dialog弹窗函数式调用实践示例
目录
- 前言
- Vue2 的弹窗常用的使用方式
- 第一种:将弹窗写在上下文中
- 第二种:原型上注入全局方法
- 第三种:通过依赖注入的方式
- 合理的使用方式
- 功能实现
- 结语
前言
Dialog 对话框组件几乎是每个前端项目必不可少的组件,通常是在保留当前页面状态并屏蔽其他用户输入的情况下,与用户交互并承载相关操作。
在 BOM 的方法中,alert
、prompt
都是以前用来做类似功能的方法,但是这些浏览器内置方法会完全停止网页代码执行,对于贫乏的前端线程资源来说实在是过于僵硬。于是便产生了各种各样的 Js 弹窗。
今天就来谈谈在前端框架 Vue 2 版本中的弹窗组件的相关实现以及我个人认为的最佳实践。
Vue2 的弹窗常用的使用方式
对于 Vue2 的 UI 框架,坊间比较火的有 element-ui
antd-vue
vant-ui
等等,不过他们在弹窗的使用方法上几乎都是一致的。下面我们以 element-ui
中的组件为例讲讲这些使用方式的缺点。
第一种:将弹窗写在上下文中
这种方式不用多讲,使用起来肯定是最麻烦的一种,这意味着你每次想使用这个弹窗组件都要写一遍负责显隐的状态以及方法,尽量只在唯一场景里使用,不具备复用条件。
第二种:原型上注入全局方法
即是通过往 Vue.prototype
中注入弹窗的方法,使你可以在 vue 上下文中使用 this.$confirm
诸如此类的方法来使用他们的弹窗功能。
这种调用方式曾经几乎是所有 vue2 全局 api 的解决方法,一时间各种插件都有了各种各样的全局 api。例如 vuex 的 this.$store
、 EventBus 的 this.$bus
等等。
这种方式在使用上虽然非常方便,但也有如下一些缺点:
上下文污染,一个 vue 的全局 this
里面,什么东西都可以有,不论是全局方法、还是全局变量,都可以放在 Vue.prototype
里面,例如前一个例子,如果复杂的弹窗需要在各种其他前端文件内打开,大概率也是用这种方式将弹窗打开和关闭方法都注入到全局 this
中使用。
类型丢失,无论你是全局状态管理还是事件总线,在使用的时候都是各种黑盒,全局方法来自于什么插件、需要传什么参数、返回什么东西,全然无感,只能想办法寻找蛛丝马迹去溯源。
只能在 vue 上下文中使用,在 vue 文件外就无法使用了。就好比 React 无法在 React 上下文之外 setState
一样难受。
第三种:通过依赖注入的方式
这种方式和第二种几乎一样,通过顶层组件 provider
出对应的函数方法,之后在子组件中 inject
进来。 一样存在着 provider 的上下文污染,也丢失了类型,并且依旧只能在 vue 上下文中使用,灵活性一样差。
合理的使用方式
除了上述所说的问题之外
综合以上的缺点,我认为一个弹窗最佳的使用方式在 Vue2 中使用方式是这样:
<template> <button @click="open">打开弹窗</button> </template> <script> import openDialog from 'my-dialog' export default { methods: { open() { openDialog() .then(() => {}) .catch(() => {}) .finally(() => {}) } } } </script>
也可以在其他文件中,例如:
// api.ts import openDialog from 'my-dialog' const getUserInfo = () => { return fetch('/xxx/api').then(() => { openDialog('success') }) }
功能实现
废话不多说,直接上核心代码
// dialog.ts import Vue, { ComponentInstance } from "vue"; import ConfirmDialog from "./confirm-dialog.vue"; export let index = 1000; export const cache = new Set<string>(); export function openDialog(component: ComponentInstance) { const div = document.createElement("div"); const el = document.createElement("div"); const id = 'dialog-' + Math.random(); div.appendChild(el); document.body.appendChild(div); const ComponentConstructor = Vue.extend(component); return (propsData = {}, parent = undefined) => { let instance = new ComponentConstructor({ propsData, parent, }).$mount(el); const destroyDialog = () => { if (cache.has(id)) return; if (instance && div.parentNode) { cache.add(id); (instance as any).visible = false; // 延时是为了在关闭动画执行完毕后卸载组件 setTimeout(() => { cache.delete(id); instance.$destroy(); // @ts-ignore instance = null; div.parentNode && div.parentNode.removeChild(div); }, 1000); } }; // visible控制 if ((instance as any).visible !== undefined) { // 支持sync/v-model instance.$watch("visible", (val) => { !val && destroyDialog(); }); Vue.nextTick(() => ((instance as any).visible = true)); } return new Promise((resolve, reject) => { // emit 一个 done 事件关闭 instance.$once("done", (data: any) => { destroyDialog(); resolve(data); }); // emit 一个 cancel 事件取消 instance.$once("cancel", (data: any) => { destroyDialog(); reject(data); }); }); }; } export function confirmDialog(content: string, editable?: boolean) { return openDialog(ConfirmDialog as any)({ content, editable }); }
使用方式和结果可以通过下面这个例子进行查看
在例子中,我们可以将任意 vue 组件包装进 openDialog
Api 中,只需要在组件的 data 里写入一个 visible
属性,而后续在组件方法中调用 this.$emits('done')
或者 this.$emits('cancel')
就可以对应 openDialog
方法返回 Promise 的 then
回调和 catch
回调。
可以自定义传参、自定义内容、自定义事件,自定义返回,灵活性直接拉满。
而且,方法不限于任何上下文,在任何文件内都可以使用,实现了真正的函数式调用 Vue2 的弹窗组件。
但是这个方法确实也有那么一点点不方便的地方,如果有掘友看得出来,可以在评论下方说说。
结语
这篇文章其实没多少东西,甚至代码都是我一年多前就写的,在受到 React hooks 的启发后,我就觉得函数式编程非常的爽,就尝试将项目中的全局变量都剥离 vue 上下文,包括 dialog
、message
组件,之后摒弃 vuex 和 pinia 这种强绑上下文的状态管理库,改用 Vue.observable
,就可以很方便的将业务与 UI 完全抽离。所有变量和方法都可以通过 import 追溯,更重要的是这种模式更契合 typescript
,类型提示也很轻易跟上来了。
以上就是本篇文章的所有内容了,后续我会封装 v2 和 v3 的函数式弹窗组件,并发布到 npm 上,到时候再更新链接到文章里,更多关于Vue2 Dialog弹窗函数式调用的资料请关注我们其它相关文章!