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(); }, },
-
root
-
我们可以通过 $parent 来访问父元素。
-
App.vue 的实现,因为 App.vue 是我们的父组件和根组件:
getParentAndRoot() { console.log(this.$parent); console.log(this.$root); },
-
在 Vue3 中已经移除了 $children 的属性;
认识生命周期
-
生命周期的含义
- 每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程;
- 在这个过程中的某一个阶段,用于可能会想要添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据);
- Vue 提供了组件的生命周期函数;
-
生命周期函数
- 生命周期函数是一些钩子函数,在某个时间会被 Vue 源码内部进行回调;
- 通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段;
- 那么就可以在该生命周期中编写属于自己的代码逻辑了;
缓存组件的生命周期
-
对于缓存的组件来说,再次进入时,组件是不会执行 create 或者 mounted 等生命周期函数的:
-
有时候我们确实希望监听到何时重新进入到了组件,何时离开了组件;
-
可以使用 activated 和 deactivated 这两个生命周期钩子函数来监听;
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;
},
},
},
\