你碰到“用Vue3.0写过组件吗?如果想实现一个Modal你会怎么设计?”这种面试问题,直接说不会确实不妥。但其实这类题的核心不是实现难度,而是考察我们对组件化、模块化思维的理解。今天我就分享一下如何从需求分析到具体实现,构建一个Vue3的Modal组件。
首先,我们分析一下这个Modal的常见需求:它需要弹窗的基本元素,比如遮罩层、标题、主体内容,以及“确定”和“取消”按钮。Modal在UI设计中是很常见的,典型的需求就是点击弹出一个内容窗口。
我们还会遇到新增和编辑功能类似、只是细节不同的情况,这种时候完全可以使用一个组件,通过传参控制内容,而不是重复写两个组件,降低冗余,提升代码复用性。
在技术实现上,我们还需考虑几个点:内容展示的多样性,比如能接收字符串、插槽内容,甚至整个html结构;弹窗的独立性,通常会挂载在body之上,避免受到父级结构的限制。
为达到这些需求,我们将使用Vue3的Teleport功能,让Modal的内容在DOM中脱离当前的Vue实例,直接渲染在body节点上。
通常情况下,我会把组件按功能分成独立模块。在这里可以设计如下目录结构:
├── plugins
│ └── modal
│ ├── Content.tsx // 用于渲染Modal的动态内容
│ ├── Modal.vue // Modal组件主结构
│ ├── config.ts // 全局默认配置
│ ├── index.ts // 组件入口文件
│ ├── locale // 国际化支持
│ │ ├── index.ts
│ │ └── lang
│ │ ├── en-US.ts
│ │ └── zh-CN.ts
│ └── modal.type.ts // TS类型声明
我们把Modal组件放到plugins/modal
目录下,方便后续通过app.use(Modal)
形式注册成为全局插件。
在实现Modal时,最外层可以用Vue3的Teleport
组件,传送Modal到body上。Modal的基本结构包括遮罩层、标题栏、内容区域和按钮区域。代码如下:
<Teleport to="body" :disabled="!isTeleport">
<div v-if="modelValue" class="modal">
<div class="mask" :style="style" @click="maskClose && !loading && handleCancel()"></div>
<div class="modal__main">
<div class="modal__title line line--b">
<span>{{ title || t("r.title") }}</span>
<span v-if="close" class="close" @click="!loading && handleCancel()">✕</span>
</div>
<div class="modal__content">
<Content v-if="typeof content === 'function'" :render="content" />
<slot v-else>{{ content }}</slot>
</div>
<div class="modal__btns line line--t">
<button :disabled="loading" @click="handleConfirm">{{ t("r.confirm") }}</button>
<button @click="!loading && handleCancel()">{{ t("r.cancel") }}</button>
</div>
</div>
</div>
</Teleport>
遮罩层与弹窗内容分开处理,点击遮罩层时调用handleCancel
,关闭弹窗。内容展示区域则通过判断内容类型(字符串或插槽)选择不同的展示方式。Modal的显示和隐藏通过modelValue
控制,Vue3中这种状态可以用v-model
绑定到父组件。
Modal内部内容的展示方式有多种,可以是插槽,也可以通过字符串传递内容。以下是通过默认插槽和字符串传递内容的示例:
<!-- 默认插槽 -->
<Modal v-model="show" title="演示 slot">
<div>hello world~</div>
</Modal>
<!-- 字符串形式 -->
<Modal v-model="show" title="演示 content" content="hello world~" />
这种灵活的内容传递方式非常适合需要定制化的场景,让Modal组件能够轻松处理多种内容需求。
在一些情况下,我们希望Modal通过API调用,而不是直接引用组件标签。Vue2中可以用Vue.extend
创建Modal实例,然后挂载到DOM上,但Vue3移除了这个API,我们可以用createVNode
实现相同效果。
import { createVNode, render } from 'vue';
import Modal from './Modal.vue';
const container = document.createElement('div');
const vnode = createVNode(Modal);
render(vnode, container);
document.body.appendChild(container);
const instance = vnode.component;
API形式下,我们还可以通过app.config.globalProperties
把Modal实例挂载到全局对象上,让其他组件通过$modal
进行调用。这种做法适合全局的、临时的Modal需求。
export default {
install(app) {
app.config.globalProperties.$modal = {
show(options) {
/* 调用 Modal 实例 */
}
};
}
}
Modal的“确定”和“取消”是最基本的交互事件。Vue3的Composition API让事件处理更直观。在setup
函数中,我们可以通过getCurrentInstance
获取当前组件实例,从而绑定特定事件的处理逻辑。下面的代码展示了事件处理的基本实现:
setup(props, { emit }) {
const handleConfirm = () => {
emit('on-confirm');
};
const handleCancel = () => {
emit('on-cancel');
};
return { handleConfirm, handleCancel };
}
通过emit
函数,我们可以触发父组件监听的事件,比如“on-confirm”和“on-cancel”。API形式调用中,我们还可以为每个Modal实例添加事件监听方法,比如通过_hub
来管理事件的回调函数:
app.config.globalProperties.$modal = {
show({
onConfirm,
onCancel
}) {
/* 监听 确定、取消 事件 */
instance._hub = {
'on-confirm': onConfirm || (() => {}),
'on-cancel': onCancel || (() => {})
};
}
};
实际开发中,Modal组件可能还会涉及国际化和TypeScript支持。国际化可以通过配置文件和locale
目录实现,在Vue实例的根组件中设定当前语言环境。TypeScript方面,为组件设置类型声明可以增强代码的健壮性和可读性。
那如果面试官问你这个问题,你可以根据下面的思路回答。
是的,我用Vue3写过组件。关于Modal组件的设计,我通常会把需求分解为几个部分:遮罩层、标题、内容展示区和交互按钮(如确定、取消)。实现时可以用Vue3的Teleport
组件将内容挂载到body上,避免被父级DOM结构影响。
内容区设计上可以根据内容的传入类型(字符串、HTML结构、或插槽)来灵活渲染。我还会提供一个API形式的调用方法,让Modal能够在Vue实例之外被独立调用,便于在项目的不同模块中快速使用。
目前,对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥私藏精品 热门推荐 虎哥作为一名老码农,整理了全网最全《前端资料合集》。