如何利用 Vue 的 `provide`/`inject` 和 `Teleport`,设计一个可扩展的全局模态框和弹窗管理器?

嘿,大家好!今天咱们来聊聊怎么用 Vue 的 provide/injectTeleport,打造一个牛哄哄的全局模态框和弹窗管理器。保证你用了之后,再也不用为了弹个窗,在组件之间传来传去 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 包含了 openclose 方法。
  • 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。

第五部分:更上一层楼,扩展性增强

这套方案已经能满足大部分需求了,但我们还可以进一步增强它的扩展性。

  1. 默认配置: 在 ModalManager 中提供一个默认配置对象,允许全局配置弹窗的样式、动画等。
// ModalManager.vue
// ...
setup() {
  const defaultConfig = {
    overlayClass: 'modal-overlay',
    contentClass: 'modal-content',
    animationDuration: 300,
  };

  const openModal = (comp, options = {}) => {
    const mergedOptions = { ...defaultConfig, ...options }; // 合并配置
    // ... 使用 mergedOptions
  };
}
//...
  1. 插槽: 在 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>
  1. 多个弹窗实例: 如果需要同时显示多个弹窗,可以考虑创建多个 ModalManager 实例,或者修改 ModalManager 的逻辑,使用一个数组来管理多个弹窗。

第六部分:高级技巧,Promise 的妙用

咱们再深入一点,聊聊 Promise 的使用技巧。

  1. 取消弹窗: 有时候,我们可能需要在弹窗打开后,允许用户取消弹窗。可以在 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>
  1. 超时关闭: 可以设置一个定时器,如果弹窗在一定时间内没有关闭,就自动关闭它。
// 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/injectTeleport,设计一个可扩展的全局模态框和弹窗管理器。

总结一下:

技术点 作用
provide/inject 实现全局状态管理,让任何组件都可以访问弹窗服务。避免了 props 逐层传递的麻烦。
Teleport 将弹窗内容渲染到 body 元素的末尾,避免被父组件的样式或者 overflow: hidden 之类的属性影响。
Promise 确保弹窗关闭后,调用者可以拿到返回值。 可以用来实现弹窗的取消,超时关闭等功能。
插槽 提供弹窗的默认内容。

希望这套方案能帮助你更好地管理 Vue 应用中的弹窗。记住,代码只是工具,关键在于理解背后的思想。掌握了这些思想,你就能灵活运用各种技术,解决实际问题。

好了,今天的讲座就到这里。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注