从零构建 Vue 弹窗组件

作者:yyt_日期:2026/1/12

整体学习路线:简易弹窗 → 完善基础功能 → 组件内部状态管理 → 父→子传值 → 子→父传值 → 跨组件传值(最终目标)


步骤 1:搭建最基础的弹窗(静态结构,无交互)

目标:实现一个固定显示在页面中的弹窗,包含标题、内容、关闭按钮,掌握 Vue 组件的基本结构。

组件文件:BasicPopup.vue

1<template>
2  <!-- 弹窗外层容器(遮罩层) -->
3  <div class="popup-mask">
4    <!-- 弹窗主体 -->
5    <div class="popup-content">
6      <h3>简易弹窗</h3>
7      <p>这是最基础的弹窗内容</p>
8      <button>关闭</button>
9    </div>
10  </div>
11</template>
12
13<script setup>
14// 现阶段无逻辑,仅搭建结构
15</script>
16
17<style scoped>
18/* 遮罩层:占满整个屏幕,半透明背景 */
19.popup-mask {
20  position: fixed;
21  top: 0;
22  left: 0;
23  right: 0;
24  bottom: 0;
25  background-color: rgba(0, 0, 0, 0.5);
26  display: flex;
27  justify-content: center;
28  align-items: center;
29}
30
31/* 弹窗主体:白色背景,固定宽高,圆角 */
32.popup-content {
33  width: 400px;
34  padding: 20px;
35  background-color: #fff;
36  border-radius: 8px;
37  text-align: center;
38}
39
40/* 按钮样式 */
41button {
42  margin-top: 20px;
43  padding: 8px 16px;
44  background-color: #1890ff;
45  color: #fff;
46  border: none;
47  border-radius: 4px;
48  cursor: pointer;
49}
50</style>
51

使用组件(App.vue

1<template>
2  <h1>弹窗学习演示</h1>
3  <BasicPopup />
4</template>
5
6<script setup>
7import BasicPopup from './components/BasicPopup.vue';
8</script>
9

步骤 2:添加基础交互(控制弹窗显示/隐藏)

目标:通过「响应式状态」控制弹窗的显示与隐藏,给关闭按钮添加点击事件,掌握 ref 和事件绑定。

改造 BasicPopup.vue(新增响应式状态和点击事件)

1<template>
2  <!--  v-if 控制弹窗是否显示 -->
3  <div class="popup-mask" v-if="isShow">
4    <div class="popup-content">
5      <h3>简易弹窗</h3>
6      <p>这是最基础的弹窗内容</p>
7      <!-- 绑定关闭按钮点击事件 -->
8      <button @click="closePopup">关闭</button>
9    </div>
10  </div>
11</template>
12
13<script setup>
14// 1. 导入 ref 用于创建响应式状态
15import { ref } from 'vue';
16
17// 2. 定义响应式变量,控制弹窗显示/隐藏
18const isShow = ref(true); // 初始值为 true,默认显示弹窗
19
20// 3. 定义关闭弹窗的方法
21const closePopup = () => {
22  isShow.value = false; // 响应式变量修改需要通过 .value
23};
24</script>
25
26<style scoped>
27/* 样式同步骤 1,不变 */
28.popup-mask {
29  position: fixed;
30  top: 0;
31  left: 0;
32  right: 0;
33  bottom: 0;
34  background-color: rgba(0, 0, 0, 0.5);
35  display: flex;
36  justify-content: center;
37  align-items: center;
38}
39
40.popup-content {
41  width: 400px;
42  padding: 20px;
43  background-color: #fff;
44  border-radius: 8px;
45  text-align: center;
46}
47
48button {
49  margin-top: 20px;
50  padding: 8px 16px;
51  background-color: #1890ff;
52  color: #fff;
53  border: none;
54  border-radius: 4px;
55  cursor: pointer;
56}
57</style>
58

补充:在 App.vue 添加「打开弹窗」按钮

我们知道,Vue 遵循「单向数据流」和「组件封装隔离」,子组件内部的方法 / 私有状态默认是对外隐藏的,外部父组件无法直接访问。

ref 就是打破这种 “隔离” 的合法方式(非侵入式),让父组件能够:

  1. 调用子组件通过 defineExpose 暴露的方法(如 openPopup 方法,用于打开弹窗)。
  2. 访问子组件通过 defineExpose 暴露的响应式状态(如弹窗内部的 isShow 状态)。
  3. 实现「父组件主动控制子组件」的交互场景(如主动打开 / 关闭弹窗、主动刷新子组件数据)。
1<template>
2  <h1>弹窗学习演示</h1>
3  <!-- 新增打开弹窗按钮 -->
4  <button @click="handleOpenPopup">打开弹窗</button>
5  <BasicPopup ref="popupRef" />
6</template>
7
8<script setup>
9import { ref } from 'vue';
10import BasicPopup from './components/BasicPopup.vue';
11
12// 获取弹窗组件实例
13const popupRef = ref(null);
14
15// 打开弹窗的方法(调用子组件的方法,后续步骤会完善)
16const handleOpenPopup = () => {
17  if (popupRef.value) {
18    popupRef.value.openPopup();
19  }
20};
21</script>
22
23<style>
24button {
25  margin: 10px;
26  padding: 8px 16px;
27  background-color: #1890ff;
28  color: #fff;
29  border: none;
30  border-radius: 4px;
31  cursor: pointer;
32}
33</style>
34

BasicPopup.vue 补充「打开弹窗」方法(暴露给父组件)

1<template>
2  <div class="popup-mask" v-if="isShow">
3    <div class="popup-content">
4      <h3>简易弹窗</h3>
5      <p>这是最基础的弹窗内容</p>
6      <button @click="closePopup">关闭</button>
7    </div>
8  </div>
9</template>
10
11<script setup>
12import { ref } from 'vue';
13
14const isShow = ref(false); // 初始值改为 false,默认隐藏
15
16const closePopup = () => {
17  isShow.value = false;
18};
19
20// 新增:打开弹窗的方法
21const openPopup = () => {
22  isShow.value = true;
23};
24
25// 暴露组件方法,让父组件可以调用(关键)
26defineExpose({
27  openPopup
28});
29</script>
30
31<style scoped>
32/* 样式同前 */
33</style>
34

核心知识点

  1. ref 创建响应式状态,修改时需要 .value
  2. @click 事件绑定,触发自定义方法
  3. defineExpose 暴露组件内部方法/状态,供父组件调用
  4. v-if 控制元素的渲染与销毁(实现弹窗显示/隐藏)

步骤 3:父组件 → 子组件传值(Props 传递)

目标:父组件向弹窗组件传递「弹窗标题」和「弹窗内容」,掌握 defineProps 的使用,实现弹窗内容的动态化。

改造 BasicPopup.vue(接收父组件传递的参数)

1<template>
2  <div class="popup-mask" v-if="isShow">
3    <div class="popup-content">
4      <!-- 渲染父组件传递的标题 -->
5      <h3>{{ popupTitle }}</h3>
6      <!-- 渲染父组件传递的内容 -->
7      <p>{{ popupContent }}</p>
8      <button @click="closePopup">关闭</button>
9    </div>
10  </div>
11</template>
12
13<script setup>
14import { ref } from 'vue';
15
16// 1. 定义 Props,接收父组件传递的值(指定类型和默认值)
17const props = defineProps({
18  // 弹窗标题
19  popupTitle: {
20    type: String,
21    default: '默认弹窗标题' // 默认值,防止父组件未传递
22  },
23  // 弹窗内容
24  popupContent: {
25    type: String,
26    default: '默认弹窗内容'
27  }
28});
29
30const isShow = ref(false);
31
32const closePopup = () => {
33  isShow.value = false;
34};
35
36const openPopup = () => {
37  isShow.value = true;
38};
39
40defineExpose({
41  openPopup
42});
43</script>
44
45<style scoped>
46/* 样式同前 */
47</style>
48

改造 App.vue(向子组件传递 Props 数据)

1<template>
2  <h1>弹窗学习演示</h1>
3  <button @click="openPopup">打开弹窗</button>
4  <!-- 向子组件传递 props 数据(静态传递 + 动态传递均可) -->
5  <BasicPopup
6    ref="popupRef"
7    popupTitle="父组件传递的标题"
8    :popupContent="dynamicContent"
9  />
10</template>
11
12<script setup>
13import { ref } from 'vue';
14import BasicPopup from './components/BasicPopup.vue';
15
16const popupRef = ref(null);
17// 动态定义弹窗内容(也可以是静态字符串)
18const dynamicContent = ref('这是父组件通过 Props 传递的动态弹窗内容~');
19
20const openPopup = () => {
21  if (popupRef.value) {
22    popupRef.value.openPopup();
23  }
24};
25</script>
26
27<style>
28button {
29  margin: 10px;
30  padding: 8px 16px;
31  background-color: #1890ff;
32  color: #fff;
33  border: none;
34  border-radius: 4px;
35  cursor: pointer;
36}
37</style>
38

核心知识点

  1. defineProps 定义组件接收的参数,支持类型校验和默认值
  2. Props 传递规则:父组件通过「属性绑定」向子组件传值,子组件只读(不能修改 Props,遵循单向数据流)
  3. 静态传值(直接写字符串)直接把等号后面的内容作为纯字符串传递给子组件的 Props,Vue 不会对其做任何解析、计算,原样传递。动态传值(:xxx="变量") Vue 会先解析求值,再把结果传递给子组件的 Props。

步骤 4:子组件 → 父组件传值(Emits 事件派发)

目标:弹窗关闭时,向父组件传递「弹窗关闭的状态」和「自定义数据」,掌握 defineEmits 的使用,实现子向父的通信。

改造 BasicPopup.vue(派发事件给父组件)

1<template>
2  <div class="popup-mask" v-if="isShow">
3    <div class="popup-content">
4      <h3>{{ popupTitle }}</h3>
5      <p>{{ popupContent }}</p>
6      <!-- 关闭按钮点击时,触发事件派发 -->
7      <button @click="handleClose">关闭</button>
8    </div>
9  </div>
10</template>
11
12<script setup>
13import { ref } from 'vue';
14
15// 1. 定义 Props
16const props = defineProps({
17  popupTitle: {
18    type: String,
19    default: '默认弹窗标题'
20  },
21  popupContent: {
22    type: String,
23    default: '默认弹窗内容'
24  }
25});
26
27// 2. 定义 Emits,声明要派发的事件(支持数组/对象格式,对象格式可校验)
28const emit = defineEmits([
29  'popup-close', // 弹窗关闭事件
30  'send-data'    // 向父组件传递数据的事件
31]);
32
33const isShow = ref(false);
34
35// 3. 改造关闭方法,派发事件给父组件
36const handleClose = () => {
37  isShow.value = false;
38  
39  // 派发「popup-close」事件,可携带参数(可选)
40  emit('popup-close', {
41    closeTime: new Date().toLocaleString(),
42    message: '弹窗已正常关闭'
43  });
44  
45  // 派发「send-data」事件,传递自定义数据
46  emit('send-data', '这是子组件向父组件传递的额外数据');
47};
48
49const openPopup = () => {
50  isShow.value = true;
51};
52
53defineExpose({
54  openPopup
55});
56</script>
57
58<style scoped>
59/* 样式同前 */
60</style>
61

改造 App.vue(监听子组件派发的事件)

1<template>
2  <h1>弹窗学习演示</h1>
3  <button @click="openPopup">打开弹窗</button>
4  <!-- 监听子组件派发的事件,绑定处理方法 -->
5  <BasicPopup
6    ref="popupRef"
7    popupTitle="父组件传递的标题"
8    :popupContent="dynamicContent"
9    @popup-close="handlePopupClose"
10    @send-data="handleReceiveData"
11  />
12</template>
13
14<script setup>
15import { ref } from 'vue';
16import BasicPopup from './components/BasicPopup.vue';
17
18const popupRef = ref(null);
19const dynamicContent = ref('这是父组件通过 Props 传递的动态弹窗内容~');
20
21const openPopup = () => {
22  if (popupRef.value) {
23    popupRef.value.openPopup();
24  }
25};
26
27// 1. 处理子组件派发的「popup-close」事件
28const handlePopupClose = (closeInfo) => {
29  console.log('接收弹窗关闭信息:', closeInfo);
30  alert(`弹窗已关闭,关闭时间:${closeInfo.closeTime}`);
31};
32
33// 2. 处理子组件派发的「send-data」事件
34const handleReceiveData = (data) => {
35  console.log('接收子组件传递的数据:', data);
36  alert([`收到子组件数据:${data}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.data.md));
37};
38</script>
39
40<style>
41button {
42  margin: 10px;
43  padding: 8px 16px;
44  background-color: #1890ff;
45  color: #fff;
46  border: none;
47  border-radius: 4px;
48  cursor: pointer;
49}
50</style>
51

核心知识点

  1. defineEmits 声明组件要派发的事件,遵循「事件名小写+短横线分隔」规范
  2. emit 方法用于派发事件,第一个参数是事件名,后续参数是要传递的数据
  3. 父组件通过 @事件名 监听子组件事件,处理方法的参数就是子组件传递的数据
  4. 单向数据流补充:子组件不能直接修改 Props,如需修改,可通过「子组件派发事件 → 父组件修改数据 → Props 重新传递」实现

步骤 5:跨组件传值(非父子组件,使用 Provide / Inject)

目标:实现「非父子组件」(如:Grandpa.vueParent.vuePopup.vue,爷爷组件向弹窗组件传值)的通信,掌握 provide / inject 的使用,这是 Vue 中跨组件传值的核心方案之一。

步骤 5.1:创建层级组件结构

1├── App.vue(入口)
2├── components/
3   ├── Grandpa.vue(爷爷组件,提供数据)
4   ├── Parent.vue(父组件,中间层级,无数据处理)
5   └── Popup.vue(弹窗组件,注入并使用数据)
6

步骤 5.2:爷爷组件 Grandpa.vue(提供数据,provide

1<template>
2  <div class="grandpa">
3    <h2>爷爷组件</h2>
4    <button @click="updateGlobalData">修改跨组件传递的数据</button>
5    <!-- 引入父组件(中间层级) -->
6    <Parent />
7  </div>
8</template>
9
10<script setup>
11import { ref, provide } from 'vue';
12import Parent from './Parent.vue';
13
14// 1. 定义要跨组件传递的响应式数据
15const globalPopupConfig = ref({
16  author: 'yyt',
17  version: '1.0.0',
18  theme: 'light',
19  maxWidth: '500px'
20});
21
22// 2. 提供(provide)数据,供后代组件注入使用
23// 第一个参数:注入标识(字符串/Symbol),第二个参数:要传递的数据
24provide('popupGlobalConfig', globalPopupConfig);
25
26// 3. 修改响应式数据(后代组件会同步更新)
27const updateGlobalData = () => {
28  globalPopupConfig.value = {
29    ...globalPopupConfig.value,
30    version: '2.0.0',
31    theme: 'dark',
32    updateTime: new Date().toLocaleString()
33  };
34};
35</script>
36
37<style scoped>
38.grandpa {
39  padding: 20px;
40  border: 2px solid #666;
41  border-radius: 8px;
42  margin: 10px;
43}
44
45button {
46  padding: 8px 16px;
47  background-color: #1890ff;
48  color: #fff;
49  border: none;
50  border-radius: 4px;
51  cursor: pointer;
52}
53</style>
54

步骤 5.3:父组件 Parent.vue(中间层级,仅做组件嵌套)

1<template>
2  <div class="parent">
3    <h3>父组件(中间层级)</h3>
4    <button @click="openPopup">打开跨组件传值的弹窗</button>
5    <!-- 引入弹窗组件 -->
6    <Popup ref="popupRef" />
7  </div>
8</template>
9
10<script setup>
11import { ref } from 'vue';
12import Popup from './Popup.vue';
13
14const popupRef = ref(null);
15
16// 打开弹窗
17const openPopup = () => {
18  if (popupRef.value) {
19    popupRef.value.openPopup();
20  }
21};
22</script>
23
24<style scoped>
25.parent {
26  padding: 20px;
27  border: 2px solid #999;
28  border-radius: 8px;
29  margin: 10px;
30  margin-top: 20px;
31}
32
33button {
34  padding: 8px 16px;
35  background-color: #1890ff;
36  color: #fff;
37  border: none;
38  border-radius: 4px;
39  cursor: pointer;
40}
41</style>
42

步骤 5.4:弹窗组件 Popup.vue(注入数据,inject

1<template>
2  <div class="popup-mask" v-if="isShow" :style="{ backgroundColor: themeBg }">
3    <div class="popup-content" :style="{ maxWidth: globalConfig.maxWidth, background: globalConfig.theme === 'dark' ? '#333' : '#fff', color: globalConfig.theme === 'dark' ? '#fff' : '#333' }">
4      <h3>{{ popupTitle }}</h3>
5      <p>{{ popupContent }}</p>
6      <!-- 渲染跨组件传递的数据 -->
7      <div class="global-info" style="margin: 15px 0; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
8        <p>跨组件传递的配置:</p>
9        <p>作者:{{ globalConfig.author }}</p>
10        <p>版本:{{ globalConfig.version }}</p>
11        <p>主题:{{ globalConfig.theme }}</p>
12        <p v-if="globalConfig.updateTime">更新时间:{{ globalConfig.updateTime }}</p>
13      </div>
14      <button @click="handleClose">关闭</button>
15    </div>
16  </div>
17</template>
18
19<script setup>
20import { ref, inject, computed } from 'vue';
21
22// 1. 定义 Props
23const props = defineProps({
24  popupTitle: {
25    type: String,
26    default: '默认弹窗标题'
27  },
28  popupContent: {
29    type: String,
30    default: '默认弹窗内容'
31  }
32});
33
34// 2. 注入(inject)爷爷组件提供的数据
35// 第一个参数:注入标识(与 provide 一致),第二个参数:默认值(可选)
36const globalConfig = inject('popupGlobalConfig', ref({ author: '默认作者', version: '0.0.1' }));
37
38// 3. 基于注入的数据创建计算属性(可选,优化使用体验)
39const themeBg = computed(() => {
40  return globalConfig.value.theme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)';
41});
42
43// 4. 定义 Emits
44const emit = defineEmits(['popup-close']);
45
46const isShow = ref(false);
47
48// 5. 关闭方法
49const handleClose = () => {
50  isShow.value = false;
51  emit('popup-close', { message: '弹窗已关闭' });
52};
53
54// 6. 打开弹窗方法
55const openPopup = () => {
56  isShow.value = true;
57};
58
59// 7. 暴露方法
60defineExpose({
61  openPopup
62});
63</script>
64
65<style scoped>
66.popup-mask {
67  position: fixed;
68  top: 0;
69  left: 0;
70  right: 0;
71  bottom: 0;
72  display: flex;
73  justify-content: center;
74  align-items: center;
75}
76
77.popup-content {
78  padding: 20px;
79  border-radius: 8px;
80  text-align: center;
81}
82
83button {
84  margin-top: 20px;
85  padding: 8px 16px;
86  background-color: #1890ff;
87  color: #fff;
88  border: none;
89  border-radius: 4px;
90  cursor: pointer;
91}
92</style>
93

步骤 5.5:入口 App.vue(引入爷爷组件)

1<template>
2  <h1>跨组件传值演示(Provide / Inject)</h1>
3  <Grandpa />
4</template>
5
6<script setup>
7import Grandpa from './components/Grandpa.vue';
8</script>
9

核心知识点

  1. provide / inject 用于跨层级组件通信(无论层级多深),爷爷组件提供数据,后代组件注入使用
  2. 传递响应式数据:provide 时传递 ref/reactive 包装的数据,后代组件可感知数据变化(同步更新)
  3. 注入标识:建议使用 Symbol 避免命名冲突(生产环境推荐),本步骤为简化使用字符串标识
  4. 注入默认值:inject 第二个参数为默认值,防止祖先组件未提供数据时出现报错
  5. 与 Props/Emits 的区别:Props 适用于父子组件,provide/inject 适用于跨层级组件

从零构建 Vue 弹窗组件》 是转载文章,点击查看原文


相关推荐


深入UDP与sk_buff:掌握Linux网络协议栈的核心机制
咸鱼_要_翻身2026/1/3

目录 一、UDP 在网络协议栈中的位置 二、UDP 报文格式(RFC 768) 字段详解 三、UDP 如何解析报文?——定长头部分离机制 1、理解UDP报头 说明 注意事项 2、UDP数据封装流程:(自上而下) 3、UDP数据分用流程:(自下往上) 四、UDP 如何将数据交付给正确的应用进程?——端口分用(Demultiplexing) 工作流程 服务端 vs 客户端 五、UDP 的核心特性 1、无连接(Connectionless) 2、不可靠(Unrelia


iOS开发必备的HTTP网络基础概览
sweet丶2025/12/25

一、从一次HTTP请求说起 以下是一个大体过程,不包含DNS缓存等等细节: sequenceDiagram participant C as 客户端(iOS App) participant D as DNS服务器 participant S as 目标服务器 participant T as TLS/SSL层 Note over C,S: 1. DNS解析阶段 C->>D: 查询域名对应IP D-->>C: 返回IP地址


🚀你以为你在写 React?其实你在“搭一套前端操作系统”
白兰地空瓶2025/12/17

——从 Vite + React 架构出发,对照 Vue,彻底看懂现代前端工程化 👉 “现代前端不是写页面,而是在设计一套「运行在浏览器里的应用架构」。” 一、先说结论:React / Vue 早就不只是“框架”了 很多人学 React / Vue 的路径是这样的: JSX / template → 组件 → 状态 → 路由 → API 请求 ✋ 到此为止 但面试官想听的不是这个。 他们更关心的是: 你知不知道项目是怎么被“跑起来”的 dev / test / production


别让页面 “鬼畜跳”!Google 钦点的 3 个性能指标,治好了我 80% 的用户投诉
PineappleCoder2025/12/9

💥告别卡顿!前端性能优化第一课:Google钦点的三大核心指标,你真的懂吗? 欢迎来到前端性能优化专栏的第一课!在这个“用户体验至上”的时代,一个卡顿、缓慢、乱跳的网站,就像一辆抛锚在高速公路上的跑车,再酷炫也只会让人抓狂。别担心,Google已经为你准备好了一份“体检报告”——核心Web指标(Core Web Vitals) 。 今天,我们就来揭开这份报告的神秘面纱,用最通俗易懂的方式,让你彻底搞懂这三大指标,迈出性能优化的第一步! ✨ LCP(Largest Contentful Pa


用户数据报协议(UDP)详解
CodePracticer2025/11/28

一、传输层协议UDP 1. 理解UDP协议 我们以前说过,0-1023端口号是知名端口号,它们是与指定的协议进行关联的,那么我们如何证明呢? 在指定目录下就可以查找到这些协议的端口号了(/etc/services)。 这里以两个例子来说明情况。 前面我们也说过协议就是一种约定,本质就是结构体。今天我们来正式认识一下UDP协议。 可以看到UDP协议的宽度是32位,源端口号和目的端口号分别占16位,UDP协议的报头是8字节。 前面我们说过,源主机的数据发送给目标主机需要先经历封装在解包的过程,


万字长文!搞懂强化学习的基础知识!
aicoting2026/1/20

推荐直接网站在线阅读:aicoting.cn 强化学习是什么? 强化学习(Reinforcement Learning, RL)是一类通过与环境交互来学习最优决策策略的机器学习方法。与监督学习不同,强化学习没有直接提供的“正确答案”,而是通过奖励信号(reward)来评估行为的好坏。智能体(agent)在环境(environment)中执行动作(action),根据环境反馈获得奖励,并观察状态(state)变化。 强化学习的目标是学习一个策略,使得智能体在长期交互中获得累计奖励最大化。典型方法包

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客