前言
在 Vue 里写弹窗、抽屉、Toast、ActionSheet 这类组件时,teleport 是非常常见的能力。它可以把内容渲染到当前组件层级之外,避免被父级的 overflow、z-index 或样式隔离影响。
但小程序不是浏览器,没有真正意义上的 document.body,也不能像 Web 一样随意移动 DOM 节点。所以以前做跨端组件时,经常需要这样写:
html
<!-- #ifdef H5 -->
<teleport to="body">
<popup />
</teleport>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<root-portal>
<popup />
</root-portal>
<!-- #endif -->
从 5.14 开始,小程序端支持直接写 teleport 了。开发者不再需要为了 H5 和小程序写两套条件编译,统一使用 Vue 的写法即可。
html
<teleport to="body">
<popup />
</teleport>
在 H5 端,它还是 Vue 原生的 teleport;在支持的小程序端,uni-app 会自动把它转换成平台原生的 root-portal。
支持平台
目前支持的平台有:
- 微信小程序
- 支付宝小程序
- 京东小程序
使用方式
用法和 Vue 保持一致:
html
<template>
<view class="page">
<button @click="show = true">打开弹窗</button>
<teleport to="body">
<view v-if="show" class="mask" @click="show = false">
<view class="dialog" @click.stop>
我是弹窗内容
</view>
</view>
</teleport>
</view>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>
编译到支持的小程序平台后,模板会变成类似这样:
html
<root-portal>
<view class="mask">
<view class="dialog">
我是弹窗内容
</view>
</view>
</root-portal>
也就是说,开发者写的是 teleport,小程序真正运行的是 root-portal。
实现原理
这次支持本质上是一个编译期转换。
uni-app 在小程序编译阶段会识别模板里的 teleport 标签,并把它转换成 root-portal。这样既保留了 Vue 的开发体验,又能利用小程序平台已有的原生能力。
整体可以理解为:
html
<teleport to="body">
<view />
</teleport>
编译为:
html
<root-portal>
<view />
</root-portal>
需要注意的是,小程序的 root-portal 和 Vue 的 teleport 并不是完全等价的能力。Vue 的 teleport 可以通过 to 指定目标节点,而小程序的 root-portal 是把内容提升到页面根节点下渲染,并不支持传送到任意位置。
所以 uni-app 做的是能力映射,而不是在小程序里完整模拟 Web Teleport。
属性差异
虽然用法统一成了 teleport,但小程序端最终还是会落到 root-portal,所以部分属性会有差异。
to 和 defer
to 和 defer 都是 Vue Teleport 的属性,但小程序 root-portal 没有对应能力,所以编译到小程序端时会被移除。
html
<teleport to="body" defer>
<popup />
</teleport>
会被转换成类似:
html
<root-portal>
<popup />
</root-portal>
如果代码同时跑 H5 和小程序,可以继续保留这些属性:to="body" 和 defer 在 H5 端仍然有意义,只是在小程序端不会生效。
disabled
Vue Teleport 使用 disabled 控制是否禁用传送:
html
<teleport :disabled="disabled">
<popup />
</teleport>
小程序 root-portal 使用的是 enable,语义正好相反。因此编译到小程序时,uni-app 会自动做一次取反转换。
例如:
html
<teleport :disabled="disabled">
<popup />
</teleport>
会被处理成类似:
html
<root-portal :enable="!disabled">
<popup />
</root-portal>
开发者仍然按照 Vue 的 disabled 语义写即可,不需要手动改成 enable。