You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Vue warn]: Failed to locate Teleport target with selector "#teleportId". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.
背景
传送门 的作用是将组件渲染到 DOM 树的任意位置,从而摆脱当前组件树的层次结构。常用于制作弹窗、弹出层等,通常 UI 框架 已经帮我们做了这部分工作( 比如渲染到
body
下 ),所以项目中很少用到。Teleport
Portal
等效代码:
Taro 在文档中是这么描述的:
不支持 Vue3
Teleport
不支持 React
Portal
跑了文档中的示例项目之后发现 Teleport / Portal 的基本功能都是支持的,可以满足将组件渲染到当前页面中的某个节点中。
不明白 跨页面的全局组件 的意义是什么( 难道是浮窗按钮? ),毕竟一个屏幕下只能同时显示一个页面的内容,将 A 页面中某个组件渲染到 B 页面中也看不见,意义不大。如果真有这样的需求,我觉得 页面级全局组件 再配合 状态管理工具( Redux 、 Pinia 等 )也能实现跨页面后台展示的效果。
需要用到 Teleport / Portal 的场景
一般我们会使用
position: fixed
来实现悬浮在某个位置的效果,不使用 Teleport / Portal 也能用,但是组件多了之后z-index
的层级问题就不好控制了。层级问题还是其次,更关键的是
fixed
在一些场景下会失效降级为absolute
:一个列表左滑删除的例子:左滑显示删除按钮,点击删除显示确认删除的弹窗。
滑动组件 带有
transform
样式导致弹窗组件的fixed
失效,为了修复这个问题只能将弹窗组件写在滑动组件外部,这时封装ListItem
组件会非常麻烦,要通过事件向上传递和弹窗组件进行通讯。项目中这样的场景不在少数,如果组件树中某个中间节点增加了
transform
样式就需要重新梳理组件结构了。如果能将
fixed
组件直接渲染到外部的话,就完全不需要考虑这方面问题了。整合思路与遇到的问题
封装传送门组件
主要是对内置的 Teleport / Portal 组件做了一层简单封装,因为 Taro 是跨平台框架,各端实现有所差异,所以需要在这一层做兼容处理。
组件提供
enable
、target
和root
三个属性,其中enable
用于控制是否从页面中脱离出来,剩下的属性用于控制渲染逻辑:指定了
target
且值非空时,渲染到指定的节点上,可以是一个 DOM 元素对象或者其 id 。当
root
值为'first'
时,渲染到页面根节点的第一个子节点。当
root
值为true
时,渲染到页面根节点。当外层用传送门组件的
Provider
包裹时,渲染到Provider
中提供的节点上。缺省渲染到页面根节点。
封装 UI 框架的弹窗组件
本文中使用的 UI 框架 是 NutUI ,正好 Vue 和 React 两个版本都支持。包装一下
Popup
组件使其默认就渲染到页面根节点的第一个子节点上,这样使用的时候就会省事很多。获取用于渲染的节点
使用
ref
语法来获取节点。由于 不同平台不同框架 ref 获取到的节点类型不同 ,这种方式的可靠性还有待验证。
使用
document.getElementById
DOM API 来获取节点。这种方式的限制就是需要保证组件 id 全局( 所有页面 )唯一( 参考 ):
H5 端 多页应用每个页面是用
div
模拟的,如果 id 不唯一就会获取到其他页面上的节点,导致失效。小程序端
getElementById
是通过全局的eventSource
实现的。组件卸载的时候会调用
eventSource.removeNodeTree
将组件对应的 id 从eventSource
中移除( 参考 ),这就导致一个问题: 如果两个页面中都存在 id 为teleportId
的组件,切换到下一页再后退回来,就会发现当前页面无法通过这个 id 获取到组件了 。Taro 文档中提供的示例项目 taro-vue-teleport 就有这个问题,其中
teleport
的v-if
和showModal
绑定了,也就是说每次关闭弹窗再打开弹窗会创建新的teleport
组件,导致每次都会重新调用一遍resolveTarget
,再结合重复 id 的问题就会得到 下面的错误 :[Vue warn]: Failed to locate Teleport target with selector "#teleportId". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.
应该避免
teleport
的重复卸载创建,卸载teleport
还可能会导致 slot 中的一些事件无法触发。比如下面这个例子中,打开弹窗后点击遮罩没法关闭的,只能点击自定义的关闭按钮才行。当然,不绑定
v-if
还是会报错:因为首次渲染完成前无法获取到 DOM 元素对象,需要延迟渲染
teleport
:说起保证 id 唯一的方法,我看一些项目中用到 随机数 来作为 id ,但这种方式还是无法完全避免重复,其实 Taro 中已经提供 自增 id 的算法,直接拿来用就好了,具体参考下面代码中的
nextTeleportId
。获取页面根节点
由于 H5 端 多页应用每个页面是用
div
模拟的,如果直接渲染到body
或者#app
( 小程序中没有的 )上,不同页面中的组件放在一起,样式效果容易打架。 每个页面的组件应该只渲染在当前页面所属的div
下面,不要越界。Taro 内部实现了一层 Page 组件作为页面的根节点,我们在项目代码中没法直接对它进行修改。所幸 Page 组件都是有 id 的,也就是 当前页面的路由路径 ( 参考 ),有了 id 就能拿到页面根节点并渲染到上面,开箱即用也省得要自己手动埋点了。
不过这个 id 直接用到
teleport
中是会报错的:因为
teleport
内部用到了document.querySelector
,而 H5 端querySelector
的参数不能包含一些特殊字符。然而同样的 id 使用getElementById
是不会报错的。模拟报错效果:
解决办法:使用
CSS.escape
进行转义( 参考 )在 Vue 中使用 Teleport
biz-teleport.vue
constants.ts
hooks.ts
Note
其中
injectTaroPageId
目前还用不上,如果项目中只是为了获取页面组件的 id ,用这个注入的方式更好。biz-popup.vue
用法示例
对比了使用
Teleport
前后的效果,使用biz-popup
更简单。默认渲染到页面根节点( 或者其第一个子节点 ),要实现渲染到自定义节点需要进一步改造。
biz-teleport-provider.vue
提供一个用于渲染的节点,并将其 id 通过 依赖注入 的方式传递给子组件。这样在子组件中使用
biz-teleport
就能自动渲染到这个节点上。当前也可以使用
ref
获取节点,然后传递给biz-teleport
:注意使用
ref
的方式,在 H5 端需要使用div
而不能用 Taro 内置的view
,否则会报错:完整代码
👉 commit anyesu/taro-demo@
f4511d4
在 React 中使用 Portal
其中createPortal
是从@tarojs/react
包导入的,对比react-dom
中的实现,主要的区别是少了 校验 并对Symbol.for
做了兼容处理。@tarojs/react
是小程序专用的 ,由于 过于精简 ,用在 H5 端 反而会引起一些错误。并且@tarojs/plugin-framework-react
插件针对 小程序端 专门做了一层alias
,将react-dom
导入映射为@tarojs/react
,所以在项目中直接统一使用react-dom
就好了。biz-portal.tsx
hooks.ts
biz-popup.tsx
用法示例
完整代码
👉 commit anyesu/taro-demo@
47e4ce8
( 修正 )其他相关问题
在 Vue 单文件组件( SFC ) 中使用 JSX
对应 Vue 版本的用法示例中的
Demo
组件。只是单纯不想多创建文件,写法上繁琐很多,也缺少语法提示,平时不建议用。
需要将
<script>
标签上的lang
属性设置为jsx
或者tsx
( 否则 prettier 会报错 ):除了 Taro 内置组件 ( 比如
View
)需要 手动导入 外其他组件可以 自动按需引入 ,然后将事件绑定改为 onCamelcase 格式的属性写法,其他的组件名和属性名都可以写成 kebab-case 格式的。[email protected]+
)在 Vue 中扩展已有的组件
对应 Vue 版本的
biz-popup
组件。其属性通过继承
nut-popup
的属性得到完整的类型提示,然后通过/* @vue-ignore */
注释避免了biz-popup
的 运行时声明 包含属于nut-popup
的属性,这样就可以直接 透传 给nut-popup
而无需做额外处理。在 React 中使用 Vue 中的 作用域插槽 用法
对应 React 版本的用法示例中的
Demo
组件。( 参考 )React Hooks 的执行顺序
一直以来只是拿
useEffect
来模拟 class 组件的生命周期 ( 生命周期图谱 ),没怎么了解过其他 Hook 的执行顺序,跑个 demo 测试下:运行结果:
微信开发者工具中
fixed
失效时页面闪烁的问题微信开发者工具 升级到目前最新的
1.06.2402040
版本还是有问题。 真机测试没问题。复现步骤:
fixed
失效的弹窗解决办法:
初步排查是祖先元素同时设置了
overflow: hidden
和border-radius
导致的,把hidden
取消掉或者border-radius
设置为0
都能解决这个闪烁问题,猜测是fixed
降级为absolute
时圆角裁剪有问题。演示效果:
源码
完整项目代码 👉 anyesu/taro-demo
获取源代码
$ git clone https://github.com/anyesu/taro-demo $ cd taro-demo
安装依赖
运行项目
浏览器访问: http://127.0.0.1:10086
结语
最初只是想写个 demo 简单记录下,结果拔出萝卜带出泥,越是深入了解坑踩得越多,不过也收获了很多,也是应证了学无止境那句话。
转载请注明出处: https://github.com/anyesu/blog/issues/51
The text was updated successfully, but these errors were encountered: