- 开始时间:2020-01-10
- 目标主要版本:3.x
- 引用 issue:N/A
- 实现的 PR:N/A
摘要
- 为 Vue core 添加一个
组件。 - 该组件需要一个目标元素,通过一个期望 HTMLElement 或 querySelector 字符串的 prop 提供。
- 该组件将其子元素移动到 DOM 选择器所确定的元素上。
- 在虚拟 DOM 层面上,虽然子元素是
的后代,但它们可以访问来自其祖先的注入。
基本范例
<body><div id="app"><h1>Move the #content with the portal component</h1><teleport to="#endofbody"><div id="content"><p>this will be moved to #endofbody.<br />Pretend that it's a modal</p><Child /></div></teleport></div><div id="endofbody"></div><script>new Vue({el: "#app",components: {Child: { template: "<div>Placeholder</div>" }}});</script></body>
这将导致以下行为:
<teleport>的所有孩子 —— 在这个例子中。<div id="content">和<Child />—— 将被添加到<div id="endofbody" >中。- 作为这些节点之一的
<Child />组件将保持为<teleport>的父节点的子节点(<teleport>是透明的)。<div id="app"><!-- --></div><div id="endofbody"><div id="content"><p>this will be moved to #endofbody.<br />Pretend that it's a modal</p><div>Placeholder</div></div></div>
动机
Vue 鼓励我们通过将 UI 和相关行为封装到组件中来构建我们的 UI,我们可以将这些组件相互嵌套,从而构建一个组件树,构建成你的应用程序 UI。这种模式在 Vue 和其他框架中得到了很多证明,但本 RFC 试图解决一个弱点:
有时候,一个组件的一部分模版在逻辑上属于这个组件,而从技术角度上来看(即:样式要求),最好把这部分模版移到 DOM 的其他地方,把它从 DOM 树中的深度嵌套位置打破。
用例
z-index
这种行为的主要用例通常与样式有关。各种常见的 UI 模式,如模态、对话框、下拉菜单、通知等,需要 fixed 或者 absolute 定位和管理它们的 z-index。
为了解决 z-index 堆叠上下文 行为的问题,一个常见的模式是将这些组件的 DOM 元素放在 标签之前,以便将它们移出任何父元素的 z-index 堆叠上下文。
Widgets
很多应用程序都有 widgets 的概念,其中它们的 UI 有一个出口(即,在侧边栏或仪表盘中),应用程序的其他部分(即插件),可以注入一小块的 UI。
在单页应用程序中,我们的 JavaScript 基本上控制了整个页面,这通常不是一个挑战。但是在我们的 Vue 应用中只控制页面的一部分的情况下,目前证明在页面的其他部分挂载单个元素和组件是有挑战的(但不可能)。
有了 <teleport>,我们就有了一种直接的方法来将子组件安装到 DOM 的其他位置。
具体设计
作为内部组件实现
<teleport> “组件” 是一个像 <trasition> 和 <keep-alive> 一样的内部组件。它是 tree-shakable 的,所以如果你不使用这个功能,这个组件的代码就会从最终的 bundle 中删除。
在模版中使用
在模版中,编译器会在生成的代码中添加 <teleport> 组件的导入,所以可以像这样使用:
export default {template: `<div><teleport to="#endofbody">Some content.</teleport><div>`};
当使用渲染函数或 JSX 时,组件必须先导入,就像任何其他组件一样:
import { Teleport, h } from "vue";export default {render() {return h("div", [h(Teleport, { to: "#endofbody" }, ["Some content"])]);},// or with JSX:render() {<div><Teleport to="#endofbody">Some content</Teleport></div>;}};
在统一目标上使用多个 portals
一个常见的案例场景时一个可重用的 append —— 在目标元素中,后面的挂载将位于前面的挂载之后。
<teleport to="#modals"><div>A</div></teleport><teleport to="#modals"><div>B</div></teleport><!-- result--><div id="modals"><div>A</div><div>B</div></div>
在迄今为止关于这个 RFC 的讨论中,讨论了更加复杂的行为(可选的预置,定义顺序 ……),但对复杂性的担忧和可预见的 SSR 和 hydration lead 问题导致我们将这种行为限制在简单的 append 上。
Props
to
该组件只有一个必要的 prop,名为 to。它接受一个字符串,它必须是一个有效的 query 选择器,或者一个 HTMLElement(如果在浏览器环境中使用)。
<!-- ok --><teleport to="#some-id" /><teleport to=".some-class" /><teleport to="[data-portal]" /><!--probably too unspecific, but technically validshould we allow this or block it?--><teleport to="h1" /><!-- Wrong --><teleport to="some-string" />></teleport>
disable
这个可选的 prop 可以用来禁用 portal 的功能,这意味着它的插槽内容不会被移动到任何地方,而是在你指定的 <teleport> 的周围父组件中呈现。
<teleport to="#popup" :disabled="displayVideoInline"><video src="./my-movie.mp4"></teleport>
动态改变它的值允许在 to prop 指定的目标和周围父组件中的实际位置之间移动相同的 DOM 元素。这意味着 <teleport> 内的任何组件都将保持 alive 并保持其内部状态。同样,<video> 元素将会在这些位置之间移动时保持其播放状态。
声明周期
Mounting
当 <teleport> 组件被它的父组件挂载时,它将使用 to 属性的值作为选择器。
- 如果 query 返回一个元素,
<teleport>的插槽将作为该元素的子节点挂载在 DOM 中。 - 如果在这个
<teleport>被挂载的时候,这个元素在 DOM 中不存在,那么在开发过程中会产生一个类似下面的警告(在生产环境中不会发生什么)。``javascriptTeleport content could not be mounted to element with selector ‘${props.to}’: element not found.
// following would be a display where in the component tree this happened etc.
<a name="Avudi"></a>#### $parent如果 `<teleport>` 的子节点包含任何组件,它们的 `this.$parent` 属性应该引用 `<teleprot>` 的父组件。换句话说,这些组件会留在它们在组件树中的原始位置,即使它们最后被挂载在 DOM 树的其他地方。`<teleport>` 根本不是一个真正的组件,它是透明的,不会在 $parent 链中作为祖先出现。```vue<template><teleport v-bind:to="targetName"><Child /></teleport></template><script>export default {name: 'Parent'components: {Child: {template: '<div/>',mounted() {console.log(this.$parent.$options.name )// => 'Parent'}}},}</script>
同样的,在 Child 中使用 inject 应该能够从 Parent 或者其先祖之一注入任何提供的内容。
Updating
to 属性可以通过 v-bind 动态的改变。当该值改变时,
如果子元素包含任何组件实例,这些实例将不会受到影响。这些实例将被保持 alive,保持它们的状态等。
<template><teleport v-bind:to="targetName"><p>This can be moved around with the button below</p></teleport><button v-on:click="toggleTarget">Toggle</button><hr /><div id="A"></div><div id="B"></div></template><script>export default {data: () => ({targetName: "A"}),methods: {toggleTarget() {this.targetName = this.targetName == "A" ? "B" : "A";}}};</script>
如果新的目标选择器不匹配任何元素:
- 在开发过程中应该记录一个警告。
- 内容将保持挂载之前的目标元素之上。
销毁(Destruction)
当 <teleport> 被销毁时(例如,因为它的父组件被销毁或因为 v-if),它的子组件被从 DOM 中移除,任何组件实例被销毁,就像它们仍然是父组件的子组件一样。
各种各样的
与原生 portal 的命名冲突
本 RFC 所引入的组件在本 RFC 的早期版本中被命名为
由于我们不希望与未来可能被称为 <portal> 的 HTML 元素发生命名冲突,特别是由于它的功能与 Vue 或 React 等软件中的 portal 的含义完全不同,我们选择将该组件更名为 <teleport>。
或者我们应该保留它,因为 Vue、React 等的 portal 概念已经是“常识”了,新的术语可能会使人们感到困惑而不是帮助?
dev-tools
<teleport> 不应该出现在父组件链中(this.$parent),但它应该在虚拟 DOM 中可以被识别,这样 Vue 的开发工具就可以在组件树的可视化中显示它们。
在 Vue 应用中的一个元素上使用 的问题
从技术上讲,这个建议允许选择 DOM 中的任何元素,包括由我们的 Vue 应用在组件树的其他部分渲染的元素。
但这使得 portal 的插槽内容处于其他组件声明周期的控制之下,这意味着如果该组件被销毁,内容可能会从 DOM 中被删除。
任何通过 <teleport> 而来的组件将有效地被删除其 DOM,而仍然在原来的虚拟 DOM 树中,这将导致这些组件试图更新出现补丁错误。
处理这个问题需要很多额外的逻辑,因此,这个用例被明确地排出在 RFC 之外。远程传输任何由 Vue 控制的 DOM 元素被认为是一种反模式,会导致真是 DOM 和虚拟 DOM 不同步。
缺点
我们看到的唯一明显的缺点是需要额外的代码来实现这一点。但从原型的实验来看,这段代码将是非常轻的,因为它只是在 virtualDOM 层挂载元素的一种稍微不同的方式,而且这个组件本身是可以 tree-shakable。、
由于这是一个附加功能,而且功能相当直接(一个属性定义为目标选择器),这也应该不会给 Vue 在文档或教学方面增加多少复杂性。
当考虑到目前的用户区解决方案是多么受欢迎,即使有其注意事项和局限性,成本/效益比似乎很明显。
备选方案
到目前为止,还没考虑其他设计。
如果我们不这样做会发生什么?
目前有几个用户区实现了这个功能,它们通常有一些注意事项和缺点,因为 Vue2 中的虚拟 DOM 级别不支持 portal。
人们可以继续使用这些具有现有限制和缺点的东西。
采纳策略
<teleport> 是一个新功能,因此纯粹是附加性质的。因此,该功能对 Vue 2.0 的应用程序迁移到 Vue 3.0 没有任何影响,除了那些可能选择将其某个组件命名为
初次接触 Vue3.0 或一般 Vue 的用户能够以常用的方式从文档中了解这一功能。并且在有意义的地方逐渐将其引入他们的项目中。
现有的第三方解决方案
如前所述,现在有几个第三方 plugin/libs 实现了类似的功能。
其中一些可能与本 RFC 无关,而另一些提供的功能超出了本提议的描述,可以调整他们的实现,在内部使用本提议的“原生” <teleport> 组件
如果 RFC vuejs/vue-next#28(Render function change)被采纳,那么这些库无论如何都要重新开发,这时它们可以采用这个新功能。
没有解决的问题
N/A
