嘿,大家好!今天咱们来聊聊怎么用 Vue 的 provide
/inject
和 Teleport
,打造一个牛哄哄的全局模态框和弹窗管理器。保证你用了之后,再也不用为了弹个窗,在组件之间传来传去 props 搞得晕头转向了。
咱们的目标是:组件想弹窗,就像对着麦克风喊一声“芝麻开门”一样简单!
第一部分:搭框架,Provide/Inject 上场
首先,我们需要一个全局的“管家”来管理所有的弹窗。这个管家就是我们的 ModalManager
组件。它会提供(provide
)一些方法,让其他组件可以注入(inject
)并使用。
// ModalManager.vue
<template>
<div>
<!-- Teleport 元素,将弹窗内容渲染到 body 底部 -->
<teleport to="body">
<div v-if="visible" class="modal-overlay">
<div class="modal-content">
<component :is="component" v-bind="props" @close="closeModal" />
</div>
</div>
</teleport>
</div>
</template>
<script>
import { ref, provide, shallowRef, onMounted } from 'vue';
export default {
name: 'ModalManager',
setup() {
const visible = ref(false);
const component = shallowRef(null); // 使用 shallowRef 避免深度响应式
const props = ref({});
let modalResolve; // 用于 Promise 的 resolve
const openModal = (comp, options = {}) => {
return new Promise((resolve) => {
visible.value = true;
component.value = comp;
props.value = options.props || {};
modalResolve = resolve; // 保存 resolve 函数
console.log('Modal opened with component:', comp.name || comp);
});
};
const closeModal = (result) => {
visible.value = false;
component.value = null;
props.value = {};
if (modalResolve) {
modalResolve(result); // 将结果传递给 Promise
modalResolve = null; // 清空 resolve 函数
}
console.log('Modal closed');
};
const modalService = {
open: openModal,
close: closeModal,
};
provide('modal', modalService);
onMounted(() => {
console.log('ModalManager mounted and providing modal service.');
});
return {
visible,
component,
props,
closeModal,
};
},
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
}
</style>
代码解释:
Teleport to="body"
: 这是关键!它能把我们的弹窗内容“传送”到body
元素的末尾,避免被父组件的样式或者overflow: hidden
之类的属性影响。visible
: 控制弹窗的显示和隐藏。component
: 要渲染的组件。注意这里使用了shallowRef
,因为我们只需要组件的引用,不需要深度响应式追踪。props
: 传递给组件的 props。openModal(comp, options)
: 打开弹窗的方法。接收一个组件comp
和一个可选的options
对象,其中options.props
用于传递 props。返回一个 Promise,允许你在弹窗关闭后获取结果。closeModal(result)
: 关闭弹窗的方法。可以传递一个result
参数,这个参数会被传递给openModal
返回的 Promise。provide('modal', modalService)
: 把modalService
对象提供出去,让其他组件可以注入。modalService
包含了open
和close
方法。Promise
: 使用 Promise 确保弹窗关闭后,调用者可以拿到返回值. 并且避免了回调地狱。
第二部分:注册管家,全局生效
接下来,我们需要在 Vue 应用中注册 ModalManager
组件,让它成为全局的“管家”。
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import ModalManager from './components/ModalManager.vue';
const app = createApp(App);
app.component('ModalManager', ModalManager); // 全局注册 ModalManager
app.mount('#app');
在 App.vue
模板中使用 ModalManager
:
// App.vue
<template>
<div>
<ModalManager />
<HelloWorld msg="Hello Vue 3 + Vite" />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
components: {
HelloWorld
}
}
</script>
第三部分:注入服务,随心所欲弹窗
现在,任何组件都可以注入 modal
服务,并使用 open
方法来打开弹窗。
// SomeComponent.vue
<template>
<button @click="openMyModal">打开我的弹窗</button>
</template>
<script>
import { inject } from 'vue';
import MyModal from './MyModal.vue'; // 引入你的弹窗组件
export default {
name: 'SomeComponent',
setup() {
const modal = inject('modal');
const openMyModal = async () => {
try {
const result = await modal.open(MyModal, {
props: {
message: '你好,我是弹窗传递过来的消息!',
},
});
console.log('弹窗返回的结果:', result);
} catch (error) {
console.error('打开弹窗出错:', error);
}
};
return {
openMyModal,
};
},
};
</script>
代码解释:
inject('modal')
: 注入我们在ModalManager
中提供的modalService
。openMyModal()
: 一个异步函数,用于打开弹窗。modal.open(MyModal, { props: { ... } })
: 调用modal
服务的open
方法,传入要显示的组件MyModal
和一个options
对象。options.props
用于传递 props 给MyModal
组件。await modal.open(...)
: 等待弹窗关闭,并获取弹窗返回的结果。
第四部分:打造你的弹窗组件
MyModal.vue
只是一个普通的 Vue 组件,但它需要一个 close
事件来通知 ModalManager
关闭弹窗。
// MyModal.vue
<template>
<div class="my-modal">
<h2>{{ message }}</h2>
<button @click="closeWithResult('确定')">确定</button>
<button @click="closeWithoutResult">取消</button>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'MyModal',
props: {
message: {
type: String,
required: true,
},
},
emits: ['close'], // 声明 close 事件
setup(props, { emit }) {
const closeWithResult = (result) => {
emit('close', result); // 触发 close 事件,并传递结果
};
const closeWithoutResult = () => {
emit('close'); // 触发 close 事件,不传递结果
};
return {
closeWithResult,
closeWithoutResult,
};
},
});
</script>
<style scoped>
.my-modal {
border: 1px solid #ccc;
padding: 20px;
border-radius: 5px;
}
</style>
代码解释:
props: { message: { ... } }
: 定义一个message
prop,用于接收来自父组件的消息。emits: ['close']
: 声明组件会触发close
事件。emit('close', result)
: 触发close
事件,并传递一个result
参数。这个参数会被传递给ModalManager
,最终传递给openModal
返回的 Promise。
第五部分:更上一层楼,扩展性增强
这套方案已经能满足大部分需求了,但我们还可以进一步增强它的扩展性。
- 默认配置: 在
ModalManager
中提供一个默认配置对象,允许全局配置弹窗的样式、动画等。
// ModalManager.vue
// ...
setup() {
const defaultConfig = {
overlayClass: 'modal-overlay',
contentClass: 'modal-content',
animationDuration: 300,
};
const openModal = (comp, options = {}) => {
const mergedOptions = { ...defaultConfig, ...options }; // 合并配置
// ... 使用 mergedOptions
};
}
//...
- 插槽: 在
ModalManager
中使用插槽,允许自定义弹窗的内容区域。
// ModalManager.vue
<template>
<div>
<teleport to="body">
<div v-if="visible" class="modal-overlay">
<div class="modal-content">
<slot v-if="!component" /> <!-- 如果没有指定组件,则渲染插槽内容 -->
<component v-else :is="component" v-bind="props" @close="closeModal" />
</div>
</div>
</teleport>
</div>
</template>
使用插槽:
// App.vue
<template>
<div>
<ModalManager>
<!-- 默认弹窗内容 -->
<h1>这是一个默认弹窗</h1>
<p>你可以自定义这里的内容。</p>
</ModalManager>
<HelloWorld msg="Hello Vue 3 + Vite" />
</div>
</template>
- 多个弹窗实例: 如果需要同时显示多个弹窗,可以考虑创建多个
ModalManager
实例,或者修改ModalManager
的逻辑,使用一个数组来管理多个弹窗。
第六部分:高级技巧,Promise 的妙用
咱们再深入一点,聊聊 Promise 的使用技巧。
- 取消弹窗: 有时候,我们可能需要在弹窗打开后,允许用户取消弹窗。可以在
openModal
方法中添加一个cancel
方法,用于拒绝 Promise。
// ModalManager.vue
setup() {
let modalResolve;
let modalReject; // 用于 reject Promise
const openModal = (comp, options = {}) => {
return new Promise((resolve, reject) => {
visible.value = true;
component.value = comp;
props.value = options.props || {};
modalResolve = resolve;
modalReject = reject; // 保存 reject 函数
});
};
const closeModal = (result) => {
if (modalResolve) {
modalResolve(result);
modalResolve = null;
modalReject = null;
}
visible.value = false;
component.value = null;
props.value = {};
};
const cancelModal = (reason) => {
if (modalReject) {
modalReject(reason); // 拒绝 Promise
modalResolve = null;
modalReject = null;
}
visible.value = false;
component.value = null;
props.value = {};
};
const modalService = {
open: openModal,
close: closeModal,
cancel: cancelModal, // 添加 cancel 方法
};
}
在组件中使用 cancel
方法:
// MyModal.vue
<template>
<div>
<button @click="cancel">取消</button>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const modal = inject('modal');
const cancel = () => {
modal.cancel('用户取消了弹窗');
};
return {
cancel,
};
},
};
</script>
- 超时关闭: 可以设置一个定时器,如果弹窗在一定时间内没有关闭,就自动关闭它。
// ModalManager.vue
setup() {
const openModal = (comp, options = {}) => {
return new Promise((resolve, reject) => {
// ...
const timeout = options.timeout || 5000; // 默认超时时间为 5 秒
const timer = setTimeout(() => {
cancelModal('弹窗超时');
}, timeout);
// 重写 closeModal 和 cancelModal 方法,清除定时器
const originalCloseModal = closeModal;
closeModal = (result) => {
clearTimeout(timer);
originalCloseModal(result);
};
const originalCancelModal = cancelModal;
cancelModal = (reason) => {
clearTimeout(timer);
originalCancelModal(reason);
};
});
};
}
第七部分:总结,弹窗管理的葵花宝典
咱们今天讲了用 Vue 的 provide
/inject
和 Teleport
,设计一个可扩展的全局模态框和弹窗管理器。
总结一下:
技术点 | 作用 |
---|---|
provide /inject |
实现全局状态管理,让任何组件都可以访问弹窗服务。避免了 props 逐层传递的麻烦。 |
Teleport |
将弹窗内容渲染到 body 元素的末尾,避免被父组件的样式或者 overflow: hidden 之类的属性影响。 |
Promise |
确保弹窗关闭后,调用者可以拿到返回值。 可以用来实现弹窗的取消,超时关闭等功能。 |
插槽 | 提供弹窗的默认内容。 |
希望这套方案能帮助你更好地管理 Vue 应用中的弹窗。记住,代码只是工具,关键在于理解背后的思想。掌握了这些思想,你就能灵活运用各种技术,解决实际问题。
好了,今天的讲座就到这里。下次再见!