一、背景:为什么需要"传送门"
在 Vue 组件树体系中,子组件的 DOM 默认始终嵌套在父组件内部。然而,许多场景要求我们将内容“搬运”到组件树之外的指定 DOM 节点进行渲染——例如模态框、对话框,它们通常依赖 position: fixed 定位,却常因父级 overflow:hidden、transform 或 z-index 样式而产生布局冲突。此外,Toast 通知、全局提示、浮层菜单、下拉列表、Tooltip 提示框,乃至跨布局区域的内容注入(如将某块内容“投递”到页脚插槽),都面临类似需求。

这类需求的核心逻辑十分清晰:逻辑归属仍属于原组件,但渲染位置需要脱离当前组件树——这便是“传送门(Portal)模式”的典型应用场景。
二、Vue2 方案:portal-vue 库
Vue2 官方并未内置传送门功能,社区中公认的解决方案是 portal-vue 库(v2 版本适配 Vue2)。该库提供了一对协同工作的组件:
| 组件 | 作用 |
|---|---|
| 发送端,将包裹的内容“投递”至指定目标位置 |
| 接收端,在其所在位置渲染接收到的内容 |
1. 基本用法
我是被传送至外部的弹窗
与 通过相同的字符串名称进行匹配, 所在位置即为内容渲染的目标区域。
2. portal-target 常用配置
| 属性 | 说明 |
|---|---|
name | 目标名称,需全局唯一(除非启用 multiple) |
multiple | 允许多个 portal 同时向同一 target 投递内容,内容按顺序依次拼接 |
slim | 设为 true 时不渲染额外的 wrapper 元素,直接使用 slot 的单一根节点 |
tag | 指定 wrapper 渲染的标签类型(默认 div),可设为空字符串配合 slim 使用 |
order | 多个 portal 投递时的排序权重,控制内容先后顺序 |
slotProps / slotTag | 当 source 使用 scoped slot 时的透传配置项 |
默认情况下,一个 target 只能接收最后一个 portal 的内容(后到达的会覆盖先到达的),若需接收多个 portal 的内容,必须显式设置 multiple:
3. Vue2 + portal-vue 的典型痛点
- 强依赖第三方库:
portal-vue体积相对较大,且需与 Vue2 版本严格匹配(v2 库对应 Vue2,v3 库对应 Vue3),版本升级成本较高。 - SSR 需特殊处理:服务端渲染时,portal 内容默认不会出现在 HTML 中(库提供了
SSRegress等机制),配置较为繁琐,容易踩坑。 - 调试困难:内容在组件树 A 中定义,却渲染在组件树 B 的节点中,Vue DevTools 中父子关系被割裂,定位问题需跨树查找。
- wrapper 嵌套:
portal-target默认会包裹一层,可能破坏预期的样式效果(需通过slim规避),且多个 portal 时 DOM 结构不够直观。 - 单根限制:Vue2 模板要求单一根节点,在配合传送编写复杂内容时不够灵活。
- 响应式/重渲染逻辑隐式:portal 内容的更新依赖库的订阅机制,偶尔会出现“内容不刷新”的问题,需要排查 target 的注册时机。
三、Vue3 方案:内置
Vue3 官方内置了 组件,将“传送门”作为一等公民直接纳入框架,从根本上解决了上述痛点。
1. 基本用法
我会被直接渲染到
标签下
to 属性可以是任意 CSS 选择器(例如 "#modal-root"、"body"、".some-class"),也可以是一个真实的 HTMLElement 对象。
2. Vue3 的针对性优化点
| 优化维度 | Vue2 portal-vue | Vue3 Teleport |
|---|---|---|
| 是否内置 | 第三方库,需独立维护 | 框架原生,零额外依赖 |
| 多源合并 | 需显式开启 multiple | 天然支持,多个 自动按顺序追加 |
| 条件禁用 | portal 侧无内建 disabled(v3 库才有) | 内建 :disabled 属性,为 true 时内容留在原位渲染 |
| SSR | 需额外配置 SSRegress | 官方内建 SSR 支持,无需额外 hack |
| 调试 | 跨树割裂,需手动查找 target | DevTools 中保持逻辑父子关系,来源清晰可追溯 |
| wrapper | portal-target 默认包裹一层 div | 不引入任何额外 DOM 包裹节点 |
| 根节点限制 | 受 Vue2 单根约束 | Vue3 Fragment 支持,内容可多根节点 |
| 体积/性能 | 库级运行时开销 | 编译期优化,运行时极轻量 |
3. 进阶能力
defer(Vue 3.1+):延迟到组件挂载后再查找目标元素,有效解决“目标节点在初始渲染时尚未存在”的问题(典型场景:目标由其他异步组件动态创建)。
目标节点尚未出现,挂载完成后再搬移过去
多 Teleport 同名合并:多个 不会相互覆盖,而是依次成为 body 的子节点,效果等同于 portal-vue 开启 multiple,但无需任何额外配置。
禁用传送保留原位渲染:
...
四、迁移建议(Vue2 → Vue3)
- 卸载
portal-vue,移除所有和。 - 发送端改写:将
替换为(注意to仍是名称字符串,但表示选择器,建议改为真实 DOM 选择器如#x更直观)。 - 接收端删除:
不再需要——直接将内容渲染到to指向的 DOM 节点,无需显式声明 target 占位符。 - 多源场景:原先依赖
multiple拼接的内容,直接编写多个即可,删除multiple属性。 - 条件渲染:原先通过动态
to或销毁 portal 实现“留在原位”的逻辑,改用:disabled属性。 - SSR 项目:移除
SSRegress相关配置,Teleport 已原生支持服务端渲染。 - slim/wrapper 清理:原来为消除包裹
div而使用的slim配置可删除,Teleport 本身不引入任何包裹节点。
迁移对照表
hi
hi
五、一句话总结
从 Vue2 的 portal-vue 到 Vue3 内置的 ,不仅仅是名称的变更,更是一次从“第三方补丁”到“框架原生能力”的质的飞跃。迁移时只需去掉发送端与接收端的成对组件,改用 Teleport to 直接指向目标 DOM 节点,再配合 disabled、defer 等内建特性,整个传送门的使用体验终于变得清爽、可控且无副作用。
