目录
4.2 defineAsyncComponent 配合 import() 实现分包
1. 局部/全局组件
1.1 局部组件
每次使用之前,都需要 import 导入组件,在 component:{} 中声明组件(不能直接使用组件)
1.2 全局组件
1.2.1 什么是全局组件
每个页面都有使用的组件,可以封装为全局组件
1.2.2 全局组件注册方法
在 main.ts 中引入组件,通过 component() 函数注册全局组件
component() 函数接受两个参数:
- 第一个参数:组件名称
- 第二个参数:组件实例
具体步骤:在 createApp(App) 后添加 .component('Button', Button)
一定不要在 .mount('#app') 后面添加!!
import { createApp } from 'vue'
import App from './App.vue'
import Button from './components/button.vue'
// 注意顺序
createApp(App).component('Button', Button).mount('#app')
1.2.3 全局组件使用方法
任意 vue 文件中,直接使用标签即可,无需引入
2. 递归组件
2.1 什么是递归组件
2.1.1 官方文档
单文件组件https://cn.vuejs.org/api/sfc-script-setup.html#using-components
2.1.2 讲解视频分享【强推】
自己调用自己,通过 条件判断 结束递归(必写,防止内存泄漏)
2.2 递归菜单树
2.2.1 定义公共数据类型接口
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
};
2.2.2 父组件中使用子组件
定义菜单数据,传给子组件
监听子组件的点击事件,用于获取当前点击的菜单项
<template>
<div class="layout">
<Menu :data="data" @on-click="getItem"></Menu>
</div>
</template>
<script setup lang="ts">
const data = reactive<TreeList[]>([
{
name: "no.1",
children: [
{
name: "no.1-1",
children: [
{
name: "no.1-1-1",
},
],
},
],
},
{
name: "no.2",
children: [
{
name: "no.2-1",
},
],
},
{
name: "no.3",
},
]);
const getItem = (item: TreeList) => {
console.log('父组件中,点击子组件', item);
};
</script>
其实一般接口返回的数据,不是直接组装好的树结构,要转换,可以用下面的方法
export const jsonToTree = (data: any) => {
// 初始化结果数组,并判断输入数据的格式
const result: any = [];
if (!Array.isArray(data)) {
return result;
}
// 使用map,将当前对象的 category_id 与当前对象对应存储起来
const map: any = {};
data.forEach((item) => {
map[item.category_id] = item;
});
data.forEach((item) => {
const parent = map[item.parent_id];
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
};
2.2.3 子组件中的第一个 script 标签
此标签里,用于定义子组件 接受参数、抛出事件 等逻辑
<script setup lang="ts">
type Props<T> = {
data?: T[] | [];
};
defineProps<Props<TreeList>>();
const emit = defineEmits(['on-click']);
const clickItem = (item: TreeList) => {
emit('on-click', item);
};
</script>
2.2.4 子组件中的第二个 script 标签
用于定义组件名称,实现组件递归调用自身
由于 setup 中无法定义 name,因此才额外加了这样一个标签
<script lang="ts">
export default {
name:"TreeItem"
}
</script>
2.2.5 子组件中的 template 标签
展示菜单名称的位置,绑定了子组件的点击事件,实现最外层菜单 点击打印 功能
使用第二个 script 标签里定义的 name,作为递归组件名称标签,并使用
递归组件的 data,也就是当前循环项的 children
递归结束的条件:v-if="item?.children?.length" 当没有 children 时,则停止递归
递归组件内部 emit 了 on-click 事件,因此在递归组件的标签上,要监听 on-click 并抛出
为了避免 子组件的点击事件 同时触发 父组件的点击事件,要给最外层添加 .stop 修饰符,阻止事件冒泡
<template>
<div style="margin-left: 10px">
<div :key="index" v-for="(item, index) in data">
<!-- 展示菜单名称,注册点击事件 -->
<div @click.stop="clickItem(item)">{{ item.name }}</div>
<!-- 调用子组件自身 -->
<TreeItem
v-if="item?.children?.length"
:data="item.children"
@on-click="clickItem"
></TreeItem>
</div>
</div>
</template>
2.2.6 最终效果
3. 动态组件 Component
3.1 什么是动态组件
让多个组件,使用同一个挂载点,并动态切换
import A from './A.vue'
import B from './B.vue'
<component :is="A"></component>
3.2 实现 Tab 切换
3.2.1 使用动态组件时,控制台出现警告
出现警告的原因:
reactive 会进行 proxy 代理,组件代理之后 毫无用处;为了节省性能开销,Vue3 推荐使用shallowRef 或者 markRaw 跳过组件 proxy 代理
解决方案:引入 markRaw,并包裹动态组件,如下所示
<template>
<div>
<div class="tab-container">
<div
v-for="(item, index) in tabList"
:key="index"
@click="changeTab(item)"
>
{{ item.name }}
</div>
</div>
<!-- 动态组件 -->
<component :is="componentId"></component>
</div>
</template>
<script setup lang="ts">
import a from "./a.vue";
import b from "./b.vue";
import c from "./c.vue";
import { markRaw, reactive, ref } from "vue";
const componentId = ref(a);
const tabList = reactive([
{
name: "第五人格",
// 这里直接写组件,会有警告,使用 markRaw 清除警告
com: markRaw(a),
},
{
name: "全球高考",
com: markRaw(b),
},
{
name: "手可摘星辰",
com: markRaw(c),
},
]);
const changeTab = (item) => {
componentId.value = item.com;
};
</script>
3.2.2 动态组件 绑定字符串、绑定组件实例
在 Vue2 中,绑定字符串,可以实现动态组件切换;
在 setup 语法糖中,绑定组件实例,才能实现动态组件切换;
4. 异步组件 suspense
4.1 为什么要使用异步组件
npm run build
执行上方代码,打包 vue3 项目,生成 dist 目录
浏览器会通过 index.html 入口文件加载项目,当执行到下方代码时,就开始下载项目代码了……这个代码可能几十兆,很大,会导致一段时间的白屏
因此,在大型应用中,需要将应用分割成小一些的代码块,减少主包体积,进而解决白屏问题;此时,便可以利用到异步组件
4.2 defineAsyncComponent 配合 import() 实现分包
在 setup 语法糖中,直接使用 await,最终代码会被编译成 async setup() {}
子组件中,发送异步请求
<script setup>
const post = await fetch(`/api/post/1`).then(data => data.json())
</script>
父组件中引用子组件时,通过 defineAsyncComponent 异步加载,配合 import()函数,便可实现分包
import { defineAsyncComponent } from "vue";
const Dialog = defineAsyncComponent(
() => import("../../components/dialog/index.vue")
);
4.3 使用异步组件 suspense
此组件有两个插槽,都直接收一个 DOM 节点
在 default 插槽中的内容展示出来之前,会展示 fallback 中的内容
<!-- 异步组件 -->
<Suspense>
<!-- 通过 defineAsyncComponent 异步加载,配合 import()函数 引入的子组件 -->
<template #default>
<Dialog>
<template #default>
<div>我在哪儿</div>
</template>
</Dialog>
</template>
<!-- 组件加载完成前,展示的内容 -->
<template #fallback>
<div>loading...</div>
</template>
</Suspense>