传送门Teleport
本文主要包含以下内容:
Teleport
是什么?为什么需要使用Teleport
?- 经典案例: 包含全屏模式的组件
Teleport
使用详解
(一) 为什么需要使用Teleport
?
Teleport
传送门组件提供一种简洁的方式可以指定它里面内容的父元素。允许我们控制Teleport
的嵌套的内容在DOM中哪个父节点下呈现HTML,而不必求助于全局状态或者拆为两个组件
使用场景:像modal
模态框这样的组件,我们一般会将它完全的和我们的vue应用的DOM 完全剥离, 管理起来反而容易
原因在于模态框的 position:absolute
以父级相对定位的 div
作为引用,若模态框是在深层嵌套的div中渲染,那么处理嵌套组件的定位,z-index和样式就会变得比较困难
这就是Teleport
解决的问题,使用Teleport
组件,可以让我们在组件的逻辑位置写模板代码,可以使用组件的data或者props状态,然后在组件的范围之外渲染它
(二)经典案例:包含全屏模式的组件
<template>
<div>
<button @click="modelOpen = true">点击打开弹窗 </button>
<teleport to="body">
<div v-if="modelOpen" class="model">
<div class="model-body">
这是一个模态框
<button @click="modelOpen = false">关闭弹窗</button>
</div>
</div>
</teleport>
</div>
</template>
<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
name: 'ModelButton',
setup() {
const modelOpen = ref(false);
return {
modelOpen
}
}
})
</script>
<style scoped>
.model {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, .3);
display: flex;
align-items: center;
justify-content: center;
}
.model-body {
width: 300px;
height: 250px;
background: #fff;
}
</style>
使用teleport
组件,通过props to
属性指定该组件的渲染位置在body下,但该组件是modelOpen
状态则由vue
内部组件控制
(三)
Teleport
使用详解
-
Props:
-
to - string, 必传属性,必须为有效的查询选择器或者HTMLElement(如果在浏览器环境中使用),指定组件嵌套内容移动到所在位置下的目标的元素
<!-- 正确 --> <teleport to="#some-id" /> <teleport to=".some-class" /> <teleport to="[data-teleport]" /> <!-- 错误 --> <teleport to="h1" /> <teleport to="some-string" />
-
disabled - boolean, 为可选属性,用于禁用
<teleport>
的功能,意味着起插槽的内容将不会移动到任何位置,而是在父组件中指定了<teleport>
的位置渲染以上面的全屏组件为例:
<template> <div> <button @click="modelOpen = true">点击打开弹窗</button> <teleport to="body" disabled> <div v-if="modelOpen" class="model"> <div class="model-body"> 这是一个模态框 <button @click="modelOpen = false">关闭弹窗</button> </div> </div> </teleport> </div> </template>
实际效果如下:
-
注意点:
<teleport to="#popup" :disabled="displayVideoInline">
<video src="./my-movie.mp4">
</teleport>
请注意,这将移动实际的 DOM 节点,而不是被销毁和重新创建,并且它还将保持任何组件实例的活动状态。所有有状态的 HTML 元素 (即播放的视频) 都将保持其状态。
-
与
Vue component
一起使用如果
<teleport>
包含vue
组件,则它仍将是<teleport>
父组件的逻辑子组件以一个
modal
组件为例:index.html <div id="app"></div> <div id="modal-container"></div> vue内部组件 <teleport to="#modal-container"> <!-- use the modal component, pass in the prop --> <modal :show="showModal" @close="showModal = false"> <template #header> <h3>custom header</h3> </template> </modal> </teleport>
js核心代码 import { defineComponent, ref } from 'vue'; import Modal from './Modal.vue'; export default defineComponent({ components: { Modal }, setup() { // modal 的封装 const showModal = ref(false); return { showModal } } })
在这种情况下,即使在不同的地方渲染
Modal
,它仍将是当前组件(调用Modal
的组件)的子级,并将从中接收show
prop这也意味着来自父组件的注入按预期工作,并且子组件将嵌套在
Vue Devtools
中的父组件之下,而不是放在实际内容移动到的位置看实际效果以及在
Vue Devtool
中
思考
-
teleport
取代vue2中的portal-vue
插件,然而,实践发现,teleport
的目标元素不能由组件本身呈现,只能移植到组件外部有效const app = { template: ` <div> <h1>App</h1> <!-- Teleport目标元素 --> <div id="dest"></div> <!-- comp 包含Teleport --> <comp /> </div>` } Vue.createApp(app).component('comp', { template: ` <div> A component <Teleport to="#dest"> Hello From Portal </Teleport> </div>` }).mount('#app')
<script src="https://unpkg.com/vue@3.0.0-rc.9/dist/vue.global.js"></script> <div id="app"></div>
页面显示效果如下,Teleport 移植的内容并没有渲染到对应的位置,并且控制台告警
警告信息:目标元素必须在组件挂载之前存在——也就是说,目标不能由组件本身呈现,理想情况下应该在整个Vue组件树之外。
[Vue warn]: Failed to locate Teleport target with selector "#dest". 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
的组件的父组件上,则无效,并会告警,移植到组件外部有效
对于这种情况的解决方案:
方案一: 添加挂载判断
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')
方案二:与方案一本质上是一致的,目标元素挂载后再启用teleport
const useTele = () => {
const target = ref(null)
return () => target
}
const useThisTele = useTele()
createApp({
setup() {
return { target: useThisTele() }
},
template: `
<div>
<h1>App</h1>
<div id="dest" :ref="d => target = d"></div>
<parent-comp/>
</div>`
}).component('parent-comp', {
template: `
<div>
<child-comp/>
</div>`
}).component('child-comp', {
setup() {
return { target: useThisTele() }
},
template: `
<div>
<Teleport :to="target" :disabled="!target">
Hello From Portal
</Teleport>
</div>`
}).mount('#app')
另外,对于teleport 这个特性,依据贡献者**LinusBorg** 的说法,还是有些脆,若是将目标元素的父元素从DOM上移除,portal内容也会随之从DOm移除,然而,源组件并不会收到通知,在它的vdom中,它假定元素仍然在DOM中。当源组件稍后进行更新时,这可能会导致更新错误。
-
在上述的实践还发现了一个问题,在vite构建的项目中,若在main.js 写入下面这段代码会告警
const app = { template: ` <div> <h1>App</h1> <!-- Teleport目标元素 --> <div id="dest"></div> <!-- comp 包含Teleport --> <comp /> </div>` } createApp(app).component('comp', { template: ` <div> A component <Teleport to="#dest"> Hello From Portal </Teleport> </div>` }).mount('#app')
告警信息:组件提供模板选项,但是在Vue的这个构建中不支持运行时编译,配置你的bundler别名
vue: vue/dist/vue.esm-bundler.js
runtime-core.esm-bundler.js:38 [Vue warn]: Component provided template option but runtime compilation is not supported in this build of Vue. Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js". at <App>
原因:
开发环境下:
- 如果是vue2的话,需要依赖构建工具,如webpack, glup 等, 流程是 先使用对应的构建工具来进行构建编译生成一个一个的bundle, 然后才是运行。
- 如果是vue3的话,有两种方式,一种是沿用vue2的开发模式,另一种是 使用 vite这个构建工具,流程是 基于现代浏览器的特点, 先查找相关的引用,然后在编译,在运行
解决:
vue3
-
使用
vite
构建: 项目根目录下面建立vite.config.js
配置别名alias: { 'vue': 'vue/dist/vue.esm-bundler.js' // 定义vue的别名,如果使用其他的插件,可能会用到别名 },
-
使用vue-cli 进行构建,项目根目录下面建立
vue.config.js
配置一个属性module.exports = { runtimeCompiler: true } // 确定是运行时候编译
vue2
项目中建立对应的vue.config.js
module.exports = { // ... resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' // 用 webpack 1 时需用 'vue/dist/vue.common.js' } } }