# Teleport
Dom 渲染控制,直接指定渲染位置无论被哪个标签包裹。
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
/*Vue “将这里面的内容传送到‘body’标签下”*/
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 与 Vue components 一起使用
如果 <teleport>
包含 Vue 组件,则它将始终是 <teleport>
父组件的逻辑子组件:
const app = Vue.createApp({
template: `
<h1>Root instance</h1>
<parent-component />
`
})
app.component('parent-component', {
template: `
<h2>This is a parent component</h2>
<teleport to="#endofbody">
<child-component name="John" />
</teleport>
`
})
app.component('child-component', {
props: ['name'],
template: `
<div>Hello, {{ name }}</div>
`
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在这种情况下,即使在不同的地方渲染 child-component
,它仍将是 parent-component
的子级,并将从中接收 name
prop。
这也意味着来自父组件的注入会正常工作,在 Vue Devtools 中你会看到子组件嵌套在父组件之下,而不是出现在他会被实际移动到的位置。
# 在同一目标上使用多个 teleport
一个常见的用例场景是一个可重用的 <Modal>
组件,它可能同时有多个实例处于活动状态。对于这种情况,多个 <teleport>
组件可以将其内容挂载到同一个目标元素。顺序将是一个简单的追加——稍后挂载将位于目标元素中较早的挂载之后。
<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>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 实战:无限循环分类列表
传统的属性结构列表递归时其样式会受到父组件的影响(会撑开父元素,导致父元素间留有大量空隙,不符合分类列表样式需求)。
可以使用 Teleport 使每一个循环列表与父组件同层。
一般的循环递归样式图:(点击父级后展开子级,但父级被撑开不在同一级)
一般分类列表的样式图:(点击父级后展开子级,父级样式不受影响,类似多级 tab 的效果)
代码说明:
1.需要放置的位置
为了方便管理全局数据的统一性,采用单向数据流,基于 provide(注入) 实现定向数据来驱动循环组件的选中状态
<template>
<div>
<!-- 存放组件的容器 -->
<div id="searchCategory"></div>
</div>
<!-- 循环分类组件(不用放在容器里,也尽量不要放在容器里) -->
<categoryMenuVue :list="categoryData"></categoryMenuVue>
</template>
<script setup>
import { provide } from 'vue';
const categoryData = [ // 模拟数据
{
id: '01',
name: '01',
children: [
{ id: '11', name: '11' },
{ id: '12', name: '12' },
],
},
{ id: '02', name: '02' },
{
id: '03',
name: '03',
children: [
{ id: '31', name: '31', children: [{ id: '41', name: '41' }] },
{ id: '32', name: '32' },
],
},
];
const categoryMenuIds = ref([]); // 每次分类被点击时,记录当前选中的分类id,如[03,31,41]
const categoryMenuIdChange = (lev, id) => { // 每次分类被点击时触发的回调,会返回被点击的分类的层级以及id
categoryMenuIds.value.splice(lev, categoryMenuIds.value.length - 1, id); // 更新选中消息
console.log(lev, id, categoryMenuIds.value);
};
provide('categoryMenu', { categoryMenuIdChange, categoryMenuIds });
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2.递归组件
为了方便组件自调用,推荐将其注册为全局组件
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import categoryMenuVue from "@/views/search/categoryMenu/index.vue"
app.component('categoryMenuVue', categoryMenuVue)
1
2
3
4
5
6
7
2
3
4
5
6
7
组件实现:
<!-- categoryMenuVue 组件 -->
<template>
<Teleport to="#searchCategory"><!-- 关键所在,将每一次的循环组件都放到同级容器中去 -->
<div class="content">
<div class="title"><span v-if="list[0].parentId == '0'">所有分类:</span></div>
<div class="list">
<div v-for="(item, index) in list" :key="item.id">
<a
:class="{ active: item.id == categoryMenuIds[_level] }"
@click="handleClickCategory(item, index)"
>
{{ item.name }}
</a>
<!-- 进行递归,组件自调用 -->
<template v-if="item.children && item.children.length > 0 && item.id == searchOptions.id">
<categoryMenuVue
:list="item.children"
:level="_level"
></categoryMenuVue>
</template>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { reactive, computed, inject } from 'vue';
const { categoryMenuIdChange, categoryMenuIds } = inject('categoryMenu');
const props = defineProps(['list', 'level']);
const _level = computed(() => {
if (props.level || props.level === 0) {
return props.level + 1;
} else {
return 0;
}
});
const searchOptions = reactive({
id: '',
});
const handleClickCategory = (item) => {
searchOptions.id = item.id;
categoryMenuIdChange(_level.value, item.id);
};
</script>
<style lang="less" scoped>
a {
color: black;
}
a:hover {
color: #fb6e23;
}
.active {
color: #fb6e23;
}
.content {
display: flex;
background-color: #f6f6f6;
padding: 16px 24px;
margin-bottom: 8px;
.title {
width: 80px;
flex-basis: 80px;
font-weight: 700;
}
.list {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80