普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月4日首页

Vue3 选择弹窗工厂函数:高效构建可复用数据选择组件

作者 VOLUN
2025年11月4日 10:40

在 Vue 项目开发中,数据选择弹窗是高频出现的交互组件,比如用户选择、角色选择、部门选择等场景。如果每个选择场景都重复开发弹窗逻辑,不仅会导致代码冗余,还会增加维护成本。本文将深入解析一个基于 Vue 3 和 Arco Design 的选择弹窗工厂函数,带你理解其设计思想、实现细节与应用方式,助力提升组件复用效率。

完整源码展示

import type { Component } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { h, ref } from 'vue'

interface CreateSelectDialogParams {
  title: string
  component: Component
  componentProps?: Record<string, any>
  tip?: string
}

/**
 * 选择弹窗配置选项接口
 * @template T 选中数据的类型
 */
interface SelectDialogOptions<T> {
  /** 弹窗标题(会覆盖创建参数中的title) */
  title?: string
  /** 是否允许多选 */
  multiple?: boolean
  /** 查询参数,通常用于初始化数据 */
  queryParams?: Record<string, any>
  /** 传递给组件的额外属性 */
  componentProps?: Record<string, any>
  /** 点击确定按钮后的回调函数 */
  onOk?: (data: T) => void
  /** 点击确定前的校验函数,返回Promise<boolean>决定是否允许确定 */
  onBeforeOk?: (data: T) => Promise<boolean>
}

/**
 * 创建一个选择类型的弹窗工厂函数
 *
 * 该函数返回一个创建特定类型选择弹窗的方法,适用于需要从列表中选择数据的场景。
 * 内部使用Vue的createVNode动态渲染组件,并通过ref获取组件实例的方法。
 *
 * @template T 选中数据的类型
 * @param {CreateSelectDialogParams} params 创建弹窗所需的基本参数
 * @returns {(options: SelectDialogOptions<T>) => void} 可配置的弹窗创建函数
 *
 * @example
 * // 创建一个用户选择弹窗
 * const selectUserDialog = createSelectDialog({
 *   title: '选择用户',
 *   component: UserSelectComponent,
 *   tip: '请至少选择一个用户'
 * })
 *
 * // 打开弹窗并处理选择结果
 * selectUserDialog({
 *   multiple: true,
 *   queryParams: { status: 'active' },
 *   onOk: (selectedUsers) => {
 *     console.log('已选择用户:', selectedUsers)
 *   }
 * })
 */
export const createSelectDialog = <T = any>(params: CreateSelectDialogParams) => {
  return (options: SelectDialogOptions<T>) => {
    const TableRef = ref<any>()
    Modal.open({
      // 优先使用options中的title,否则使用params中的title
      title: options.title || params.title,
      // 动态渲染传入的组件,设置ref引用并合并属性
      content: () => h(params.component, {
        ref: (e: any) => (TableRef.value = e),
        multiple: options.multiple,
        queryParams: options.queryParams,
        ...params.componentProps,
        ...options.componentProps
      }),
      // 设置弹窗宽度自适应
      width: 'calc(100% - 20px)',
      modalStyle: { maxWidth: '1000px' },
      bodyStyle: { overflow: 'hidden', height: '500px', padding: 0 },
      onBeforeOk: async () => {
        // 检查组件是否暴露了必要的getSelectedData方法
        if (!TableRef.value?.getSelectedData) {
          Message.warning('组件必须暴露getSelectedData方法')
          return false
        }

        // 获取选中的数据
        const data = TableRef.value?.getSelectedData?.() || []

        // 验证是否选择了数据
        if (!data.length) {
          Message.warning(params.tip || '请选择数据')
          return false
        }

        // 如果提供了前置校验函数,则调用并根据结果决定是否继续
        if (options?.onBeforeOk) {
          return await options.onBeforeOk(data)
        }

        // 调用确定回调函数,传递选中的数据
        options.onOk?.(data)
        return true
      }
    })
  }
}

一、函数设计背景与核心目标

在中后台系统中,数据选择弹窗通常具备以下共性需求:

  1. 统一的弹窗容器(标题、确认 / 取消按钮、尺寸控制);
  1. 动态嵌入不同的选择组件(如用户列表、角色表格);
  1. 支持单选 / 多选切换、初始化查询参数传递;
  1. 选中数据校验、前置拦截与结果回调;
  1. 组件间通信与方法调用(如获取选中数据)。

传统开发方式中,这些需求往往通过 “复制粘贴 + 修改” 实现,导致代码重复率高、

逻辑分散而本文解析的createSelectDialog工厂函数,正是为解决这些痛点而生,其核心目标是:封装共性逻辑,暴露个性化配置,实现 “一次定义,多场景复用”

二、核心架构与类型定义解析

在理解函数实现前,我们先梳理其类型接口与整体架构,这是保障代码健壮性和可维护性的基础。

1. 关键接口定义

函数通过 TypeScript 接口明确了参数与配置的结构,避免类型混乱,提升开发体验。

(1)创建弹窗的基础参数接口:CreateSelectDialogParams

该接口定义了创建特定类型选择弹窗的 “固定属性”,是工厂函数的 “原料”:

interface CreateSelectDialogParams {
  title: string; // 弹窗默认标题
  component: Component; // 嵌入弹窗的选择组件(如用户列表)
  componentProps?: Record<string, any>; // 传递给选择组件的默认属性
  tip?: string; // 未选择数据时的提示文本
}

  • component:核心属性,指定弹窗内渲染的选择组件(需暴露getSelectedData方法,用于获取选中数据);
  • componentProps:为选择组件设置默认属性,如表格的border、rowKey等通用配置。

(2)弹窗配置选项接口:SelectDialogOptions

该接口定义了每次打开弹窗时的 “动态配置”,支持个性化调整,泛型T用于指定选中数据的类型,提升类型安全性:

interface SelectDialogOptions<T> {
  title?: string; // 覆盖默认标题
  multiple?: boolean; // 单选/多选切换
  queryParams?: Record<string, any>; // 初始化查询参数(如筛选“活跃用户”)
  componentProps?: Record<string, any>; // 覆盖默认组件属性
  onOk?: (data: T) => void; // 确定按钮回调(返回选中数据)
  onBeforeOk?: (data: T) => Promise<boolean>; // 确定前校验(如“最多选择10个用户”)
}
  • 泛型T:解决不同选择场景下数据类型不一致的问题(如用户类型User、角色类型Role);
  • onBeforeOk:支持异步校验(如调用接口检查选中数据合法性),返回Promise决定是否允许关闭弹窗。

2. 工厂函数整体架构

createSelectDialog是一个高阶函数,其核心逻辑分为两步:

  1. 接收CreateSelectDialogParams参数,封装弹窗的 “固定逻辑”(如组件渲染、基础样式);
  1. 返回一个新函数,该函数接收SelectDialogOptions参数,处理弹窗的 “动态配置”(如单选 / 多选、回调函数),并打开弹窗。

这种设计的优势在于:将 “固定共性” 与 “动态个性” 分离,一次创建可多次调用,且每次调用可灵活配置。

三、核心功能实现细节

接下来,我们深入函数内部,解析关键功能的实现逻辑,理解其如何解决数据选择弹窗的核心痛点。

1. 动态组件渲染与 Ref 引用

弹窗内容通过 Vue 的h函数(创建虚拟 DOM)动态渲染传入的component,并通过ref获取组件实例,实现方法调用:

content: () => h(params.component, {
  ref: (e: any) => (TableRef.value = e), // 绑定组件Ref
  multiple: options.multiple, // 传递单选/多选配置
  queryParams: options.queryParams, // 传递查询参数
  ...params.componentProps, // 合并默认组件属性
  ...options.componentProps // 合并动态组件属性(优先级更高)
})
  • 属性合并规则:options.componentProps > params.componentProps,支持动态覆盖默认属性;
  • Ref 引用核心作用:通过TableRef.value获取选择组件实例,调用其暴露的getSelectedData方法,这是 “获取选中数据” 的关键。

2. 选中数据校验与前置拦截

onBeforeOk是弹窗的 “核心校验逻辑”,负责确保选中数据合法,并支持自定义拦截,流程如下:

onBeforeOk: async () => {
  // 1. 检查组件是否暴露getSelectedData方法
  if (!TableRef.value?.getSelectedData) {
    Message.warning('组件必须暴露getSelectedData方法');
    return false;
  }
  // 2. 获取选中数据
  const data = TableRef.value?.getSelectedData?.() || [];
  // 3. 校验是否选择数据
  if (!data.length) {
    Message.warning(params.tip || '请选择数据');
    return false;
  }
  // 4. 自定义前置校验(如异步接口校验)
  if (options?.onBeforeOk) {
    return await options.onBeforeOk(data);
  }
  // 5. 触发确定回调,返回选中数据
  options.onOk?.(data);
  return true;
}
  • 强制接口约束:要求嵌入的选择组件必须暴露getSelectedData方法,否则弹窗无法正常工作,这是 “组件间通信” 的约定;
  • 异步校验支持:onBeforeOk返回Promise,支持调用接口进行校验(如 “检查选中用户是否已被占用”);
  • 友好提示:通过Message组件提供明确的错误提示,提升用户体验。

3. 弹窗样式自适应

为适配不同屏幕尺寸,函数对弹窗样式做了精细化控制:

width: 'calc(100% - 20px)', // 宽度自适应(左右各留10px边距)
modalStyle: { maxWidth: '1000px' }, // 最大宽度限制(避免大屏下过宽)
bodyStyle: { overflow: 'hidden', height: '500px', padding: 0 } // 固定高度+隐藏滚动
  • 自适应宽度:在小屏设备(如平板)上占满屏幕,大屏设备上限制最大宽度;
  • 固定 body 高度:避免选择组件(如长表格)导致弹窗过高,同时通过overflow: hidden配合组件内部滚动,保证弹窗整体美观。

四、使用示例与场景拓展

理解了函数设计后,我们通过实际示例,看如何在项目中应用该工厂函数。

1. 基础使用:创建用户选择弹窗

假设我们有一个UserSelectComponent(用户选择组件,已暴露getSelectedData方法),通过以下步骤创建用户选择弹窗:

// 1. 导入依赖与组件
import { createSelectDialog } from './createSelectDialog';
import UserSelectComponent from './UserSelectComponent.vue';
// 2. 创建用户选择弹窗函数(固定配置)
const selectUserDialog = createSelectDialog({
  title: '选择用户', // 默认标题
  component: UserSelectComponent, // 嵌入的用户选择组件
  tip: '请至少选择一个用户', // 未选择时的提示
  componentProps: { // 传递给用户组件的默认属性
    border: false,
    showSearch: true
  }
});
// 3. 在业务组件中调用(动态配置)
const handleSelectUser = () => {
  selectUserDialog({
    multiple: true, // 允许多选
    queryParams: { status: 'active' }, // 初始化查询“活跃用户”
    onBeforeOk: async (selectedUsers) => {
      // 自定义校验:最多选择5个用户
      if (selectedUsers.length > 5) {
        Message.warning('最多只能选择5个用户');
        return false;
      }
      // 异步校验:检查选中用户是否已关联角色
      const res = await checkUserRole(selectedUsers.map(u => u.id));
      return res.data.isValid;
    },
    onOk: (selectedUsers) => {
      // 确定后的逻辑:如渲染选中用户列表
      console.log('已选择用户:', selectedUsers);
      // 业务逻辑:更新页面状态、提交表单等
    }
  });
};

2. 场景拓展:支持不同类型的选择弹窗

除了用户选择,该工厂函数还可用于角色选择、部门选择等场景,只需替换component参数即可:

// 角色选择弹窗
const selectRoleDialog = createSelectDialog({
  title: '选择角色',
  component: RoleSelectComponent,
  tip: '请选择角色'
});
// 部门选择弹窗
const selectDeptDialog = createSelectDialog({
  title: '选择部门',
  component: DeptSelectComponent,
  tip: '请选择部门'
});

通过这种方式,我们无需重复开发弹窗逻辑,只需关注 “选择组件本身”,极大提升开发效率。

五、优势总结与优化方向

1. 核心优势

  • 高复用性:一次定义工厂函数,支持多类型选择弹窗(用户、角色、部门等);
  • 类型安全:通过 TypeScript 泛型与接口,明确参数类型与返回值,减少运行时错误;
  • 灵活配置:支持动态覆盖标题、单选 / 多选、查询参数等,适配不同业务场景;
  • 约定式通信:通过 “组件暴露getSelectedData方法” 的约定,简化组件间通信逻辑。

2. 优化方向

  • 支持自定义弹窗样式:当前弹窗宽度、高度为固定配置,可新增style参数,允许动态调整;
  • 添加加载状态:在onBeforeOk异步校验时,添加弹窗加载状态(如禁用确认按钮),避免重复点击;
  • 支持弹窗销毁回调:新增onClose参数,处理弹窗关闭后的逻辑(如清理组件缓存、重置状态);
  • 类型强化:将TableRef的类型从any改为泛型,明确组件实例的方法与属性,提升类型安全性。

六、总结

createSelectDialog工厂函数通过 “封装共性、暴露个性” 的设计思想,解决了 Vue 项目中数据选择弹窗的复用问题。其核心在于:

  1. 用 TypeScript 接口规范参数结构,保障代码健壮性;
  1. 用 Ref 引用实现组件间方法调用,简化通信逻辑;
  1. 用高阶函数分离固定配置与动态配置,提升复用效率。

在实际项目中,我们可以基于该函数的设计思路,进一步拓展弹窗的功能(如自定义按钮、支持分页),也可将其封装为 Vue 插件,在全局范围内复用。这种 “抽象共性、灵活扩展” 的组件设计思想,不仅适用于弹窗,也适用于表单、表格等其他高频组件,是提升前端开发效率的关键。

Vue3 中 watch 第三个参数怎么用?6 大配置属性 + 场景指南

作者 VOLUN
2025年11月4日 10:28

在 Vue3 中,watch 的第三个参数是一个 配置选项对象(可选),用于精细控制监听行为。它支持多个属性,各自对应不同的功能,核心作用是调整监听的触发时机、深度监听、防抖节流等逻辑。以下是完整的配置属性及详细说明:

一、核心配置属性(常用)

1. immediate: boolean(默认 false)

  • 作用:控制监听是否在 初始渲染时立即执行一次回调函数(无需等待被监听值变化)。
  • 场景:需要页面加载时就根据初始值执行逻辑(例如:初始化时请求接口、设置默认状态)。
  • 示例
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 初始渲染时立即执行一次,之后 count 变化时再执行
watch(count, (newVal) => {
  console.log('count 值:', newVal) // 初始输出 0,后续修改时输出新值
}, { immediate: true })
</script>

2. deep: boolean(默认 false)

  • 作用:控制是否 深度监听引用类型(对象 / 数组)的内部属性变化
  • 注意
    • Vue3 的 watch 对 响应式对象(如 reactive 创建的) 默认会进行 “深度监听”(无需手动设 deep: true),但仅监听 “被访问过的属性”(基于 Proxy 代理的惰性监听);
    • 非响应式对象(如 ref 包裹的普通对象)需要监听所有内部属性(包括未访问过的) 时,必须手动设置 deep: true。
  • 场景:监听对象的嵌套属性(如 user.info.age)、数组的元素变化。
  • 示例
<script setup>
import { ref, watch } from 'vue'
const user = ref({ name: '张三', info: { age: 20 } })
// 监听 user 内部属性变化(需 deep: true)
watch(user, (newUser) => {
  console.log('age 变化:', newUser.info.age) // 修改 user.value.info.age 时触发
}, { deep: true })
// 触发监听:修改嵌套属性
setTimeout(() => {
  user.value.info.age = 21
}, 1000)
</script>

3. flush: 'pre' | 'post' | 'sync'(默认 'pre')

  • 作用:控制回调函数的 执行时机(即:回调在 Vue 组件更新周期的哪个阶段运行)。
  • 三个取值说明:
取值 执行时机 场景示例
'pre'(默认) 组件更新 之前 执行 需在 DOM 更新前修改数据(避免 DOM 闪烁)
'post' 组件更新 之后 执行(DOM 已更新) 需操作更新后的 DOM(如获取元素尺寸、滚动位置)
'sync' 被监听值变化时 同步立即执行 需实时响应变化(极少用,可能影响性能)
  • 示例(操作更新后的 DOM):
<template>
  <div ref="box" :style="{ width: `${count * 100}px` }"></div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(1)
const box = ref(null)
// 回调在 DOM 更新后执行,可获取最新的元素宽度
watch(count, () => {
  console.log('box 宽度:', box.value.offsetWidth) // 正确输出更新后的宽度
}, { flush: 'post' })
// 触发监听:修改 count
setTimeout(() => {
  count.value = 2
}, 1000)
</script>

二、其他实用配置(Vue3.2+ 支持)

4. once: boolean(默认 false)

  • 作用:控制监听是否 只触发一次(触发后自动停止监听)。
  • 场景:只需响应第一次变化(如:首次加载后的一次性初始化、仅需执行一次的回调)。
  • 示例
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 仅在 count 第一次变化时触发(之后再修改不触发)
watch(count, (newVal) => {
  console.log('count 首次变化:', newVal) // 仅输出 1(第一次修改时)
}, { once: true })
// 触发两次修改
setTimeout(() => { count.value = 1 }, 1000)
setTimeout(() => { count.value = 2 }, 2000) // 无输出
</script>

5. debounce: number(默认 undefined)

  • 作用:给监听添加 防抖(延迟 n 毫秒后执行回调,若期间值多次变化,仅执行最后一次)。
  • 单位:毫秒(ms)。
  • 场景:监听输入框输入(避免频繁触发接口请求)、频繁变化的数值(如滚动位置)。
  • 示例(输入框防抖):
<template>
  <input v-model="keyword" placeholder="搜索...">
</template>
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
// 输入停止 500ms 后才执行回调(避免输入时频繁触发)
watch(keyword, (newVal) => {
  console.log('搜索关键词:', newVal) // 输入停止 500ms 后输出
}, { debounce: 500 })
</script>

6. throttle: number(默认 undefined)

  • 作用:给监听添加 节流(每隔 n 毫秒最多执行一次回调,期间值多次变化仅执行一次)。
  • 单位:毫秒(ms)。
  • 场景:监听滚动事件、窗口大小变化等高频触发的场景。
  • 示例(滚动节流):
<script setup>
import { ref, watch } from 'vue'
const scrollTop = ref(0)
// 监听滚动位置,每隔 300ms 最多执行一次
window.addEventListener('scroll', () => {
  scrollTop.value = window.scrollY
})
watch(scrollTop, (newVal) => {
  console.log('滚动位置:', newVal) // 每 300ms 输出一次
}, { throttle: 300 })
</script>

三、配置属性总结表

属性名 类型 默认值 核心作用
immediate boolean false 初始渲染时是否立即执行回调
deep boolean false 是否深度监听引用类型内部属性变化
flush string 'pre' 回调执行时机(pre/post/sync)
once boolean false 是否只触发一次回调(触发后停止监听)
debounce number undefined 防抖延迟(ms),多次变化仅最后一次执行
throttle number undefined 节流间隔(ms),高频变化时限制执行频率

四、注意事项

  1. deep 的使用场景
    • 监听 reactive 对象时,默认会 “惰性深度监听”(仅监听被访问过的属性),若需监听所有属性(包括未访问的),仍需设置 deep: true;
    • 监听 ref 包裹的对象时,必须设置 deep: true 才能监听内部属性变化。
  1. debounce / throttle 的限制:仅 Vue3.2+ 版本支持,低版本需手动通过 lodash 等库实现。
  1. 性能优化:避免不必要的 deep: true(会增加监听开销),尽量精准监听具体属性(如 () => user.info.age)。
❌
❌