vue3 就业课程 组件的变化
一、路由的变化
1、 vue3
三步打通路由页面切换
1. 安装
2. 创建并配置路由
3. 在启动文件里应用路由
1. 安装
npm i vue-router@next 袁老师安装的是测试版 ^4.0.0-beta.13
npm i vue-router 我默认安装的版本
"dependencies": { "nprogress": "^0.2.0", "vue": "^3.0.4", "vue-router": "^4.5.0" },
2. 创建并配置路由
为了更好的 Tree-shaking 优化,vue3 里面变成了具名导出
没有构造函数 vue,导出的是具名函数 { createApp }
路由也一样,没有了构造函数,导出具名函数 { createRouter } 用该函数创建路由
/src/router/index.js
import {createRouter, createWebHistory} from "vue-router"; import routes from "./routes"; export default createRouter({ history: createWebHistory("/blog"), // 之前是mode: "history" routes, });
通过导出的 {createWebHistory} 函数创建 history 对象,该对象记录页面的路由,
baseUrl 变成了 createWebHistory 的参数
手册
https://github.com/vitejs/vue-router-next
点击 Migration Guide 迁移文档(如何升级)
查看 Breaking Changes 不兼容的更改
路由配置
/src/router/routes.js
import Home from "../views/Home.vue"; import About from "../views/About.vue"; export default [ {path: "/", component: Home}, {path: "/about", component: About}, ];
3. 在启动文件里面应用路由插件(这里不是构造函数 Vue 应用插件了)
main.js
import { createApp } from 'vue' import App from './App.vue' import router from './router'; // 导入路由 createApp(App).use(router).mount('#app'); // 应用插件
根组件里面使用导航 <router-link>,
以及路由匹配到的组件 <RouterView/>
App.vue
<template> <div id="nav"> <router-link to="/">Home</router-link> | <RouterLink to="/about">About</RouterLink> </div> <RouterView/> </template> <script> export default { name: 'App', } </script> <style> body{ font-family: Aveir, Helvetica, Arial, sans-serif; text-align: center; color: #2c3e50; } #nav a{ font-weight: bold; color: #2c3e50; text-decoration: none; } #nav a.router-link-exact-active{ color: #42b983; } </style>
2、vue2
路由版本
"dependencies": { "core-js": "^3.8.3", "vue": "^2.6.14", "vue-router": "^3.5.1" },
以前导入构造函数 VueRouter
src/router/index.js
import Vue from 'vue'; import VueRouter from 'vue-router'; // 导入构造函数 import Home from "@/views/Home.vue"; import About from "../views/About.vue"; Vue.use(VueRouter) const routes = [ {path: "/", component: Home}, {path: "/about", component: About}, ] const router = new VueRouter({ mode: "history", baseURL: "/blog", routes }) export default router
应用路由插件
main.js
import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App) }).$mount('#app')
二、异步组件
本节专门做 Home 组件
home 页面有多个区域,相当于很多组件共同组成的页面,一些组件是直接加载的,还有一些组件是异步加载的
有两个组件的功能太多了,
如果全面打包到一个 js 里面 Home 页面的 js 文件会很大,所以需要异步去加载这两个组件的 js,
也就是说这两个组件的区是域懒加载
1、基本使用
vue2 要配置一个动态的 import
vue3 配置的步骤
1. 要导入 { defineAsyncComponent } 函数来定义一样异步组件
2. 参数要传一个函数 () => {},该参数函数要返回一个 promise
3. 比如返回一个动态 import() 导入组件 Block3.vue,动态的 import 就是一个 promise
4. defineAsyncComponent 函数得到(返回)一个 Block3,然后注册这个 Block3
src/views/Home.vue
<template> <div class="container"> <div class="block"> <h2>区域1</h2> <p> <button>打开蒙层</button> </p> </div> <div class="block mid"> <h2>区域2</h2> </div> <div class="block big"><Block3/></div> <div class="block big"><h2>区域4</h2></div> <div class="block mid"><h2>区域5</h2></div> <div class="block"><h2>区域6</h2></div> </div> </template> <script> import { defineAsyncComponent } from "vue"; import {delay} from "../util"; const Block3 = defineAsyncComponent(async () => { await delay(3000); return import("../components/Block3.vue"); }); export default { components:{ Block3 }, } </script> <style scoped> .container{ display: flex; width: 1000px; margin: 0 auto; flex-wrap: wrap; padding: 0 50px; justify-content: space-between; } .block{ width: 200px; margin: 15px; height: 250px; border: 1px solid #ebebeb; border-radius: 3px; box-shadow: 0 0 8px 0 rgba(232, 237, 250, 0.6), 0 2px 4px 0 rgba(232, 237, 250, 0.5); display: flex; justify-content: center; align-items: center; flex-direction: column; } .mid{ width: 300px; } .big{ width: 400px; } </style>
const Block3 = defineAsyncComponent( () => import("../components/Block3.vue") )
这个 block3 不是项目中的 Block3.vue 文件,它是由函数 defineAsyncComponent 返回的,
这个 <block3/> 组件要等待加载完成后才会显示出来
import("../components/Block3.vue")
这里 import 动态导入返回是一个 promies,resolve() 的时候是一个组件就可以了,也可以自己写一个 Promise()
const Block3 = defineAsyncComponent(() => { return new Promise(); });
平时开发的时候就直接返回 import 动态导入一个组件就可以了,但是为了看到效果让它慢一加载
1. defineAsyncComponent 的参数传一个 async 函数(它返回的一定是 promise)
2. delay 暂停一段时间在返回 import,这样达到测试的效果
const Block3 = defineAsyncComponent(async () => { await delay(3000); return import("../components/Block3.vue"); });
两个需要异步加载的异步组件
<!-- src/components/Block3.vue --> <template> <h2>异步区域3</h2> </template> <!-- src/components/Block5.vue --> <template> <h2>异步区域5</h2> </template>
2、晋级配置
有这样的场景
1. 异步组件正在加载中,如何显示 loading 效果
2. 如果加载出错,如何显示
defineAsyncComponent 的参数除了配置一个 () =>{} 函数,还可以配置一个 {} 对象
其实配置对象是 vue2 的写法
const AsyncComponent = () => ({ // 需要加载的组件(应该是一个Promise对象) component: @import("./MyComponent.vue"), // 异步组件加载时使用的组件 loading: LoadingComponent, // 加载失败时使用的组件 error: ErrorComponent, // 展示加载时组件的延迟时间,默认值是200毫秒 delay: 200, // 如果提供了超时间且组件加载也超时了,则使用加载失败时的组件,默认值是Infinity timeout: 3000 })
vue3 的写法
defineAsyncComponent 配置一个对象
loader 属性的值为刚才作为参数的函数
loadingComponent 正在加载中的时候
errorComponent 当出错误是 reject 状态,这时候显示的组件
<script> import { defineAsyncComponent, h } from "vue"; import {delay} from "../util"; import Loading from "../components/loading.vue"; import Error from "../components/Error.vue"; const Block3 = defineAsyncComponent({ loader: async () => { await delay(3000); if(Math.random() < 0.5){ // 加一个错误的场景,报错相当于reject throw new TypeError(); } return import("../components/Block3.vue"); }, loadingComponent: Loading, // 当promise在pending的时候,将显示这里的组件 errorComponent: { render(){ return h(Error, "出错了"); } } }); export default { components:{ Block3 } } </script>
Error 组件对象导入进来了,如何渲染一个有属性的组件呢,就是如何传递该组件的 slot 内容呢?
组件就是一个对象,errorComponent: {} 属性这里可以配置一个新组件对象,
在新组件对象里面使用 rander 函数
rander 渲染一个 Error 组件,组件的内容是“出错了”
在 vue3 里面渲染的 h 函数变成了一个普通的函数,变成一个全局导出的普通 { h } 函数,
这样在任何地方甚至不在组件里面都可以渲染虚拟节点,虚拟节点就是一个普通对象
errorComponent: {
render( h ){
return h(Error, "出错了");
},
}
Loading 组件
src/components/Loading.vue
<template> <svg viewBox="25 25 50 50" class="circular"> <circle cx="50" cy="50" r="20" fill="none" class="path"></circle> </svg> </template> <style scoped> .circular { height: 42px; width: 42px; animation: loading-rotate 2s linear infinite; } .path { animation: loading-dash 1.5s ease-in-out infinite; stroke-dasharray: 90, 150; stroke-dashoffset: 0; stroke-width: 2; stroke-width: 1; stroke: #409eff; stroke: #ff88cc; stroke-linecap: round; } @keyframes loading-rotate { 100% { transform: rotate(1turn); } } @keyframes loading-dash { 0% { stroke-dasharray: 1, 200; stroke-dashoffset: 0; } 50% { stroke-dasharray: 90, 150; stroke-dashoffset: -40px; } 100% { stroke-dasharray: 90, 150; stroke-dashoffset: -120px; } } </style>
错误文本用 slot 来替代
src/components/Error.vue
<template> <h3><slot></slot></h3> </template> <style scoped> h3 { color: #f40; } </style>
3、封装一个辅助函数
把定义异步组件的代码,封装一个辅助函数
src/utIl.js
import { defineAsyncComponent, h } from "vue"; import Loading from "../components/loading.vue"; import Error from "../components/Error.vue"; export function delay(duration){ if(duration){ duration = getRandom(1000, 5000); } return new Promise((resolve) => { setTimeout(() => { resolve(); }, duration); }); } export function getRandom(min, max){ return Math.floor(Math.random() * (max - min)) + min; } // 函数返回的是一个异步组件,得到一个异步组件,参数path是组件加载的路径 export function getAsyncComponent(path){ return defineAsyncComponent({ loader: async () => { await delay(3000); if(Math.random() < 0.5){ throw new TypeError(); } return import(path); }, loadingComponent: Loading, errorComponent: { render(){ return h(Error, "出错了"); }, }, }); }
使用 getAsyncComponent 函数得到异步组件
然后注册,在模板中使用
scr/views/Home.vue
<template> <div class="container"> <div class="block"> <h2>区域1</h2> <p> <button>打开蒙层</button> </p> </div> <div class="block mid"> <h2>区域2</h2> </div> <div class="block big"><Block3/></div> <div class="block big"><h2>区域4</h2></div> <div class="block mid"><Block5/></div> <div class="block"><h2>区域6</h2></div> </div> </template> <script> import {getAsyncComponent} from "../util/index.js"; const Block3 = getAsyncComponent("../components/Block3.vue"); const Block5 = getAsyncComponent("../components/Block5.vue"); export default { components:{ Block3, Block5 }, } </script>
三、异步页面
Home 页面也需要异步加载,页面本质上也是组件
道理是一样的,
使用 defineAsyncComponent 封装一个定义异步组件的方法
src/util/index.js
import { defineAsyncComponent, h } from "vue"; import Loading from "../components/loading.vue"; import Error from "../components/Error.vue"; import "nprogress/nprogress.css"; import NProgress from "nprogress" NProgress.configure({ trickleSpeed: 50, showSpinner: false, color: "red" }) export function delay(duration){ if(duration){ duration = getRandom(1000, 5000); } return new Promise((resolve) => { setTimeout(() => { resolve(); }, duration); }); } export function getRandom(min, max){ return Math.floor(Math.random() * (max - min)) + min; } // 得到一个异步页面 export function getAsyncPage(path){ return defineAsyncComponent({ loader: async () => { NProgress.start(); await delay(3000); const comp = await import(path); NProgress.done(); return comp; }, loadingComponent: Loading, }); } export function getAsyncComponent(path){ return defineAsyncComponent({ loader: async () => { await delay(3000); if(Math.random() < 0.5){ throw new TypeError(); } return import(path); }, loadingComponent: Loading, errorComponent: { render(){ return h(Error, "出错了"); }, }, }); }
路由配置这里调用异步组件的方法
src/router/routes.js
import {getAsyncPage} from "../util/index.js"; // 导入异步组件 const Home = getAsyncPage("../views/Home.vue"); const About = getAsyncPage("../views/About.vue"); export default [ {path: "/", component: Home}, // 这里的就是异步组件了 {path: "/about", component: About}, ];
四、进度条
https://github.com/rstacruz/nprogress
npm i nprogress
测试进度条
1. 导入 nprogress.css
2. 导入 NProgress 对象
main.js
import { createApp } from 'vue' import App from './App.vue' import router from './router'; // 导入路由 import "nprogress/nprogress.css"; import NProgress from "nprogress" NProgress.configure({ trickleSpeed: 50, // 进度条增长速度 showSpinner: false, // 去掉小圈圈 }) window.NProgress = NProgress; // NProgress.start(); // NProgress.done(); // 应用插件 createApp(App).use(router).mount('#app');
五、Teleport
蒙层的位置,组件的逻辑结构非常合理,但是真实的 dom 元素结构是不合理的,
组件结果表示逻辑解构,逻辑解构非常的合理,
但是真实的 dom 结构非常的不合理,蒙层的 div 最好出现 body 里面的最后是最合理的
Teleport 会把里面生成的真实 dom,放到 css 选择器选中的元素里面
<Teleport to="css选择器"> </Teleport>
Home.vue
<template> <div class="container"> <div class="block"> <h2>区域1</h2> <p> <button @click="visibleModal = true">打开蒙层</button> </p>{{isShowModal}} <Teleport to="body"> <Modal v-if="visibleModal"> <button @click="visibleModal = false">关闭蒙层</button> </Modal> </Teleport> </div> <div class="block mid"> <h2>区域2</h2> </div> <div class="block big"><Block3/></div> <div class="block big"><h2>区域4</h2></div> <div class="block mid"><Block5/></div> <div class="block"><h2>区域6</h2></div> </div> </template> <script> import {ref} from "vue"; import Modal from "../components/Modal.vue"; import {getAsyncComponent} from "../util/index.js"; const Block3 = getAsyncComponent("../components/Block3.vue"); const Block5 = getAsyncComponent("../components/Block5.vue"); export default { components:{ Block3, Block5, Modal }, setup(){ const visibleModal = ref(false); return { visibleModal, } } } </script> <style scoped></style>
蒙层组件
src/components/Modal.vue
<template> <div class="modal"> <slot></slot> </div> </template> <script> </script> <style scoped> .modal{ display: flex; justify-content: center; align-items: center; position: fixed; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); left: 0; top: 0; } </style>