Go to comments

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>




Leave a comment 0 Comments.

Leave a Reply

换一张