使用 TypeScript 编写一个 Vue 3 模态框(Modal)组件

本文展示了一个基于Vue3的模态框组件实现,采用TypeScript编写。


组件通过Teleport实现DOM内容定位,使用props控制显示状态(isOpen),通过插槽(slot)支持内容自定义,并利用emit触发关闭事件。


特别介绍了<script setup>语法糖的改进实现,包括更简洁的props/emits定义(TypeScript类型安全)、自动暴露方法和scoped样式隔离。


组件具备条件渲染(v-if)、父子通信(props/emit)等特性,适合作为通用模态框解决方案。


代码分析


javascript 复制代码
<template>
  <teleport to="#modal">
    <!-- teleport:将应该渲染在父组件中的内容渲染到指定位置 -->
    <!-- teleport 是把已渲染的组件内容移动到目标节点,若组件没有被挂载(未在模板中使用),就不会有任何内容被 teleport 到目标节点。 -->
    <div id="center" v-if="isOpen">
      <!-- slot:插槽,可以插入自定义内容 -->
      <h2><slot>this is a modal</slot></h2>
      <div>
        <button @click="closeModal()">close</button>
      </div>
    </div>
  </teleport>
</template>

<script lang="ts">
  import { defineComponent } from "vue";
  export default defineComponent({
    props:{
      isOpen:{
        type: Boolean,
        default: false
      }
    },
    // emits:{// 向外触发自定义事件
    //   "close-modal":(payload:any)=>{
    //     //运行时校验
    //     return payload.type === "close";
    //   }
    // },
    //不需要验证
    emits:{
      "close-modal":null
    },
    // setup(props,context){
    //   //Modal.vue:28 [Vue warn]: Invalid event arguments: event validation failed for event "close-modal".
    //   //context.emit("close-modal",{type:"hello"});
    //   context.emit("close-modal",{type:"close"});
    // }
    //当前不需要验证
    setup(props,context){
      const closeModal=()=>{
        context.emit("close-modal");
      }
      return {
        closeModal
      };
    }
  });
</script>

<style>
  #center {
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    position: absolute;
    top: 0;
    left: 0;
    border:2px solid rgb(42, 39, 39);
    z-index: 1000;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    color: rgb(36, 35, 35);
  }
</style>

这段代码是一个 Vue 3 模态框(Modal)组件,使用 TypeScript 编写。下面是对代码的详细解释:


模板部分 (<template>)


1. Teleport 组件

复制代码
<teleport to="#modal">
  • 作用 :将组件的内容渲染到 DOM 中的指定位置(#modal),而不是默认的父组件位置

  • 原理:组件逻辑上仍属于父组件,但 DOM 结构会移动到目标节点

  • 注意:组件必须已经被挂载(在模板中使用)才会被 teleport


2. 模态框容器

复制代码
<div id="center" v-if="isOpen">
  • v-if="isOpen":控制模态框显示/隐藏,由父组件传入的 isOpen prop 控制

3. 插槽 (Slot)

复制代码
<h2><slot>this is a modal</slot></h2>
  • 提供默认内容 "this is a modal"

  • 父组件可以传入自定义内容替换默认内容


4. 关闭按钮

复制代码
<button @click="closeModal()">close</button>
  • 点击触发 closeModal 方法,发射自定义事件通知父组件

脚本部分 (<script>)

1. Props 定义

复制代码
props:{
  isOpen:{
    type: Boolean,
    default: false
  }
}
  • 接收父组件传入的 isOpen 属性控制模态框显示状态

  • 默认值为 false(不显示)


2. 自定义事件定义

复制代码
// 有验证的版本(注释中)
emits:{
  "close-modal":(payload:any)=>{
    return payload.type === "close"; // 验证 payload
  }
}

// 实际使用的无验证版本
emits:{
  "close-modal":null  // 不进行验证
}
  • 声明组件会触发的自定义事件

  • 有两种写法:带验证和不带验证

  • 当前使用不带验证的版本


3. Setup 函数

复制代码
setup(props,context){
  const closeModal=()=>{
    context.emit("close-modal");  // 触发事件
  }
  return {
    closeModal  // 暴露给模板使用
  };
}
  • context.emit():触发自定义事件,通知父组件

  • 返回 closeModal 函数供模板绑定


样式部分 (<style>)

模态框样式

  • 全屏覆盖width: 100%; height: 100%; position: absolute; top: 0; left: 0;

  • 半透明背景background-color: rgba(0, 0, 0, 0.5);

  • 层级最高z-index: 1000;(确保在最上层)

  • 居中显示:使用 flex 布局实现水平和垂直居中


使用示例

父组件可能这样使用:

javascript 复制代码
<template>
  <div>
    <button @click="showModal = true">打开模态框</button>
    <Modal :isOpen="showModal" @close-modal="showModal = false">
      自定义内容
    </Modal>
    <div id="modal"></div> <!-- Teleport 目标 -->
  </div>
</template>

关键特性

  1. Teleport:解决模态框的 z-index 和样式隔离问题

  2. 插槽:提供内容自定义能力

  3. Props/Events:实现父子组件通信

  4. 条件渲染 :通过 v-if 控制显示

  5. 样式隔离:模态框样式不会影响其他组件


script 部分改为 <script setup> 实现


以下是使用 <script setup> 语法重写的代码:

vue

javascript 复制代码
<template>
  <teleport to="#modal">
    <div id="center" v-if="isOpen">
      <h2><slot>this is a modal</slot></h2>
      <div>
        <button @click="closeModal">close</button>
      </div>
    </div>
  </teleport>
</template>

<script setup lang="ts">
//import { defineProps, defineEmits } from "vue";
//defineProps 导入声明与局部声明冲突
//这个问题是因为在 <script setup> 中,defineProps 和 defineEmits 是编译时宏,不需要从 Vue 中显式导入。Vue 编译器会自动处理它们。

// 定义 props
const props = defineProps<{
  isOpen: boolean;
}>();

// 定义 emits(两种写法)

// 写法1:简单声明(无参数验证)
const emit = defineEmits<{
  (e: "close-modal"): void;
}>();

// 写法2:带参数验证
// const emit = defineEmits<{
//   (e: "close-modal", payload: { type: string }): void;
// }>();

// 写法3:使用运行时声明(类似选项式API)
// defineEmits(["close-modal"]);
// const emit = defineEmits();

// 关闭模态框方法
const closeModal = () => {
  // 触发自定义事件
  emit("close-modal");
  
  // 如果使用带验证的写法
  // emit("close-modal", { type: "close" });
};
</script>

<style scoped>
/* 使用 scoped 让样式只作用于当前组件 */
#center {
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  position: absolute;
  top: 0;
  left: 0;
  border: 2px solid rgb(42, 39, 39);
  z-index: 1000;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  color: rgb(36, 35, 35);
}
</style>

主要改动和说明:

1. 使用 <script setup> 语法

  • 更简洁的 Composition API 写法

  • 不需要显式导出,顶层绑定会自动暴露给模板


2. Props 定义

复制代码
const props = defineProps<{
  isOpen: boolean;
}>();
  • 使用 TypeScript 类型字面量,提供类型安全

  • 等效于原来的选项式声明,但更简洁


3. Emits 定义(三种写法)

写法1:简单声明

复制代码
const emit = defineEmits<{
  (e: "close-modal"): void;
}>();

写法2:带参数验证

复制代码
const emit = defineEmits<{
  (e: "close-modal", payload: { type: string }): void;
}>();

写法3:运行时声明(向下兼容)

复制代码
defineEmits(["close-modal"]);

4. 方法定义

复制代码
const closeModal = () => {
  emit("close-modal");
};
  • 方法直接声明为顶层变量

  • 无需在 setup 函数中返回,自动暴露给模板

5. 样式作用域

复制代码
<style scoped>
  • 添加 scoped 属性,使样式只作用于当前组件

  • 避免样式污染全局

6. 模板中的使用

  • 使用 closeModal 或者 closeModal()

在实际开发中,建议:

  • 如果函数需要参数,使用带括号的写法

  • 如果函数不需要参数,两种写法都可以,但带括号的写法更明确


两种写法的区别:

1. @click="closeModal()"(带括号)

  • 直接调用函数,可以传递参数

  • 例如:@click="closeModal(item)"

2. @click="closeModal"(不带括号)

  • Vue 会自动将事件对象作为第一个参数传递给函数

  • 等价于:@click="(...args) => closeModal(...args)"

  • 但前提是closeModal 函数本身接受参数

使用示例(父组件)

javascript 复制代码
<template>
  <div>
    <button @click="showModal = true">打开模态框</button>
    <!-- 使用组件 -->
    <Modal :isOpen="showModal" @close-modal="handleClose">
      自定义模态框内容
    </Modal>
    <!-- Teleport 目标 -->
    <div id="modal"></div>
  </div>
</template>

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

const showModal = ref(false);

const handleClose = () => {
  showModal.value = false;
};
</script>

<script setup> 的优势

  1. 更简洁的代码:减少样板代码

  2. 更好的类型推断:TypeScript 支持更完善

  3. 自动暴露:顶层绑定自动暴露给模板

  4. 更好的性能:编译时优化,运行时开销更小

  5. 更清晰的逻辑组织:相关逻辑可以分组在一起


这种写法是 Vue 3 推荐的 Composition API 写法,特别适合 TypeScript 项目。

相关推荐
踢球的打工仔2 小时前
typescript-void和never
前端·javascript·typescript
hugo_im2 小时前
GrapesJS 完全指南:从零构建你的可视化拖拽编辑器
前端·javascript·前端框架
盘子素2 小时前
前端实现跳转子系统,但限制只能跳转一次
前端·javascript
前端_yu小白2 小时前
React实现Vue的watch和computed
前端·vue.js·react.js·watch·computed·hooks
多看书少吃饭2 小时前
OnlyOffice 编辑器的实现及使用
前端·vue.js·编辑器
float_六七2 小时前
JS比较运算符:从坑点速记到实战口诀
开发语言·javascript·ecmascript
用户65868180338403 小时前
Vue3 项目编码规范:基于Composable的清晰架构实践
vue.js
小酒星小杜3 小时前
在AI时代,技术人应该每天都要花两小时来构建一个自身的构建系统 - Build 篇
前端·vue.js·架构
zengyufei3 小时前
2.4 watch 监听变化
vue.js