从零构建 Vue 弹窗组件
整体学习路线:简易弹窗 → 完善基础功能 → 组件内部状态管理 → 父→子传值 → 子→父传值 → 跨组件传值(最终目标)
步骤 1:搭建最基础的弹窗(静态结构,无交互)
目标:实现一个固定显示在页面中的弹窗,包含标题、内容、关闭按钮,掌握 Vue 组件的基本结构。
组件文件:BasicPopup.vue
<template>
<!-- 弹窗外层容器(遮罩层) -->
<div class="popup-mask">
<!-- 弹窗主体 -->
<div class="popup-content">
<h3>简易弹窗</h3>
<p>这是最基础的弹窗内容</p>
<button>关闭</button>
</div>
</div>
</template>
<script setup>
// 现阶段无逻辑,仅搭建结构
</script>
<style scoped>
/* 遮罩层:占满整个屏幕,半透明背景 */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
/* 弹窗主体:白色背景,固定宽高,圆角 */
.popup-content {
width: 400px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
text-align: center;
}
/* 按钮样式 */
button {
margin-top: 20px;
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
使用组件(App.vue)
<template>
<h1>弹窗学习演示</h1>
<BasicPopup />
</template>
<script setup>
import BasicPopup from './components/BasicPopup.vue';
</script>
步骤 2:添加基础交互(控制弹窗显示/隐藏)
目标:通过「响应式状态」控制弹窗的显示与隐藏,给关闭按钮添加点击事件,掌握 ref 和事件绑定。
改造 BasicPopup.vue(新增响应式状态和点击事件)
<template>
<!-- 用 v-if 控制弹窗是否显示 -->
<div class="popup-mask" v-if="isShow">
<div class="popup-content">
<h3>简易弹窗</h3>
<p>这是最基础的弹窗内容</p>
<!-- 绑定关闭按钮点击事件 -->
<button @click="closePopup">关闭</button>
</div>
</div>
</template>
<script setup>
// 1. 导入 ref 用于创建响应式状态
import { ref } from 'vue';
// 2. 定义响应式变量,控制弹窗显示/隐藏
const isShow = ref(true); // 初始值为 true,默认显示弹窗
// 3. 定义关闭弹窗的方法
const closePopup = () => {
isShow.value = false; // 响应式变量修改需要通过 .value
};
</script>
<style scoped>
/* 样式同步骤 1,不变 */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.popup-content {
width: 400px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
text-align: center;
}
button {
margin-top: 20px;
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
补充:在 App.vue 添加「打开弹窗」按钮
我们知道,Vue 遵循「单向数据流」和「组件封装隔离」,子组件内部的方法 / 私有状态默认是对外隐藏的,外部父组件无法直接访问。
而 ref 就是打破这种 “隔离” 的合法方式(非侵入式),让父组件能够:
- 调用子组件通过
defineExpose暴露的方法(如openPopup方法,用于打开弹窗)。 - 访问子组件通过
defineExpose暴露的响应式状态(如弹窗内部的isShow状态)。 - 实现「父组件主动控制子组件」的交互场景(如主动打开 / 关闭弹窗、主动刷新子组件数据)。
<template>
<h1>弹窗学习演示</h1>
<!-- 新增打开弹窗按钮 -->
<button @click="handleOpenPopup">打开弹窗</button>
<BasicPopup ref="popupRef" />
</template>
<script setup>
import { ref } from 'vue';
import BasicPopup from './components/BasicPopup.vue';
// 获取弹窗组件实例
const popupRef = ref(null);
// 打开弹窗的方法(调用子组件的方法,后续步骤会完善)
const handleOpenPopup = () => {
if (popupRef.value) {
popupRef.value.openPopup();
}
};
</script>
<style>
button {
margin: 10px;
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
给 BasicPopup.vue 补充「打开弹窗」方法(暴露给父组件)
<template>
<div class="popup-mask" v-if="isShow">
<div class="popup-content">
<h3>简易弹窗</h3>
<p>这是最基础的弹窗内容</p>
<button @click="closePopup">关闭</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isShow = ref(false); // 初始值改为 false,默认隐藏
const closePopup = () => {
isShow.value = false;
};
// 新增:打开弹窗的方法
const openPopup = () => {
isShow.value = true;
};
// 暴露组件方法,让父组件可以调用(关键)
defineExpose({
openPopup
});
</script>
<style scoped>
/* 样式同前 */
</style>
核心知识点
-
ref创建响应式状态,修改时需要.value -
@click事件绑定,触发自定义方法 -
defineExpose暴露组件内部方法/状态,供父组件调用 -
v-if控制元素的渲染与销毁(实现弹窗显示/隐藏)
步骤 3:父组件 → 子组件传值(Props 传递)
目标:父组件向弹窗组件传递「弹窗标题」和「弹窗内容」,掌握 defineProps 的使用,实现弹窗内容的动态化。
改造 BasicPopup.vue(接收父组件传递的参数)
<template>
<div class="popup-mask" v-if="isShow">
<div class="popup-content">
<!-- 渲染父组件传递的标题 -->
<h3>{{ popupTitle }}</h3>
<!-- 渲染父组件传递的内容 -->
<p>{{ popupContent }}</p>
<button @click="closePopup">关闭</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 1. 定义 Props,接收父组件传递的值(指定类型和默认值)
const props = defineProps({
// 弹窗标题
popupTitle: {
type: String,
default: '默认弹窗标题' // 默认值,防止父组件未传递
},
// 弹窗内容
popupContent: {
type: String,
default: '默认弹窗内容'
}
});
const isShow = ref(false);
const closePopup = () => {
isShow.value = false;
};
const openPopup = () => {
isShow.value = true;
};
defineExpose({
openPopup
});
</script>
<style scoped>
/* 样式同前 */
</style>
改造 App.vue(向子组件传递 Props 数据)
<template>
<h1>弹窗学习演示</h1>
<button @click="openPopup">打开弹窗</button>
<!-- 向子组件传递 props 数据(静态传递 + 动态传递均可) -->
<BasicPopup
ref="popupRef"
popupTitle="父组件传递的标题"
:popupContent="dynamicContent"
/>
</template>
<script setup>
import { ref } from 'vue';
import BasicPopup from './components/BasicPopup.vue';
const popupRef = ref(null);
// 动态定义弹窗内容(也可以是静态字符串)
const dynamicContent = ref('这是父组件通过 Props 传递的动态弹窗内容~');
const openPopup = () => {
if (popupRef.value) {
popupRef.value.openPopup();
}
};
</script>
<style>
button {
margin: 10px;
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
核心知识点
-
defineProps定义组件接收的参数,支持类型校验和默认值 - Props 传递规则:父组件通过「属性绑定」向子组件传值,子组件只读(不能修改 Props,遵循单向数据流)
- 静态传值(直接写字符串)直接把等号后面的内容作为纯字符串传递给子组件的 Props,Vue 不会对其做任何解析、计算,原样传递。动态传值(
:xxx="变量") Vue 会先解析求值,再把结果传递给子组件的 Props。
步骤 4:子组件 → 父组件传值(Emits 事件派发)
目标:弹窗关闭时,向父组件传递「弹窗关闭的状态」和「自定义数据」,掌握 defineEmits 的使用,实现子向父的通信。
改造 BasicPopup.vue(派发事件给父组件)
<template>
<div class="popup-mask" v-if="isShow">
<div class="popup-content">
<h3>{{ popupTitle }}</h3>
<p>{{ popupContent }}</p>
<!-- 关闭按钮点击时,触发事件派发 -->
<button @click="handleClose">关闭</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 1. 定义 Props
const props = defineProps({
popupTitle: {
type: String,
default: '默认弹窗标题'
},
popupContent: {
type: String,
default: '默认弹窗内容'
}
});
// 2. 定义 Emits,声明要派发的事件(支持数组/对象格式,对象格式可校验)
const emit = defineEmits([
'popup-close', // 弹窗关闭事件
'send-data' // 向父组件传递数据的事件
]);
const isShow = ref(false);
// 3. 改造关闭方法,派发事件给父组件
const handleClose = () => {
isShow.value = false;
// 派发「popup-close」事件,可携带参数(可选)
emit('popup-close', {
closeTime: new Date().toLocaleString(),
message: '弹窗已正常关闭'
});
// 派发「send-data」事件,传递自定义数据
emit('send-data', '这是子组件向父组件传递的额外数据');
};
const openPopup = () => {
isShow.value = true;
};
defineExpose({
openPopup
});
</script>
<style scoped>
/* 样式同前 */
</style>
改造 App.vue(监听子组件派发的事件)
<template>
<h1>弹窗学习演示</h1>
<button @click="openPopup">打开弹窗</button>
<!-- 监听子组件派发的事件,绑定处理方法 -->
<BasicPopup
ref="popupRef"
popupTitle="父组件传递的标题"
:popupContent="dynamicContent"
@popup-close="handlePopupClose"
@send-data="handleReceiveData"
/>
</template>
<script setup>
import { ref } from 'vue';
import BasicPopup from './components/BasicPopup.vue';
const popupRef = ref(null);
const dynamicContent = ref('这是父组件通过 Props 传递的动态弹窗内容~');
const openPopup = () => {
if (popupRef.value) {
popupRef.value.openPopup();
}
};
// 1. 处理子组件派发的「popup-close」事件
const handlePopupClose = (closeInfo) => {
console.log('接收弹窗关闭信息:', closeInfo);
alert(`弹窗已关闭,关闭时间:${closeInfo.closeTime}`);
};
// 2. 处理子组件派发的「send-data」事件
const handleReceiveData = (data) => {
console.log('接收子组件传递的数据:', data);
alert(`收到子组件数据:${data}`);
};
</script>
<style>
button {
margin: 10px;
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
核心知识点
-
defineEmits声明组件要派发的事件,遵循「事件名小写+短横线分隔」规范 -
emit方法用于派发事件,第一个参数是事件名,后续参数是要传递的数据 - 父组件通过
@事件名监听子组件事件,处理方法的参数就是子组件传递的数据 - 单向数据流补充:子组件不能直接修改 Props,如需修改,可通过「子组件派发事件 → 父组件修改数据 → Props 重新传递」实现
步骤 5:跨组件传值(非父子组件,使用 Provide / Inject)
目标:实现「非父子组件」(如:Grandpa.vue → Parent.vue → Popup.vue,爷爷组件向弹窗组件传值)的通信,掌握 provide / inject 的使用,这是 Vue 中跨组件传值的核心方案之一。
步骤 5.1:创建层级组件结构
├── App.vue(入口)
├── components/
│ ├── Grandpa.vue(爷爷组件,提供数据)
│ ├── Parent.vue(父组件,中间层级,无数据处理)
│ └── Popup.vue(弹窗组件,注入并使用数据)
步骤 5.2:爷爷组件 Grandpa.vue(提供数据,provide)
<template>
<div class="grandpa">
<h2>爷爷组件</h2>
<button @click="updateGlobalData">修改跨组件传递的数据</button>
<!-- 引入父组件(中间层级) -->
<Parent />
</div>
</template>
<script setup>
import { ref, provide } from 'vue';
import Parent from './Parent.vue';
// 1. 定义要跨组件传递的响应式数据
const globalPopupConfig = ref({
author: 'yyt',
version: '1.0.0',
theme: 'light',
maxWidth: '500px'
});
// 2. 提供(provide)数据,供后代组件注入使用
// 第一个参数:注入标识(字符串/Symbol),第二个参数:要传递的数据
provide('popupGlobalConfig', globalPopupConfig);
// 3. 修改响应式数据(后代组件会同步更新)
const updateGlobalData = () => {
globalPopupConfig.value = {
...globalPopupConfig.value,
version: '2.0.0',
theme: 'dark',
updateTime: new Date().toLocaleString()
};
};
</script>
<style scoped>
.grandpa {
padding: 20px;
border: 2px solid #666;
border-radius: 8px;
margin: 10px;
}
button {
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
步骤 5.3:父组件 Parent.vue(中间层级,仅做组件嵌套)
<template>
<div class="parent">
<h3>父组件(中间层级)</h3>
<button @click="openPopup">打开跨组件传值的弹窗</button>
<!-- 引入弹窗组件 -->
<Popup ref="popupRef" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import Popup from './Popup.vue';
const popupRef = ref(null);
// 打开弹窗
const openPopup = () => {
if (popupRef.value) {
popupRef.value.openPopup();
}
};
</script>
<style scoped>
.parent {
padding: 20px;
border: 2px solid #999;
border-radius: 8px;
margin: 10px;
margin-top: 20px;
}
button {
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
步骤 5.4:弹窗组件 Popup.vue(注入数据,inject)
<template>
<div class="popup-mask" v-if="isShow" :style="{ backgroundColor: themeBg }">
<div class="popup-content" :style="{ maxWidth: globalConfig.maxWidth, background: globalConfig.theme === 'dark' ? '#333' : '#fff', color: globalConfig.theme === 'dark' ? '#fff' : '#333' }">
<h3>{{ popupTitle }}</h3>
<p>{{ popupContent }}</p>
<!-- 渲染跨组件传递的数据 -->
<div class="global-info" style="margin: 15px 0; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
<p>跨组件传递的配置:</p>
<p>作者:{{ globalConfig.author }}</p>
<p>版本:{{ globalConfig.version }}</p>
<p>主题:{{ globalConfig.theme }}</p>
<p v-if="globalConfig.updateTime">更新时间:{{ globalConfig.updateTime }}</p>
</div>
<button @click="handleClose">关闭</button>
</div>
</div>
</template>
<script setup>
import { ref, inject, computed } from 'vue';
// 1. 定义 Props
const props = defineProps({
popupTitle: {
type: String,
default: '默认弹窗标题'
},
popupContent: {
type: String,
default: '默认弹窗内容'
}
});
// 2. 注入(inject)爷爷组件提供的数据
// 第一个参数:注入标识(与 provide 一致),第二个参数:默认值(可选)
const globalConfig = inject('popupGlobalConfig', ref({ author: '默认作者', version: '0.0.1' }));
// 3. 基于注入的数据创建计算属性(可选,优化使用体验)
const themeBg = computed(() => {
return globalConfig.value.theme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)';
});
// 4. 定义 Emits
const emit = defineEmits(['popup-close']);
const isShow = ref(false);
// 5. 关闭方法
const handleClose = () => {
isShow.value = false;
emit('popup-close', { message: '弹窗已关闭' });
};
// 6. 打开弹窗方法
const openPopup = () => {
isShow.value = true;
};
// 7. 暴露方法
defineExpose({
openPopup
});
</script>
<style scoped>
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}
.popup-content {
padding: 20px;
border-radius: 8px;
text-align: center;
}
button {
margin-top: 20px;
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
步骤 5.5:入口 App.vue(引入爷爷组件)
<template>
<h1>跨组件传值演示(Provide / Inject)</h1>
<Grandpa />
</template>
<script setup>
import Grandpa from './components/Grandpa.vue';
</script>
核心知识点
-
provide/inject用于跨层级组件通信(无论层级多深),爷爷组件提供数据,后代组件注入使用 - 传递响应式数据:
provide时传递ref/reactive包装的数据,后代组件可感知数据变化(同步更新) - 注入标识:建议使用
Symbol避免命名冲突(生产环境推荐),本步骤为简化使用字符串标识 - 注入默认值:
inject第二个参数为默认值,防止祖先组件未提供数据时出现报错 - 与 Props/Emits 的区别:Props 适用于父子组件,
provide/inject适用于跨层级组件