Vue 3 Teleport 是否只能移植到 vue 之外?

Fuz*_*yma 6 vue.js vuejs3 vue-teleport

Vue 3 有一个新的 Teleport 功能,它取代了 vue 2 中的 portal-vue 插件。但是,我发现不可能将组件移植到 vue 控制的地方(=在 vue 应用程序中)。它仅在移植到外部(主体、其他元素......)时有效。

const app = {
  template: `
  <div>
    <h1>App</h1>
    <div id="dest"></div>
    <comp />
  </div>`
}


Vue.createApp(app).component('comp', {
  template: `
  <div>
    A component
    <Teleport to="#dest">
      Hello From Portal
    </Teleport>
  </div>`
}).mount('#app')
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/vue@3.0.0-rc.9/dist/vue.global.js"></script>

<div id="app"></div>
Run Code Online (Sandbox Code Playgroud)

如您所见,控制台甚至报告说需要已经渲染传送目标。但是我怎么能告诉 vue 先渲染哪个组件呢?在我的例子中,目标在传送前的 dom 中。

这在 portal-vue 中不是问题,而且非常令人失望,因为它使整个概念变得不那么可用。

Dan*_*iel 10

Vue 错误消息指向了问题所在。它说Invalid Teleport target on mount: null

问题是目标不存在YET

这可以通过仅在安装组件后渲染传送部分来轻松解决。

看起来像这样的东西,Vue公司应在不明确的检查处理。当您将 id 作为字符串传递时,很难判断目标是否是 Vue 组件,尤其是在尚未呈现的情况下。但我只是在推测球队的意图。

const app = {
  template: `
  <div>
    <h1>App</h1>
    <div id="dest"></div>
    <comp />
  </div>`
}

Vue.createApp(app).component('comp', {
  template: `
  <div>
    A component
    <Teleport to="#dest" v-if="isMounted">
      Hello From Portal
    </Teleport>
  </div>`,
  data: function(){
    return { 
        isMounted: false
    }
  },
  mounted(){
    this.isMounted = true
  }
}).mount('#app')
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/vue@3.0.0-rc.9/dist/vue.global.js"></script>

<div id="app"></div>
Run Code Online (Sandbox Code Playgroud)


Bat*_*Man 10

或者可以创建MountedTeleport组件来扩展 @Daniel 解决方案,以便模型不被污染

<template>
    <Teleport :to="to" v-if="isMounted"><slot></slot></Teleport>
</template>

<script>
export default {
    name: "MountedTeleport",
    props: ['to'],
    data() {
        return {isMounted: false}
    },
    mounted() {
        this.isMounted = true;
    }
}
</script>
Run Code Online (Sandbox Code Playgroud)

它被用作

<MountedTeleport to="#given-selector">your html....</MountedTeleport>
Run Code Online (Sandbox Code Playgroud)

如果仍然传送初始化第一个可以使用

mounted() {
    this.$nextTick(() => {
        this.isMounted = true;
    })
}
Run Code Online (Sandbox Code Playgroud)

或者即使先渲染然后可能setTimeout会完成这项工作


Xin*_*hao 7

模板引用是对真实DOM 元素的引用,因此在 DOM 实际渲染之前,模板引用将只是null.

Vue采用虚拟dom架构。您的代码通常仅指示 vue 更改虚拟 dom,并将这些更改同步到真实 dom 的过程称为修补。修补是异步进行的。this.$refsVue 会在必要时(每次修补 dom 之后)负责填充/重新填充,并且最早$refs可访问的时间通常是已安装的钩子(如果有条件渲染,则可能会更晚)。

不过,并不是说$refs以后就永远不会改变。Vue 组件安装顺序有时也非常令人惊讶(例如,首先安装子组件还是父组件?)。通常所有这些都不会成为任何问题,因为在反应性世界中这些序列应该无关紧要。然而,当你开始做命令性的事情时——例如依靠生命周期钩子来设置一个标志,稍后驱动模板的条件渲染等——你会再次容易受到反应式框架首先试图解决的问题的影响。

因此,为了更安全的方法,您最好保持反应性,观察变化$refs并做出反应。但问题是$refs 不是响应式的

但 Vue 允许 ref 指令采用一个函数,以便您可以控制要存储模板引用的位置。因此,您可以将 ref 存储在本质上是反应性的东西中 - 即data。将数据作为道具输入到子组件中,并将您的传送目标绑定到该道具。现在一切又恢复了反应。

为了演示,我修改了您的示例,添加了两个额外的按钮来控制是否渲染传送目标 div。现在您无法再访问已安装挂钩中的目标 div 模板引用,因为在您单击“传送”按钮之前,div 将不会被渲染:

const app = {
  template: `
    <div>
      <h1>App</h1>
      <button @click="show=true"> teleport on </button>
      <button @click="show=false"> teleport off </button>
      <div v-if="show" :ref="(el) => dest = el"></div>
      <comp :target="dest"/>
    </div>`,

  data() {
    return { dest: null, show: false };
  }
}


Vue.createApp(app).component('comp', {
  props: ["target"],

  template: `
    <div>
      A component
      <Teleport v-if="target !== null" :to="target">
        Hello From Portal
      </Teleport>
    </div>`
}).mount('#app')
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app"></div>
Run Code Online (Sandbox Code Playgroud)

  • @Dom 是的,也不是。像 Teleport 这样的组件根据定义与其目标以及拥有该目标的组件紧密耦合。您无法传送到不存在的元素,并且拥有目标的组件无法有条件地删除目标,因为这会中断传送。因此,正确实施的 Teleport 系统通常需要更多与上述方式类似的脚手架。也就是说,提供/注入可以将目标注入深度嵌套的组件。并且 target 可以是可以直接观看的 ref (元素的) (2认同)