Vue3 中组件的变化
# Vue3 中组件的变化
Vue router 的一些变化
vue 异步组件
Teleport
首先我们先来看下 vue router 的一些变化
安装的方式 -- https://router.vuejs.org/zh/guide/#javascript
npm install vue-router@4
首先是创建 Home、About 页面
我们新建一个 views 文件夹存放我们的页面,然后新建Home.vue和About.vue
// Home.vue <template> <div class="common-view default-view"> <div class="child-view"> <span>子组件:一</span> </div> <div class="child-view"> <span>子组件:二</span> </div> <div class="child-view"> <span>子组件:三</span> </div> <div class="child-view"> <span>子组件:四</span> </div> <div class="child-view"> <span>子组件:五</span> </div> <div class="child-view"> <span>子组件:六</span> </div> </div> </template> <script> export default {}; </script> <style scoped> .common-view { width: 90%; height: 40%; border: 1px solid #f0f0f0; margin: 0 auto; margin-top: 30px; overflow: hidden; } .default-view { display: flex; justify-content: space-around; align-items: center; flex-wrap: wrap; } .child-view { width: 30%; height: 45%; background: #bdcac8; text-align: center; line-height: 158px; border-radius: 8px; } .child-view span{ color: #fff; } </style>
// About.vue <template> <span>this is about view</span> </template> <script> export default {}; </script> <style scoped> span{ font-size: 16px; font-weight: bold; } </style>
然后新建 Router 文件夹来引入 vue-router
新增 index.js
import { createRouter, createWebHashHistory } from "vue-router"; import routes from "./routes"; const router = createRouter({ history: createWebHashHistory(), routes, }); export default router;
我们可以看到 vue-router 也放弃了构造函数,直接导出的形式来创建 router,
并且 路由的形式是通过 函数创建的方式来使用 createWebHashHistory 这是 Hash 路由
然后在 main.js 中使用 use 来引用我们的插件,符合链式调用的新特性。
import { createApp } from "vue"; import App from "./App.vue"; import router from "./Router"; const app = createApp(App); // console.log(app); app.use(router).mount("#app");
<div class="router-line-edit"> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </div> <router-view />
然后在 App.vue 中使用 router ,这里和原来是一致的。
ok 至此 vue-router 成功集成
异步组件
<template> <div class="common-view default-view"> <div class="child-view"> <span>子组件:一</span> </div> <div class="child-view"> <span>子组件:二</span> </div> <ChildBlock /> <div class="child-view"> <span>子组件:四</span> </div> <div class="child-view"> <span>子组件:五</span> </div> <div class="child-view"> <span>子组件:六</span> </div> </div> </template> <script> import ChildBlock from "../components/ChildBlock.vue"; export default { components: { ChildBlock, }, }; </script>
我们将组件三改为组件引入方式,但是如果我们的文件过多,全部整合到一个 js 中,会导致 js 文件过大,响应慢,这时候需要按需引用的方式来异步加载我们的组件
这里需要用到 defineAsyncComponent 这个成员 他呢接收一个函数并返回一个 Promise
<script> import { defineAsyncComponent } from "vue"; const Block3 = defineAsyncComponent(() => import("../components/ChildBlock.vue") ); export default { components: { Block3, }, }; </script>
刷新之后,我们看到 诶? 这个效果和之前是一样的呀,
那肯定的,我们在本地加载肯定是很快的,而且 ChildBlock 组件里面东西很少,加载速度会很快,但是我们来到浏览器看到 network 中 ChildBlock是在 Home.vue 之后才加载的

我们来用一些手段模拟一下
新增一个 文件夹 util 和 utils 工具 js
export function delay() { let duration = random(1000, 4000); return new Promise((resolve) => { setTimeout(() => { resolve(); }, duration); }); } function random(min, max) { return Math.floor(Math.random() * (max - min) + min); }
然后使用工具函数
<script> import { delay } from "../util/utils"; import { defineAsyncComponent } from "vue"; const Block3 = defineAsyncComponent(async () => { await delay(); return import("../components/ChildBlock.vue"); }); export default { components: { Block3, }, }; </script>
这个时候我们会发现,好像是这么回事!
然后 defineAsyncComponent 还可以使用配置的方式来进行定制化
<script> import { delay } from "../util/utils"; import { defineAsyncComponent } from "vue"; const Block3 = defineAsyncComponent({ loader: async () => { await delay(); const comp = import("../components/ChildBlock.vue"); return comp; }, }); export default { components: { Block3, }, }; </script>
两种方式相同,但是这个可以处理比较多的场景,比如说,在组件加载的 pendding 状态,和组件加载后的 error 状态。
加载中
我们新增一个Loading 组件
<template> <div class="child-view loading"> <span>加载中···</span> </div> </template> <style scoped> .loading { background-color: #afb9be; } span { color: orange; } </style>
然后 Home.vue 中的 loadingComponent 配置,这个配置是 组件加载中的状态。
const Block3 = defineAsyncComponent({ loader: async () => { await delay(); const comp = import("../components/ChildBlock.vue"); return comp; }, loadingComponent: Loading, });
error状态
我们可以新增一个 error 状态的组件 ,这里 error 使用了插槽的方式来填写
<template> <div class="child-view loading"> <span><slot></slot></span> </div> </template> <style scoped> .loading { background-color: #badbcb; } span { color: red; } </style>
配置,然后 配置 errorComponent,但是我们会发现直接引用组件的话没有办法添加属性,但是组件的创建是不是也是对象? 我们可以用配置的方式来配置组件 使用 render 函数。但是 由于 render 函数中参数 h 取消 改为 vue 中的 成员,所以要在 vue 中解构 h
<script> import { delay } from "../util/utils"; import { defineAsyncComponent, h } from "vue"; import Loading from "../components/Loading.vue"; import Error from "../components/Error.vue"; const Block3 = defineAsyncComponent({ loader: async () => { await delay(); throw new Error("error"); const comp = import("../components/ChildBlock.vue"); return comp; }, loadingComponent: Loading, errorComponent: { render() { return h(Error, "组件加载失败!"); }, }, }); export default { components: { Block3, }, }; </script>
同理 我们在 App vue 中是否也可以用这种方式来异步加载页面。
但是这样写是否有些麻烦,每次都写一大串,我们简单封装一下。
Utils.js
export function getAsyncComponent(path) { const Block = defineAsyncComponent({ loader: async () => { await delay(); throw new ReferenceError("error"); const comp = import(path); return comp; }, loadingComponent: Loading, errorComponent: { render() { return h(ErrorCom, "组件加载失败!"); }, }, }); return Block; }
这样可以通过传入路径来获取我们的 异步组件啦!
同理,我们封装一个获取异步页面的函数
export function getAsyncPage(path) { const Block = defineAsyncComponent({ loader: async () => { await delay(); const comp = import(path); return comp; } }); return Block; }
然后 在 routes 中将组件替换为异步获取
import { getAsyncPage } from "../util/utils"; const Home = getAsyncPage("../views/Home.vue"); const About = getAsyncPage("../views/About.vue"); export default [ { path: "/", component: Home }, { path: "/about", component: About }, ];
完整代码:
import { defineAsyncComponent, h } from "vue"; import Loading from "../components/Loading.vue"; import ErrorCom from "../components/Error.vue"; import "nprogress/nprogress.css"; import NProgress from "nprogress"; export function delay() { let duration = random(1000, 4000); return new Promise((resolve) => { setTimeout(() => { resolve(); }, duration); }); } function random(min, max) { return Math.floor(Math.random() * (max - min) + min); } export function getAsyncComponent(path) { const Block = defineAsyncComponent({ loader: async () => { NProgress.start(); await delay(); const comp = import(path); NProgress.start(); return comp; }, loadingComponent: Loading, errorComponent: { render() { return h(ErrorCom, "组件加载失败!"); }, }, }); return Block; } export function getAsyncPage(path) { const Block = defineAsyncComponent({ loader: async () => { NProgress.start(); await delay(); const comp = import(path); NProgress.done(); return comp; }, }); return Block; }
Teleport
Teleport 有什么作用呢?
我们用一个例子来讲述一下。
首先创建一个MaskView组件,全局蒙版
//MaskView.vue <template> <div class="mask"> <slot></slot> </div> </template> <style scoped> .mask { position: fixed; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); } </style>
这就是一个模糊的蒙版
然后我们在 Home 页面导入 MaskView 组件,然后新增一个按钮来使蒙版显示或隐藏。
<button @click="onMaskShow">show mask</button> <mask-view v-show="maskRef"> <button @click="onMaskOff">turn off</button> </mask-view> setup() { const maskRef = ref(false); const onMaskShow = () => { maskRef.value = true; }; const onMaskOff = () => { maskRef.value = false; }; return { maskRef, onMaskShow, onMaskOff, }; },
项目跑起来之后我们可以观察一下项目结构,
很明显我们的 MaskView 是在 app Home 页面下的对吧,
但是这个 MaskView 是不是应该在 body 下面呢?
以前我们 Vue 不可能实现这个功能,模板结构,即页面逻辑。
但是现在不一样了,Vue3 中提供给我们一种新的组件,这是一个内置的全局组件Teleport, 我们可以用它来帮助我们,改变页面构造
<Teleport to="body"> <mask-view v-show="maskRef"> <button @click="onMaskOff">turn off</button> </mask-view> </Teleport>
ok 这就可以了, Teleport 存在一个属性 to 意思是去哪。