Vue的组件化开发

60 阅读7分钟

Vue 的组件化开发

Provide 和 Inject

  • Provide/Inject 用于费父子组件之间共享数据

    • 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容;
    • 在这种情况下,如果我们仍然将 props 沿着链逐级传递下去,就会非常麻烦;
  • 对于这种情况,我们可以使用 Project 和 Inject:

    • 无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者;
    • 父组件有一个 provide 选项来提供数据;
    • 子组件有一个 inject 选项来开始使用这些数据;
  • 实际上,依赖注入可以看做是 “long range props”,除了:

    • 父组件不需要知道那些子组件使用它 provide 的 property;
    • 子组件不需要知道 inject 的 property 来自哪里;

Provide 和 Inject 函数的写法和处理响应式数据

  • 如果 Provide 中提供的一些数据是来自 data,那么我们可能会通过 this 来获取:
<script>
import Home from "./Home.vue";
import { computed } from "vue";
​
export default {
  components: {
    Home,
  },
  provide() {
    return {
      name: "why",
      age: 18,
      length: computed(() => this.names.length),
    };
  },
  data() {
    return {
      names: ["abc", "cba", "nba"],
    };
  },
  methods: {
    addName() {
      this.names.push("why");
    },
  },
};
</script>

全局事件总线 mitt 库

  • 安装

    npm install mitt

  • 其次,可以封装一个 eventbus.js 工具

import mitt from "mitt";
​
const emitter = mitt();
​
export default emitter;
​
  • mitt 的监听

    export default {
      methods: {
        btnClick() {
          console.log("about按钮的点击");
          emitter.emit("why", { name: "why", age: 18 });
        },
      },
    }
    
  • mitt 的触发

    export default {
      created() {
        emitter.on("why", (info) => {
          console.log(info);
        });
      },
    };
    
  • mitt 的取消

    emitter.off();   // 第一种方法
    ​
    emitter.all.clear();  // 第二种方法
    

认识插槽 Slot

  • 在开发中,我们经常封装一个个可复用的组件:

    • 我们可以通过 props 传递给组件一些数据,让组件来进行展示;
    • 但是为了让这个组件具备更强的通用性,不能将组建中的内容限制为固定的 div、span 等元素;
    • 比如某些情况下我们使用组件,希望组件显示的是一个按钮,某些情况我们使用组件希望显示的是一张图片;
  • 举个例子:假如我们定制一个通用的导航组件 - NavBar

    • 这个组件分成三块区域:左 - 中 - 右,每块区域的内容是不固定的;
    • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
    • 中间区域可能现实一个搜索框,也可能是一个列表,也可能是一个标题等;
    • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;

如何使用插槽 Slot

  • 可以自定义插槽 Slot

    • 插槽的使用过程其实是抽取共性,预留不同;
    • 我们会将共同的元素、内容依旧在组件内进行封装;
    • 同时将不同的元素使用 Slot 作为占位,让外部决定到底显示什么样的元素;
  • 如何使用 Slot

    • Vue 中将 元素作为承载分发内容的出口;
    • 在封装组件中,使用特殊的元素 就可以封装组件开启一个插槽;
    • 该插槽插入什么内容取决于父组件如何使用;

插槽的基本使用

  • 创建一个组件 MySlotCpn.vue:该组件中有一个插槽,我们可以在插槽中放入需要显示的内容;
  • 我们在 App.vue 中使用它们:我们可以插入普通的元素、html 元素、组件元素等各种元素;
<template>
  <div>
    <h2>组件开始</h2>
    <slot></slot>
    <h2>组件结束</h2>
  </div>
</template>
<template>
  <div>
    <my-slot-cpn>
      <button>我是按钮</button>
    </my-slot-cpn>
​
    <my-slot-cpn> 我是普通的文本 </my-slot-cpn>
​
    <my-slot-cpn>
      <my-button />
    </my-slot-cpn>
  </div>
</template>

插槽的默认内容

  • 我们希望在使用插槽时,如果没有插入对应的内容,需要显示一个默认的内容:

    • 当然这个默认的内容只会在没有提供插入的内容时才会显示;
    <slot>
      <i>我是默认的i元素</i>
    </slot>

多个插槽的效果

  • 如果一个组件中含有多个插槽,默认情况下每个插槽都会获取到插入的内容;

具名插槽的使用

  • 事实上,我们希望达到的效果是插槽对应的显示,可以使用具名插槽:

    • 元素有一个特殊的 attribute:name;
    • 一个不带 name 的 slot ,会带有隐含的名字 default;
<template>
  <div>
    <nav-bar>
      <template v-slot:left>
        <button>左边的按钮</button>
      </template>
      <template v-slot:center>
        <h2>我是标题</h2>
      </template>
      <template v-slot:right>
        <i>右边的i元素</i>
      </template>
    </nav-bar>
  </div>
</template>
<template>
  <div class="nav-bar">
    <div class="left">
      <slot name="left"></slot>
    </div>
    <div class="center">
      <slot name="center"></slot>
    </div>
    <div class="right">
      <slot name="right"></slot>
    </div>
  </div>
</template>

动态插槽名

  • 目前我们使用的插槽明都是固定的;
  • 比如 v-slot:left、v-slot:center 等;
  • 可以通过 v-slot:[dynamicSlotName] 方式动态绑定一个名称;
<template v-slot:[name]></template>


data() {
    return {
        name: 'left',
    }
}

具名插槽的缩写

  • 具名插槽使用的缩写:

    • 把参数之前的所有内容(v-slot:)替换为字符 #
<template>
  <div>
    <nav-bar>
      <template #left>
        <button>左边的按钮</button>
      </template>
      <template #center>
        <h2>我是标题</h2>
      </template>
      <template #right>
        <i>右边的i元素</i>
      </template>
    </nav-bar>
  </div>
</template>

渲染作用域

  • 在 Vue 中有渲染作用域的概念:

    • 父级模板里的所有内容都是在父级作用域中编译的;
    • 子模板里的所有内容都是在子作用域中编译的;

认识作用域插槽

  • 我们希望插槽可以访问到子组件中的内容:

    • 当一个组件被用来渲染一个数组元素时,我们使用插槽,并且希望插槽中没有显示每项的内容;
    • Vue 给我们提供了作用域插槽;
<template>
  <div>
    <show-names :names="names">
      <template v-slot="slotProps">
        <button>{{ slotProps.item }}-{{ slotProps.index }}</button>
      </template>
    </show-names>

    <!-- 独占默认插槽 如果还有其他的具名插槽,那么默认插槽必须使用 template 书写-->
    <show-names :names="names" v-slot="slotProps">
      <button>{{ slotProps.item }}-{{ slotProps.index }}</button>
    </show-names>

    <show-names :names="names">
      <template v-slot="slotProps">
        <strong>{{ slotProps.item }}-{{ slotProps.index }}</strong>
      </template>
    </show-names>
  </div>
</template>

<script>
// import ChildCpn from "./ChildCpn.vue";
import ShowNames from "./ShowNames.vue";

export default {
  components: {
    // ChildCpn,
    ShowNames,
  },
  data() {
    return {
      names: ["why", "kobe", "james", "curry"],
    };
  },
};
</script>






<template>
  <div>
    <template v-for="(item, index) in names" :key="item">
      <slot :item="item" :index="index"></slot>
    </template>
  </div>
</template>
<script>
export default {
  props: {
    names: {
      type: Array,
      default: () => [],
    },
  },
};
</script>

独占默认插槽的缩写

  • 如果时默认插槽 default,在使用的时候 v-slot:default="slotProps"可以简写为 v-slot="slotProps":
    <show-names :names="names">
      <template v-slot="slotProps">
        <button>{{ slotProps.item }}-{{ slotProps.index }}</button>
      </template>
    </show-names>
  • 如果只存在默认插槽,组件的标签可以被当作插槽的模板来使用,可以直接将 v-slot 直接用在组件上;
    <show-names :names="names" v-slot="slotProps">
      <button>{{ slotProps.item }}-{{ slotProps.index }}</button>
    </show-names>
  • 如果还有其他的具名插槽,那么默认插槽必须使用 template 完整写法书写

切换组件案例

  • v-if 显示不同的组件

    <template>
      <div>
        <button
          v-for="item in tabs"
          :key="item"
          @click="itemClick(item)"
          :class="{ active: currentTab === item }"
        >
          {{ item }}
        </button>
    ​
       <template v-if="currentTab === 'home'">
          <home></home>
        </template>
    ​
    ​
        <template v-else-if="currentTab === 'about'">
          <about></about>
        </template>
    ​
        <template v-else>
          <category></category>
        </template>
      </div>
    </template>
    
  • 动态组件的实现

    • 使用 component 组件,通过一个特殊的 attribute is 来实现;

    • 这个 currentTab 的值内容

      • 可以使通过 component 函数注册的组件;
      • 在一个组件对象的 components 对象中注册的组件;
    <template>
      <div>
        <button
          v-for="item in tabs"
          :key="item"
          @click="itemClick(item)"
          :class="{ active: currentTab === item }"
        >
          {{ item }}
        </button>
    ​
        <component :is="currentTab"></component>
      </div>
    </template>
    

动态组件的传值

  • 我们需要将属性和监听事件放到 component 上来使用;

        <component
          :is="currentTab"
          name="coderwhy"
          :age="18"
          @pageClick="pageClick"
        ></component>
    

认识 keep-alive

  • 在默认情况下,我们在切换组件后,原先组件就会被销毁,再次回来时会重新创建组件;

  • 在开发中,某些情况我们希望继续保持组件的状态,可以使用内纸组件:keep-alive;

        <keep-alive>
          <component
            :is="currentTab"
            name="coderwhy"
            :age="18"
            @pageClick="pageClick"
          ></component>
        </keep-alive>
    

keep-alive 属性

  • keep-alive 有一些属性

    • include - string | RegExp (正则表达式) | Array。只有名称匹配才会被缓存;
    • exclude - string | RegExp 正则表达式) | Array。任何名称匹配的组件都不会被缓存;
    • max - number | string。最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁;
  • include 和 exclude prop 允许组件有条件地缓存:

    • 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示;
    • 匹配首先检查组件自身的 name 选项;
    <!-- name 属性为 home 或者 about 的组件会被缓存-->   
    <keep-alive include="home,about">
      <component
        :is="currentTab"
        name="coderwhy"
        :age="18"
        @pageClick="pageClick"
      ></component>
    </keep-alive>

Webpack 的代码分包

  • 默认的打包过程:

    • 默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的,那么 webpack 在打包时就会将组件模块打包到一起(比如一个 app.js 文件中);
    • 随着项目的不断增大,app.js 文件的内容过大,会造成首屏的渲染速度变慢;
  • 打包时,代码的分包:

    • 所以,对于一些不需要立即使用的组件,我们可以单独对它们进行拆分,拆分成一些晓得代码块 chunk.js;
    • 这些 chunk.js 会在需要时从服务器下载下来,并运行代码,显示对应的内容;
  • 代码:

    // 通过import函数导入的模块,后续webpack对其进行打包的时候就会进行分包的操作
    import("./12_异步组件的使用/utils/math").then((res) => {
      res.sum(20, 30);
    });
    ​
    

Vue 中实现异步组件

  • 如果项目过大,对于某些组件我们希望通过异步的方式来进行加载(目的是可以对其进行分包处理),那么 Vue 中给我们提供了一个函数:defineAsyncComponent。

  • defineAsyncComponent 接受两种类型的参数;

    • 类型一:工厂函数,该工厂函数需要返回一个 Promise 对象;
    • 类型二:接受一个对象类型,对异步函数进行配置;
  • 工厂函数的写法:

    <script>
    import { defineAsyncComponent } from "vue";
    const AsyncCategory = defineAsyncComponent(() => import("./AsyncCategory.vue"));
    export default {
      components: {
        AsyncCategory,
      },
    };
    </script>
    

异步组件和 Suspense

  • Suspense 是一个内置的全局组件,有两个插槽:

    • default:如果 default 可以显示,那么显示 default 的内容;
    • fallback:如果 default 无法显示,那么显示 fallback 插槽的内容;
    <suspense>
      <template #default>
        <async-category></async-category>
      </template>
      <template #fallback>
        <loading></loading>
      </template>
    </suspense>

$refs 的使用

  • 某些情况下,我们在组件中想要直接获取到元素对象或者子组件实例:

    • 在 Vue 开发中我们是不推荐进行 DOM 操作的;
    • 我们可以给元素或者组件绑定一个 ref 的 attribute 属性;
  • 组件实例有一个 $refs 属性:

    • 是一个对象 Object,持有注册过的 ref attribute 的所有 DOM 元素和组件实例。

        methods: {
          btnClick() {
            console.log(this.$refs.title);
            console.log(this.$refs.navBar.message);
            this.$refs.navBar.sayHello();
          },
        },
      

parentparent 和 root

  • 我们可以通过 $parent 来访问父元素。

  • App.vue 的实现,因为 App.vue 是我们的父组件和根组件:

        getParentAndRoot() {
          console.log(this.$parent);
          console.log(this.$root);
        },
    
  • 在 Vue3 中已经移除了 $children 的属性;

认识生命周期

  • 生命周期的含义

    • 每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程;
    • 在这个过程中的某一个阶段,用于可能会想要添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据);
    • Vue 提供了组件的生命周期函数;
  • 生命周期函数

    • 生命周期函数是一些钩子函数,在某个时间会被 Vue 源码内部进行回调;
    • 通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段;
    • 那么就可以在该生命周期中编写属于自己的代码逻辑了;

缓存组件的生命周期

  • 对于缓存的组件来说,再次进入时,组件是不会执行 create 或者 mounted 等生命周期函数的:

    • 有时候我们确实希望监听到何时重新进入到了组件,何时离开了组件;

    • 可以使用 activateddeactivated 这两个生命周期钩子函数来监听;

        activated() {
          console.log("about activated");
        },
        deactivated() {
          console.log("about deactivated");
        },
      

组件的 v-model

    <hy-input v-model="message" v-model:title="title"></hy-input>
​
​
    // 子组件代码 
    <input v-model="value" />
    <input v-model="why" />
​
​
  computed: {
    value: {
      set(value) {
        this.$emit("update:modelValue", value);
      },
      get() {
        return this.modelValue;
      },
    },
    why: {
      set(why) {
        this.$emit("update:title", why);
      },
      get() {
        return this.title;
      },
    },
  },

\