Vue2/3 通用指令化弹窗组件封装
Vue2.7解决方案
可用 Vue.extend
扩展出组件实例并挂载到DOM
//useCommandComp.ts
import Vue, { getCurrentInstance, type ComponentInstance } from 'vue';
import { hasOwn } from '@vueuse/core';
export interface Options {
visible?: boolean;
onclose?: () => void;
appendTo?: HTMLElement | string;
[key: string]: unknown;
}
export interface CommandComponent {
(options: Options): ComponentInstance | null;
close: () => void;
}
const getAppendToElement = (props: Options): HTMLElement => {
let appendTo: HTMLElement | null = document.body;
if (props.appendTo) {
if (typeof props.appendTo === 'string') {
appendTo = document.querySelector(props.appendTo);
}
if (props.appendTo instanceof HTMLElement) {
appendTo = props.appendTo;
}
if (!(appendTo instanceof HTMLElement)) {
appendTo = document.body;
}
}
return appendTo;
};
export const useCommandComponent = (component: any): CommandComponent => {
let instance: ComponentInstance | null = null;
const parent = getCurrentInstance()?.proxy;
const container = document.createElement('div');
const close = () => {
if (instance) {
(instance as any).visible = false;
// @ts-ignore
}
};
const initInstance = (_component: any, props: Options) => {
instance = new (Vue.extend(_component))({
propsData: props
});
instance.$on('close', close);
// 跟随父组件销毁而销毁
parent?.$on('hook:beforeDestroy', () => {
instance?.$el.remove();
instance?.$destroy();
instance = null;
});
getAppendToElement(props).appendChild(container);
instance.$mount(container);
return instance;
};
const commandComponent = (options: Options) => {
if (!hasOwn(options, 'visible')) {
options.visible = true;
}
if (typeof options.onclose !== 'function') {
options.onclose = close;
} else {
const originOnClose = options.onclose;
options.onclose = () => {
originOnClose();
close();
};
}
if (!instance) {
initInstance(component, options);
} else {
for (const prop in options) {
if (hasOwn(options, prop) && hasOwn(instance?.$props || {}, prop)) {
// @ts-ignore
instance[prop] = options[prop];
}
}
}
for (const prop in options) {
if (hasOwn(options, prop) && !hasOwn(instance?.$props || {}, prop)) {
// @ts-ignore
instance[prop] = options[prop];
}
}
return instance;
};
// 可在外部关闭
commandComponent.close = close;
return commandComponent;
};
export default useCommandComponent;
Vue3解决方案
// useCommandComp.tsx
import { type AppContext, type Component, type ComponentPublicInstance, createVNode, getCurrentInstance, render, type VNode } from 'vue';
import { inBrowser, noop } from '@hgyn/utils';
export interface Options {
visible?: boolean;
onclose?: () => void;
appendTo?: HTMLElement | string;
[key: string]: unknown;
}
export interface CommandComponent {
(options: Options): VNode;
close: () => void;
}
const getAppendToElement = (props: Options): HTMLElement => {
let appendTo: HTMLElement | null = document.body;
if (props.appendTo) {
if (typeof props.appendTo === 'string') {
appendTo = document.querySelector<HTMLElement>(props.appendTo);
}
if (props.appendTo instanceof HTMLElement) {
appendTo = props.appendTo;
}
if (!(appendTo instanceof HTMLElement)) {
appendTo = document.body;
}
}
return appendTo;
};
const initInstance = <T extends Component>(Component: T, props: Options, container: HTMLElement, appContext: AppContext | null = null) => {
const vNode = createVNode(Component, props);
vNode.appContext = appContext;
render(vNode, container);
getAppendToElement(props).appendChild(container);
return vNode;
};
export const useCommandComponent = <T extends Component>(Component: T): CommandComponent | (() => void) => {
if (!inBrowser) return noop;
const appContext = getCurrentInstance()?.appContext;
// 补丁:Component中获取当前组件树的provides
if (appContext) {
const currentProvides = (getCurrentInstance() as any)?.provides;
Reflect.set(appContext, 'provides', { ...appContext.provides, ...currentProvides });
}
const container = document.createElement('div');
const close = () => {
render(null, container);
container.parentNode?.removeChild(container);
};
const CommandComponent = (options: Options): VNode => {
if (!Reflect.has(options, 'visible')) {
options.visible = true;
}
if (typeof options.onClose !== 'function') {
options.onClose = close;
} else {
const originOnClose = options.onClose;
options.onClose = () => {
originOnClose();
close();
};
}
const vNode = initInstance<T>(Component, options, container, appContext);
const vm = vNode.component?.proxy as ComponentPublicInstance<Options>;
for (const prop in options) {
if (Reflect.has(options, prop) && !Reflect.has(vm.$props, prop)) {
vm[prop as any] = options[prop];
}
}
return vNode;
};
CommandComponent.close = close;
return CommandComponent;
};
export default useCommandComponent;
命令式组件定制规范
本项目已封装对弹窗组件的命令式调用hook useCommandComp
。无论组件的实现方式是sfc还是tsx,只要遵循简单的约定即可完成命令式调用。
使用示例
<script setup lang="ts">
import Dialog from '@/common/dialog'
import useCommandComponent from '@/composables/useCommandComp';
const dialogCaller = useCommandComponent(Dialog)
dialogCaller({
content: '您将退出阅读,请确认是否已读?',
confirmText: '完成阅读',
cancelText: '继续阅读',
});
</script>
须遵循的约定
- 弹窗组件内部须提供名为
visible
的props
,用于控制组件的显隐。 - 弹窗组件关闭时须
emit
一个close
事件。 useCommandComponent
须在setup
函数内调用。
执行流程概述
- 父组件调用
useCommandComponent
hook 传入弹窗组件,于hook内部创建实例缓存变量,返回commandComponent
方法。 - 调用
commandComponent
方法,传入所需参数。 - 判断当前弹窗组件是否已实例化,如果已实例化,则更新
props
。否则实例化组件。 - 组件实例化,将入参作为
props
传入组件内部,监听弹窗close
关闭事件,监听父组件beforeDestroy
钩子 用于执行弹窗销毁。(这样只要上层组件有一个触发了beforeDestroy事件,后续组件就将递归销毁。所以在命令式组件中嵌套命令式组件也是可以的。) - 组件挂载