Vue3组件化开发
组件化开发1
组件拆分
前面我们是将所有的逻辑放到一个 App.vue 中:
- 在之前的案例中,我们只是 创建了一个组件 App
- 如果我们一个应用程序 将所有的逻辑都放在一个组件 中,那么这个组件就会变成 非常的臃肿和难以维护
- 所以组件化的核心思想应该是 对组件进行拆分,拆分成 一个个小的组件
- 再 将这些组件组合嵌套在一起,最终形成 我们的应用程序
按照如上的拆分方式后,我们开发对应的逻辑只需要去对应的组件编写就可
组件通信
- 可开启也可以不开启自动引入组件
- App 组件是 Header、Main、Footer 组件的父组件
- Main 组件是 Banner、ProductList 组件的父组件
在开发过程中,我们会经常遇到需要组件之间相互进行通信:
- 比如 App 可能使用了多个 Header,每个地方的 Header展示的内容不同,那么我们就需要使用者 传递给 Header 一些数据,让其进行展示
- 又比如我们在 Main 中一次性 请求了 Banner 数据和 ProductList 数据,那么就需要传递给它们来进行展示
- 也可能是 子组件中发生了事件,需要 由父组件来完成某些操作,那就需要 子组件向父组件传递事件
父子组件之间如何进行通信:
- 父组件传递给子组件:通过 props 属性
- 子组件传递给父组件:通过 $emit 触发事件
什么是 props
- props 是你可以在组件上 注册一些自定义的 attribute
- 父组件给 这些 attribute 赋值,子组件通过 attribute 的名称获取到对应的值
Props 用法
- 方式一:字符串数组,数组中的字符串就是 attribute 的名称
- 方式二:对象类型,对象类型我们可以在指定 attribute 名称的同时,指定它需要传递的类型、是否是必须的、默认值等等
数组用法
export default {
props: ['title', 'content']
}
对象用法
- 数组用法中我们 只能说明传入的 attribute 的名称,并 不能对其进行任何形式的限制
当使用对象语法的时候,我们可以对传入的内容限制更多:
- 比如指定传入的 attribute 的类型
- 比如指定传入的 attribute 是否是必传的
- 比如指定没有传入时,attribute 的默认值
export default {
props: {
title: String,
content: {
type: String,
required: true,
default: '123'
}
}
}
Props 细节补充
type 类型
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
对象类型其他写法
export default {
props: {
// 基础的类型检查(null 和 undefined 会通过任何类型验证)
propA: Number,
// 多个可能得类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
default() {
// 对象或数组默认值必须从一个工厂函数获取
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator(value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].includes(value)
}
},
// 具有默认值的函数
propG: {
type: Function,
// 与对象或数组默认值不同,这不是一个工厂函数——这是一个用作默认值的函数
default() {
return 'Default Function'
}
}
}
}
prop 大小写命名
- HTML 中的 attribute 名是大小写不敏感 的,所以 浏览器会把所有大写字符解释为小写字符
- 这意味着当你使用 DOM 中的模板 时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名
<show-message messageInfo="哈哈"></show-message>
<show-message message-info="哈哈"></show-message>
非 prop 的 attribute
- 当我们 传递一个组件某个属性,但是 该属性并没有定义对应的 props 或者 emits 时,就称为 非 Prop 的 Attribute
- 常见的包括 class、style、id 属性 等
Attribute 继承
- 当组件有单个根节点时,非 Prop 的 Attribute 将自动添加到根节点的 Attribute 中
禁用 Attribute 继承
如果我们 不希望组件的根元素继承 attribute,可以在组件中设置
inheritAttrs: false
禁用 attribute 继承的常见情况是 需要将 attribute应用于根元素之外的其他元素
jsexport default { inheritAttrs: false }
我们可以通过 $attrs 来访问所有的非 props 的 attribute
- 多个根节点的 attribute 如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上
<template>
<div :class="$attrs.class">NotPropAttribute组件1</div>
<div>NotPropAttribute组件2</div>
</template>
子组件传递给父组件
什么情况下子组件需要传递内容到父组件呢?
- 当 子组件有一些事件发生 的时候,比如在组件中发生了点击,父组件需要切换内容
- 子组件 有一些内容想要传递给父组件 的时候
如何进行操作?
- 首先,我们需要在 子组件中定义好在某些情况下触发的事件名称
- 其次,在 父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中
- 最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件
export default {
emits: ["add", "sub", "addN"],
methods: {
increment() {
this.$emit('add')
},
decrement() {
this.$emit('sub')
}
}
}
在 vue3 当中,我们可以对传递的参数进行验证
export default {
emits: {
add: null,
sub: null,
addN: (num, name, age) => {
console.log(num, name, age)
if (num > 10) {
return true
}
return false
}
},
methods: {
incrementN() {
this.$emit('addN', this.num, 'why', 18)
}
}
}
组件化开发2
Provide/Inject
非父子组件的通信
- Provide/Inject
- Mitt 全局事件总线
Provide/Inject 用于非父子组件之间共享数据:
- 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容
- 在这种情况下,如果我们仍然将 props 沿着组件链逐级传递下去,就会非常的麻烦
对于这种情况下,我们可以使用 Provide 和 Inject
- 无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者
- 父组件有一个 provide 选项来提供数据
- 子组件有一个 inject 选项来开始使用这些数据
结构:App.vue -> Home.vue -> HomeComtent.vue
如果 Provide 提供的一些数据是来自 data,那么我们可能会想要通过 this 来获取
- 这时候会报错
export default {
// 报错
provide: {
length: this.names.length
},
// 正确用法
provide() {
return {
length: this.names.length
}
}
}
这时如果我们修改了 this.names 的内容,会发现子组件中是没有反应的
- 如果想让数据编程响应式的,可以使用响应式的一些 API 来完成这些功能,比如 computed 函数
- 我们在使用 length 的时候需要获取其中的 value,这时因为 computed 返回的是一个 ref 对象,需要取出其中的 value 来使用
Mitt 全局事件总线
Vue3 从实例中移除了
- Vue3 官方有推荐一些库,例如 mitt 或 tiny-emitter
npm install mitt
我们可以封装一个工具 eventbus.js:
import mitt from 'mitt'
const emitter = mitt()
export default emitter
使用事件总线工具
- 在 Home.vue 中监听事件
import emitter from './utils/eventbus'
export default {
created() {
emitter.on('why', info => {
console.log('why:', info)
})
emitter.on('kobe', info => {
console.log('kobe:', info)
})
// 监听所有事件
emitter.on('*', (type, info) => {
console.log('* listener:', type, info)
})
}
}
- 在 App.vue 中触发事件
import emitter from './utils/eventbus'
export default {
methods: {
btnClick() {
emitter.emit('why', { name: 'why', age: 18 })
}
}
}
在某些情况我们可能希望 取消掉之前注册的函数监听
// 取消emitter中所有的监听
emitter.all.clear()
// 定义一个函数
function onFoo() {}
emitter.on('foo', onFoo) // 监听
emitter.off('foo', onFoo) // 取消监听
Slot
在开发中,我们会经常封装一个个可复用的组件:
- 前面我们会 通过 props 传递 给组件一些数据,让组件来进行展示
- 但是为了让这个组件具备 更强的通用性,我们 不能将组件中的内容限制为固定的 div、span 等等这些元素
- 比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片
- 我们应该让使用者可以决定某一块区域到底存放什么内容和元素
举个栗子:假如我们定制一个通用的导航组件 NavBar
- 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定
- 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示
- 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等
- 右边可能是一个文字,也可能是一个图标,也可能什么都不显示
这个时候我们就可以来定义插槽 slot:
- 插槽的使用过程其实是抽取共性、预留不同
- 我们会将共同的元素、内容依然在组件内进行封装
- 同时会将不同的元素使用 slot 作为占位,让外部决定到底显示什么样的元素
如何使用 slot 呢?
- Vue 中将
<slot>
元素作为承载分发内容的出口 - 在封装组件中,使用特殊的元素
<slot>
就可以为封装组件开启一个插槽 - 该插槽插入什么内容取决于父组件如何使用
Slot 基本使用
我们在 App.vue 中使用它们:我们可以插入普通的内容、html 元素、组件元素,都可以是可以的
有时候我们希望在使用插槽时,如果没有插入对应的内容,那么我们需要显示一个默认的内容:
如果一个组件中含有多个插槽,我们插入多个内容时是什么效果?
具名插槽使用
- 具名插槽顾名思义就是给插槽起一个名字,
<slot>
元素有一个特殊的attribute:name
- 一个不带 name 的 slot,会带有隐含的名字 default
<nav-bar :name="name">
<template v-slot:left>
<button>左边的按钮</button>
</template>
<template v-slot:center>
<h2>我是标题</h2>
</template>
<template v-slot:right>
<i>右边的i元素</i>
</template>
</nav-bar>
<!-- NavBar.vue -->
<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>
动态插槽名
- 我们可以通过
v-slot:[dynamicSlotName]
方式动态绑定一个名称
<nav-bar :name="name">
<template v-slot:[name]>
<i>why内容</i>
</template>
</nav-bar>
<script>
export default {
data() {
return {
name: 'why'
}
}
}
</script>
<!-- NavBar.vue -->
<div class="addition">
<slot :name="name"></slot>
</div>
<script>
export default {
props: {
name: String
}
}
</script>
具名插槽的缩写
- 跟 v-on 和 v-bind 一样,v-slot 也有缩写
- 即把参数之前的所有内容 (v-slot:) 替换为字符 #
<template #left>
<button>左边的按钮</button>
</template>
作用域插槽
- 父级模板里的所有内容都是在父级作用域中编译的
- 子模板里的所有内容都是在子作用域中编译的
有时候我们希望插槽 可以访问子组件中的内容
- 当一个组件被用来渲染一个数组元素时,我们使用插槽,并且希望插槽中没有显示每项的内容
案例
- 在 App.vue 中定义好数据
- 传递给 ShowNames 组件中
- ShowNames 组件中遍历 names 数据
- 定义插槽的 prop
- 通过 v-slot:default 的方式获取到 slot 的 props
- 使用 slotProps 中的 item 和 index
<!-- ShowNames.vue -->
<template>
<template v-for="(item, index) in names" :key="item">
<slot :item="item" :index="index"></slot>
</template>
</template>
<script>
export default {
props: {
names: {
type: Array,
default: () => []
}
}
}
</script>
<!-- App.vue -->
<show-names :names="names">
<template v-slot="slotProps">
<strong>{{ slotProps.item }}-{{ slotProps.index }}</strong>
</template>
</show-names>
<script>
import ShowNames from './ShowNames.vue'
export default {
components: {
ShowNames
},
data() {
return {
names: ['why', 'kobe', 'james', 'curry']
}
}
}
</script>
默认插槽 default,那么在使用的时候 v-slot:default="slotProps"
可以简写为 v-slot="slotProps"
- 如果我们的插槽只有默认插槽时,组件的标签可以被当做插槽的模板来使用,这样,我们就可以将 v-slot 直接用在组件上
<show-names :names="names">
<template v-slot="slotProps">
<strong>{{ slotProps.item }}-{{ slotProps.index }}</strong>
</template>
</show-names>
<show-names :names="names" v-slot="slotProps">
<strong>{{ slotProps.item }}-{{ slotProps.index }}</strong>
</show-names>
如果我们有默认插槽和具名插槽,那么按照完整的 template 来编写
<show-names :names="names">
<template v-slot="coderwhy">
<button>{{ coderwhy.item }}-{{ coderwhy.index }}</button>
</template>
<template v-slot:why>
<h2>我是why的插入内容</h2>
</template>
</show-names>
组件化开发3
动态组件
点击一个tab-bar,切换不同的组件显示
- 方式一:通过 v-if 来判断,显示不同的组件
<template v-if="currentTab === 'home'">
<home></home>
</template>
<template v-else-if="currentTab === 'about'">
<about></about>
</template>
<template v-else>
<category></category>
</template>
方式二:动态组件的方式
可以是通过 component 函数注册的组件
在一个组件对象的 components 对象中注册的组件
<component :is="currentTab"> </component>
动态组件传值只需要我们将属性和监听事件放到 component 上来使用
<component :is="currentTab" name="coderwhy" :age="18" @pageClick="pageClick"> </component>
keep-alive
在开发中某些情况我们希望继续保持组件的状态,而不是销毁掉,这个时候我们就可以使用一个内置组件:keep-alive
<keep-alive include="home,about">
<component :is="currentTab" name="coderwhy" :age="18" @pageClick="pageClick"> </component>
</keep-alive>
keep-alive 有一些属性:
- include -
string | RegExp | Array
。只有名称匹配的组件会被缓存 - exclude -
string | RegExp | Array
。任何名称匹配的组件都不会被缓存 - max -
number | string
。最多可以缓存多少个组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁
include 和 exclude prop 允许组件有条件的缓存:
- 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示
- 匹配首先检查组件自身的 name 选项
异步组件
默认的打包过程:
- 默认情况下,在构建整个组件树的过程中,因为组件和组件之间是 通过模块化直接依赖 的,那么 webpack 在打包时就会将组件模块打包在一起(比如一个 app.js 文件中)
- 这时候随着 项目不断庞大,app.js 文件的内容过大,会造成首屏渲染速度变慢
打包时,代码的分包:
- 对于一些 不需要立即使用的组件,我们可以 单独对它们进行拆分,拆分成一些 小的代码块 chunk.js
- 这些 chunk.js 会在 需要时从服务器加载下载,并且运行代码,显示对应的内容
- 通过 import 函数导入的模块, 后续 webpack 对其进行打包的时候就会进行分包的操作
如果我们的项目过大了,对于某些组件我们希望通过异步的方式来(目的是可以对其进行分包处理),那么 Vue 中给我们提供了一个函数:defineAsyncComponent
defineAsyncComponent 接收两种类型的参数:
- 类型一:工厂函数,该工厂函数需要返回一个 Promise 对象
- 类型二:接收一个对象类型,对异步函数进行配置
import { defineAsyncComponent } from 'vue'
// 写法一:参数为函数
const AsyncCategory = defineAsyncComponent(() => import('./AsyncCategory.vue'))
// 写法二:参数为对象
const AsyncCategoryObj = defineAsyncComponent({
// 工厂函数
loader: () => import('./Foo.vue'),
// 加载异步组件时要使用的组件
loadingComponent: LoadingComponent,
// 加载失败时要使用的组件
errorComponent: ErrorComponent,
// 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
delay: 200,
// 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
// 默认值:Infinity(即永不超时,单位 ms)
timeout: 3000,
// 定义组件是否可挂起 | 默认值:true
suspensible: false,
})
Suspense
Suspense 是一个试验性的新特性,其 API 可能随时会发生变动
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 元素和组件实例
<template>
<!-- 绑定到一个元素上 -->
<h2 ref="title">哈哈哈</h2>
<!-- 绑定到一个组件实例上 -->
<nav-bar ref="navBar"></nav-bar>
<button @click="btnClick">获取元素</button>
</template>
<script>
export default {
methods: {
btnClick() {
console.log(this.$refs.navBar.message)
console.log(this.$refs.navBar.$el)
}
}
}
</script>
我们可以通过 $parent
来访问父元素:
- 这里我们也可以通过
$root
来实现,因为 App 是我们的根组件
注意:在 Vue3 中已经移除了 $children
的属性,所以不可以使用了
<template>
<button @click="getParentAndRoot">获取父组件和根组件</button>
</template>
<script>
export default {
methods: {
getParentAndRoot() {
console.log(this.$parent)
console.log(this.$root)
}
}
}
</script>
生命周期
什么是生命周期呢?
- 每个组件都可能会经历从 创建、挂载、更新、卸载 等一系列的过程
- 在这个过程中的 某一个阶段,用于可能会想要 添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据)
生命周期函数:
- 生命周期函数是一些钩子函数,在某个时间会被 Vue 源码内部进行回调
- 通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段
- 那么我们就可以在该生命周期中编写属于自己的逻辑代码了
对于缓存的组件来说,再次进入时,我们是不会执行 created 或者 mounted 等生命周期函数的:
- 但是有时候我们确实希望监听到何时重新进入到了组件,何时离开了组件
- 这个时候我们可以使用 activated 和 deactivated 这两个生命周期钩子函数来监听;
组件的 v-model
为了我们的 MyInput 组件可以正常工作,这个组件内部的 <input>
必须:
- 将其 value attribute 绑定到一个名叫 modelValue 的 prop 上
- 在其 input 事件触发时,将新的值通过 自定义的 update:modelValue 事件 抛出
如果我们希望组件内部按照双向绑定的做法去完成,我们可以使用计算属性的 setter 和 getter 来完成
<template>
<div>
<!-- 1.通过input -->
<!-- <input :value="modelValue" @input="inputChange" /> -->
<!-- 2.直接绑定到 props 中是不对的 -->
<!-- <input v-model="modelValue" /> -->
<!-- 3.通过计算属性实现双向数据绑定 -->
<input v-model="value" />
</div>
</template>
<script>
export default {
props: {
modelValue: String
},
emits: ['update:modelValue'],
computed: {
value: {
set(value) {
this.$emit('update:modelValue', value)
},
get() {
return this.modelValue
}
}
},
methods: {
inputChange(event) {
this.$emit('update:modelValue', event.target.value)
}
}
}
</script>
绑定多个属性
- 默认情况下的v-model其实是绑定了 modelValue 属性和 @update:modelValue 的事件
- 如果我们希望绑定更多,可以给v-model传入一个参数,那么这个参数的名称就是我们绑定属性的名称
<hy-input v-model="message" v-model:title="title"></hy-input>
<script>
export default {
props: {
modelValue: String,
title: String
},
emits: ['update:modelValue', 'update:title']
}
</script>