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

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

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

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>
相关推荐
雾岛LYC听风2 分钟前
3. 轴指令(omron 机器自动化控制器)——>MC_GearInPos
前端·数据库·自动化
weixin_443566982 分钟前
39-Ajax工作原理
前端·ajax
WebInfra10 分钟前
Rspack 1.3 发布:内存大幅优化,生态加速发展
前端·javascript·github
努力的搬砖人.12 分钟前
axios使用
vue.js
ak啊13 分钟前
Webpack 构建阶段:模块解析流程
前端·webpack·源码
学习OK呀23 分钟前
后端上手学习react基础知识
前端
星火飞码iFlyCode24 分钟前
大模型提效之服务端日常开发
前端
zoahxmy092925 分钟前
Canvas 实现单指拖动、双指拖动和双指缩放
前端·javascript
花花鱼25 分钟前
vue3 动态组件 实例的说明,及相关的代码的优化
前端·javascript·vue.js
Riesenzahn27 分钟前
CSS的伪类和伪对象有什么不同?
前端·javascript