前端操作窗口返回结果的多种方式

本文将介绍业务常见的弹窗组件返回结果的多种方式。

以下为展示代码所使用的技术栈:

json 复制代码
"dependencies": {
  "element-plus": "^2.9.7",
  "mitt": "^3.0.1",
  "vue": "^3.5.13"
},
"devDependencies": {
  "@vitejs/plugin-vue": "^5.2.1",
  "@vue/tsconfig": "^0.7.0",
  "typescript": "~5.7.2",
  "vite": "^6.2.0",
  "vue-tsc": "^2.2.4"
}

每种方式的弹窗组件都命名为Child.vue,以下将称为子组件,使用的父组件都命名为parent.vue

emits事件

这应该是我初学前端时使用最频繁的一种方法。也是vue的标准做法。

子组件内使用 emits 触发事件。

vue 复制代码
<template>
	<el-dialog
		v-model="visible"
		title="操作窗口"
		append-to-body
		:close-on-click-modal="false"
		@close="handleClose"
	>
        <div>...</div>
        
		<template #footer>
			<el-button @click="handleClose">
				取消
			</el-button>
			<el-button  type="primary" @click="handleConfirm">
				确认
			</el-button>
		</template>
    </el-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const visible = ref(false)

const emits = defineEmits<{
    close: [],
    confirm: [string]
}>()

/**
 * 关闭
 */
function handleClose() {
    visible.value = false
    emits('close')
}

/**
 * 确认
 */
function handleConfirm() {
    visible.value = false
    emits('confirm', 'confirm result')
}

defineExpose({
    open: () => {
        visible.value = true
    }
})
</script>

父组件调用子组件defineExpose出来的open方法打开子组件弹窗,然后通过监听confirm事件接收弹窗结果

vue 复制代码
<template>
    <div>
        <el-button  type="primary" @click="handleOpenDialog">
            打开操作弹窗
        </el-button>
        <Child ref="ChildRef" @close="handleDialogClose" @confirm="handleDialogConfirm" />
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const ChildRef = ref<InstanceType<typeof Child>>()

/**
 * 打开弹窗
 */
function handleOpenDialog() {
    ChildRef.value?.open()
}

/**
 * 弹窗关闭回调事件
 */
function handleDialogClose() {
    console.log('弹窗关闭回调事件')
}

/**
 * 弹窗确认回调事件
 */
function handleDialogConfirm(result: string) {
    console.log('弹窗确认回调事件')
    console.log(result)
}
</script>

callback回调函数

子组件会在defineExposeopen方法内多接收两个回调函数。然后用包裹函数进行一层封装,在关闭或确认时执行对应的回调函数。

vue 复制代码
<template>
	<el-dialog
		v-model="visible"
		title="操作窗口"
		append-to-body
		:close-on-click-modal="false"
		@close="handleClose"
	>
        <div>...</div>
        
		<template #footer>
			<el-button @click="handleClose">
				取消
			</el-button>
			<el-button  type="primary" @click="handleConfirm">
				确认
			</el-button>
		</template>
    </el-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const visible = ref(false)

type Callbacks = {
    confirm?: (result: string) => void
    close?: () => void
}
const callbacks = ref<Callbacks>({})

/**
 * 关闭
 */
function handleClose() {
    callbacks.value.close?.()
}

/**
 * 确认
 */
function handleConfirm() {
    callbacks.value.confirm?.('confirm result')
}

/**
 * 包裹回调,执行完后清空回调
 */
function wrapCallback<T extends ((...args: any[]) => void) | undefined>(
    callback: T
): T {
    if (!callback) return undefined as T

    return ((...args: Parameters<Extract<T, Function>>) => {
        callback(...args)
        callbacks.value = {}
        visible.value = false
    }) as T
}

defineExpose({
    open: (
		confirmCallback?: Callbacks['confirm'],
		closeCallback?: Callbacks['close']
	) => {
    visible.value = true
    callbacks.value = {
            confirm: wrapCallback(confirmCallback),
            close: wrapCallback(closeCallback),
    }
  }
})
</script>

父组件需要在调用open方法时将回调函数传入子组件,而不是用监听事件了

vue 复制代码
<template>
    <div>
        <el-button  type="primary" @click="handleOpenDialog">
            打开操作弹窗
        </el-button>
        <Child ref="ChildRef" />
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const ChildRef = ref<InstanceType<typeof Child>>()

/**
 * 打开弹窗
 */
function handleOpenDialog() {
    ChildRef.value?.open(handleDialogConfirm, handleDialogClose)
}

/**
 * 弹窗关闭回调事件
 */
function handleDialogClose() {
    console.log('弹窗关闭回调')
}

/**
 * 弹窗确认回调事件
 */
function handleDialogConfirm(result: string) {
    console.log('弹窗确认回调')
    console.log(result)
}
</script>

Promise

子组件在open方法调用时返回一个Promise实例,并且将resolvereject保存到promiseController变量,再用controlPromise函数控制执行。

vue 复制代码
<template>
	<el-dialog
		v-model="visible"
		title="操作窗口"
		append-to-body
		:close-on-click-modal="false"
		@close="handleClose"
	>
        <div>...</div>
        
		<template #footer>
			<el-button @click="handleClose">
				取消
			</el-button>
			<el-button  type="primary" @click="handleConfirm">
				确认
			</el-button>
		</template>
    </el-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const visible = ref(false)

type PromiseController = { 
    resolve?: (value?: string | PromiseLike<string>) => void,
    reject?: (reason?: any) => void,
}
// promise 控制器
let promiseController: PromiseController = {}

// 控制 promise 状态
function controlPromise(key: keyof PromiseController, value?: string) {
    promiseController[key]?.(value)
    promiseController = {}
    visible.value = false
}

/**
 * 关闭
 */
function handleClose() {
    controlPromise('reject')
}

/**
 * 确认
 */
function handleConfirm() {
    controlPromise('resolve', 'confirm result')
}

defineExpose({
    open: () => {
  		return new Promise<string | undefined>((resolve, reject) => {
  			visible.value = true
  			promiseController = { resolve, reject }
  		})
    }
})
</script>

父组件只需要在调用子组件open方法时进行await,就能在弹窗确认时拿到结果并且进行操作。如果需要在关闭时进行操作,就需要使用try...catch包裹进行捕获。

或者调用.then方法注册成功回调和失败回调。

vue 复制代码
<template>
    <div>
        <el-button  type="primary" @click="handleOpenDialog">
            打开操作弹窗
        </el-button>
        <Child ref="ChildRef" />
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const ChildRef = ref<InstanceType<typeof Child>>()

/**
 * 打开弹窗
 */
async function handleOpenDialog() {
    try {
        const result = await ChildRef.value?.open()

        console.log('弹窗确认回调事件')
        console.log(result)
    } catch (e) {
        console.error(e)
        console.log('弹窗关闭或错误回调事件')
    }
}
</script>

可以看到这个方法使用起来最简便,只是子组件需要维护promiseControllercontrolPromise,代码并不雅观。

如果子组件defineExpose导出多个操作函数,比如addedit有时可能在同一个弹窗内进行操作,那么需要维护的promiseControllercontrolPromise可能就会随之增多。

所以我们需要将这些操作进行封装。

usePromisify

usePromisify会将业务操作函数包装成Promise函数,通过返回的controlPromise函数操作结果。

ts 复制代码
type PromiseController<R> = {
    resolve?: (value: R) => void;
    reject?: (reason?: any) => void;
};

type ControlPromise<R> = {
    (type: "resolve", res: R): void;
    (type: "reject", reason?: any): void;
}

function usePromisify<R, T extends (...args: any[]) => void>(
    callback: T
): [
    ControlPromise<R>,
    (...args: Parameters<T>) => Promise<R>
] {
    let promiseController: PromiseController<R> = {};

    // 控制 Promise 状态的核心函数
    const controlPromise: ControlPromise<R> = (type: 'resolve' | 'reject', res: R) => {
        // 获取对应的 resolve/reject 方法
        const handler = promiseController[type];
        if (handler) {
            handler(res);        // 执行状态变更
            promiseController = {}; // 清空控制器避免重复调用
        }
    };

    // 包装后的 Promise 函数
    const wrappedFunction = (...args: Parameters<T>): Promise<R> => {
        return new Promise<R>((resolve, reject) => {
            // 暂存 resolve/reject 方法供外部调用
            promiseController.resolve = resolve;
            promiseController.reject = reject;
            // 触发原始回调函数
            callback(...args);
        });
    };

    return [controlPromise, wrappedFunction];
}

export default usePromisify;

如果你的项目不用ts,那么这里也有加了jsdoc的js版。

js 复制代码
/**
 * @template R 返回值类型
 * @template {Function} T 函数类型,接受任意参数且无返回值
 * @param {T} callback 要包装的回调函数
 * @returns {[
 *   (type: 'resolve' | 'reject', res: R) => void, // 控制 Promise 的操作函数
 *   (...args: Parameters<T>) => Promise<R>       // 包装后的函数,返回 Promise
 * ]} 返回操作函数和包装后的 Promise 函数
 */
function usePromisify(callback) {
	/** @type {Partial<Record<'resolve' | 'reject', (value: R) => void>>} */
	let promiseController = {}

	/**
	 * 控制 Promise 的状态
	 * @param {'resolve' | 'reject'} type 操作类型
	 * @param {R} res Promise 的结果
	 */
	const controlPromise = (type, res) => {
		if (promiseController[type] && typeof promiseController[type] === 'function') {
			[type](res)
			promiseController = {}
		}
	}

	/**
	 * 包装后的函数
	 * @param {...Parameters<T>} args 回调函数的参数
	 * @returns {Promise<R>} 包装后的 Promise
	 */
	const wrappedFunction = (...args) => {
		return new Promise((resolve, reject) => {
			promiseController.resolve = resolve
			promiseController.reject = reject
			callback(...args)
		})
	}

	return [controlPromise, wrappedFunction]
}

export default usePromisify

使用usePromisify之后子组件的代码少了很多,可以更加专注业务代码了。

vue 复制代码
<template>
	<el-dialog
		v-model="visible"
		title="操作窗口"
		append-to-body
		:close-on-click-modal="false"
		@close="handleClose"
	>
        <div>...</div>
        
		<template #footer>
			<el-button @click="handleClose">
				取消
			</el-button>
			<el-button  type="primary" @click="handleConfirm">
				确认
			</el-button>
		</template>
    </el-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import usePromisify from './usePromisify'

const visible = ref(false)

const [openControl, open] = usePromisify<string, () => void>(() => {
	visible.value = true
})

/**
 * 关闭
 */
function handleClose() {
    openControl('reject')
}

/**
 * 确认
 */
function handleConfirm() {
    openControl('resolve', 'confirm result')
}

defineExpose({
    open: open
})
</script>

父组件的使用没有变化,还是用await或者.then就可以了。

vue 复制代码
<template>
    <div>
        <el-button  type="primary" @click="handleOpenDialog">
            打开操作弹窗
        </el-button>
        <Child ref="ChildRef" />
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const ChildRef = ref<InstanceType<typeof Child>>()

/**
 * 打开弹窗
 */
async function handleOpenDialog() {
    try {
        const result = await ChildRef.value?.open()

        console.log('弹窗确认回调事件')
        console.log(result)
    } catch (e) {
        console.error(e)
        console.log('弹窗关闭或错误回调事件')
    }
}
</script>

这个方法目前是我使用下来最简便的方法,推荐大家使用该方法。

全局发布订阅

还有一种特殊情况,弹窗组件和需要监听弹窗结果的组件不一定是父子关系。或者有多处地方需要监听一个弹窗的结果。

这时候就需要有一个eventbus事件中心,通过发布订阅模式来串联多方。

先用mitt创建一个弹窗的事件中心。注意CLOSECONFIRM是字符串常量,不是类型,用于区分组件触发的事件类型。

ts 复制代码
import mitt from 'mitt'

export const CLOSE = 'close'
export const CONFIRM = 'confirm'

export type ModalResult = typeof CLOSE | typeof CONFIRM

export type ModalEventOptions = { type: ModalResult, data?: any }


export const emitter =  mitt<{
    [k: string]: ModalEventOptions
}>()

弹窗组件用defineOptions.customOptions向外开放一个mittKey事件键,这样需要监听的组件可以通过导入这个组件的静态属性用于监听了。

如果担心事件键重复,可以使用symbol类型。

需要注意一次窗口不要emit多次事件。

vue 复制代码
<template>
	<el-dialog
		v-model="visible"
		title="操作窗口"
		append-to-body
		:close-on-click-modal="false"
		@close="handleClose"
	>
        <div>...</div>
        
		<template #footer>
			<el-button @click="handleClose">
				取消
			</el-button>
			<el-button  type="primary" @click="handleConfirm">
				确认
			</el-button>
		</template>
    </el-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { emitter, CLOSE, CONFIRM } from '@/eventbus/modal'

const visible = ref(false)

const mittKey = 'MyMittKey'
defineOptions({
	customOptions: {
		mittKey: mittKey
	}
})

/**
 * 关闭
 */
function handleClose() {
	// 避免重复调用关闭事件
    if (visible.value) emitter.emit(mittKey, { type: CLOSE })
  	visible.value = false
}

/**
 * 确认
 */
function handleConfirm() {
    emitter.emit(mittKey, { type: CONFIRM, data: 'confirm result' })
  	visible.value = false
}

defineExpose({
    open: () => {
  		visible.value = true
  	}
})
</script>

需要监听的内容组件如下,通过取弹窗组件的customOptions.mittKey来精准监听该组件的回调。

vue 复制代码
<template>
    <div>
        content
    </div>
</template>

<script setup lang="ts">
import { onUnmounted, onMounted } from 'vue'
import OperateModal from '@/components/OperateModal.vue'
import { emitter, CLOSE, CONFIRM, type ModalEventOptions } from '@/eventbus/modal'

/**
 * 弹窗关闭回调事件
 */
function handleDialogClose() {
    console.log('弹窗关闭回调事件')
}

/**
 * 弹窗确认回调事件
 */
function handleDialogConfirm(result: string) {
    console.log('弹窗确认回调事件')
    console.log(result)
}

function handleEmitter(e: ModalEventOptions) {
    // 执行对应操作函数
    if (e.type === CLOSE) {
        handleDialogClose()
    } else if (e.type === CONFIRM) {
        handleDialogConfirm(e.data)
    }
}

onMounted(() => {
    // 监听弹窗组件的事件
    emitter.on(
        OperateModal.customOptions.mittKey,
        handleEmitter
    )
})

// 组件销毁时,移除事件监听
onUnmounted(() => {
    emitter.off(
        OperateModal.customOptions.mittKey,
        handleEmitter
    )
})
</script>

这样就可以不用关心两个组件的层级问题了,多个组件同时监听弹窗结果也没问题。

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import content from '@/views/content.vue'
import OperateModal from '@/components/OperateModal.vue'


const OperateModalRef = ref<InstanceType<typeof OperateModal>>()
/**
 * 打开弹窗
 */
function handleOpenDialog() {
  OperateModalRef.value?.open()
}
</script>

<template>
  <div>
    <el-button  type="primary" @click="handleOpenDialog">
        打开操作弹窗
    </el-button>

    <content />
    <OperateModal ref="OperateModalRef" />
  </div>
</template>
相关推荐
Boilermaker199213 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子25 分钟前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上102440 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan1 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构