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 之后才加载的

    ![截屏2022-03-27 上午12.08.22](/Users/lucasy/Desktop/截屏2022-03-27 上午12.08.22.png)

    我们来用一些手段模拟一下

    新增一个 文件夹 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 意思是去哪。