Vue2 的 <transition><transition-group><keep-alive> 我们都很熟。到了 Vue3,API 更强大,还新增了 Teleport(传送门),可以把弹窗、抽屉挂到 body。今天我们用动画+弹窗的案例,教你快速迁移旧项目的过渡逻辑,并掌握 Teleport 的高级用法。

🎯 需求

  • 列表项淡入淡出。
  • 弹窗从按钮“传送”到 body。
  • 多个弹窗共享遮罩,支持滚动穿透控制。

🧱 基础:transition

<template>
  <div>
    <button @click="show = !show">切换</button>
    <Transition name="fade">
      <p v-if="show">你好,这是一段测试文字</p>
    </Transition>
  </div>
</template>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}
</style>
  • Vue3 里还是用 name + -enter-from/-enter-active 写法。
  • 想要 JS 钩子:@before-enter@after-leave 等。

🧠 transition-group

<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">
    {{ item.text }}
  </li>
</TransitionGroup>
  • 必须提供唯一 key。
  • Vue3 默认使用 v-move class 增加移动过渡。
  • CSS:
    .list-enter-active, .list-leave-active {
      transition: all 0.3s;
    }
    .list-enter-from, .list-leave-to {
      opacity: 0;
      transform: translateY(10px);
    }
    .list-move {
      transition: transform 0.3s;
    }
    

🚪 Teleport:弹窗传送门

<template>
  <Teleport to="body">
    <div v-if="visible" class="modal-mask">
      <div class="modal">
        <slot />
        <button @click="$emit('close')">关闭</button>
      </div>
    </div>
  </Teleport>
</template>
  • to="body" 把内容挂到 body。
  • 适合弹窗、通知、Tooltip。
  • 接收多个 Teleport 目标:<Teleport to="#portal-root">

🔄 Teleport + Transition 结合

<Teleport to="body">
  <Transition name="fade-scale">
    <div v-if="visible" class="modal-mask">
      <div class="modal">
        <slot />
      </div>
    </div>
  </Transition>
</Teleport>
  • 传送后仍然保留过渡动画。
  • 可使用 appear 属性让初次渲染也有动画。

🧰 滚动穿透控制

watch(visible, (val) => {
  document.body.style.overflow = val ? 'hidden' : '';
});

onUnmounted(() => {
  document.body.style.overflow = '';
});
  • Teleport 出去的元素不受父容器限制,所以要手动控制滚动。

🧯 常见坑

现象 解决
Teleport 目标不存在 控制台警告 确保 DOM 中存在对应 selector
transition-group 动画乱跳 未设置固定高度 给列表项定义高度或使用 flex
动画重复触发 key 重复或丢失 确保唯一 key
Teleport 内容没继承样式 父级作用域样式未作用 使用全局样式或 :global()

🧱 keep-alive 搭配

<KeepAlive include="TabA,TabB">
  <component :is="activeTab" />
</KeepAlive>
  • <script setup> 中使用:
    <KeepAlive>
      <component :is="currentView" />
    </KeepAlive>
    
  • 配合 Teleport 时保持状态:例如 Tab 里的弹窗不丢失内容。

🏁 小练习

  1. 把项目里的弹窗改用 Teleport,解决遮罩层被父元素遮挡的问题。
  2. 写一个 useModal 组合式函数,控制 Teleport 弹窗的显示/关闭。

下一篇,我们将讨论“构建工具迁移”:Vue3 项目用 Vite 替代 Vue CLI/webpack,需要改哪些配置?怎么处理别名、env、跨域代理?一次说清楚。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐