由于组件化特殊,需采用
vue cli
创建项目。如何安装 Vue cli 请见初识 Vue3
注册组件分成两种:
注册全局组件的流程:
main.js
中引入并注册该组件通常使用组件的时候采用的都是局部注册,因为全局组件就算有些没有被使用,在打包时也会被一起打包,增加包的大小。
<!-- ComponentA 组件 -->
<template>
<h2>这是局部组件 A</h2>
</template>
<!-- ComponentB 组件 -->
<template>
<h2>这是局部组件 B</h2>
</template>
<!-- 组件 C 使用 A 和 B 两个组件 -->
<template>
<h1>这个是App组件</h1>
<ComponentA />
<ComponentB />
</template>
<script>
// 引入组件
import ComponentA from '@/components/ComponentA.vue';
import ComponentB from '@/components/ComponentB.vue';
export default {
name: 'App',
// 注册局部组件
components: {
// 这里是 ComponentA:ComponentA 的简写
ComponentA,
ComponentB,
},
};
</script>
MyComponent
注册后,在 template
中使用时可以写作<my-component />
或<MyComponent />
[!WARNING] 局部引入的组件,一定要在
components
方法中进行注册才能使用。components
内部是以[key: string]: value
的格式书写
当我们想要引用一个嵌套极深的文件时。需要../../../../utils/test
,这对于开发是很友好的。 vue.config.js
,该文件会和webpack
的默认配置进行合并,从而实现对webpack
的修改。而我们想要解决路径嵌套的问题,首先需要在webpack
中进行配置。 第一步: 在vue.config.js
中写入configureWebpack
进行修改默认配置
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
resolve: {
// 配置路径别名
// @ 是内置已经配置好的路径别名:对应的是 src 的路径
alias: {
utils: '@/utils',
},
},
},
});
当我们配置完之后,就可以使用import {} from 'utils/test'
进行直接使用。虽然配置生效了,但是 Vscode 是没有智能提示的,原因是 Vscode 并不知道配置了该路径别名。 第二步: 在js.config.json
中进行配置。js.config.json
是针对于 Vscode 的文件,以便给与更友好的提示
{
"compilerOptions": {
// ...
"paths": {
"@/*": ["src/*"],
"utils/*": ["src/utils"]
}
// ...
}
}
父子组件之间进行通信的两种方式:
组件最大的优势就是可复用性,不同的信息需要相似的结构去展示时,就可以将相似的结构封装为一个组件。并动态展示不同的数据信息。
attribute
attribute
赋值,子组件通过 attribute
的名称接收和使用对应的值;props
属性进行接收这些传递过来的属性。使用方式和data
一致<template>
<h1>这里是父组件</h1>
<InfoItem name="张三" sex="男" age="18" />
<InfoItem name="李四" sex="女" age="20" />
<InfoItem />
</template>
<script>
import InfoItem from './components/InfoItem.vue';
export default {
name: 'App',
components: {
InfoItem,
},
};
</script>
<template>
<ul>
<li>姓名:{{ name }}</li>
<li>性别:{{ sex }}</li>
<li>年龄:{{ age }}</li>
</ul>
</template>
<script>
export default {// 必须通过 props 接收
props: ['name', 'sex', 'age']
}
Props 有两种常见的用法:
方式一:字符串数组,数组中的字符串就是 attribute
的名称;
方式二:对象类型,对象类型我们可以在指定 attribute
名称的同时,指定它需要传递的类型、是否是必须的、默认值等等;
数组类型只能接收,不能对接收到的值进行限制,比如,要求传入的值必须是 Array,必须为 Number 类型
export default {
props: ['name', 'sex', 'age'],
};
常用写法:
export default {
props: {
// 限制 name 为 字符串,若不传 name,则显示 默认值 default
name: {
type: String,
default: '我是默认Name',
},
// sex 属性为必传项
sex: {
type: String,
default: '人妖',
required: true,
},
// 仅仅限制 age 为 数字类型
age: Number,
},
};
当子组件要求
age
字段必须为数字时,父组件的<InfoItem age="18" />
就需要改为<InfoItem :age="18" />
type 的类型有: String、Number、Boolean、Array、Object、Date、Function、Symbol
对象类型常用写法汇总:
export default {
props: {
A: String,
// 基础的类型检查(null 和 undefined 会通过任何类型验证)
B: Number,
// 多个可能的类型
C: [String, Number],
// 必填的字符串
D: {
type: String,
required: true,
},
// 带有默认值的数字
E: {
type: Number,
default: 100,
},
},
};
对象类型其它写法汇总:
export default {
props: {
// 带有默认值的对象
A: {
type: Object,
// 对象 或 数组 默认值必须从一个工厂函数中获取
// 缩写:default: ()=> ({ message: "hello" })
default() {
return { message: 'hello' };
},
},
// 自定义验证函数
B: {
validator(value) {
// 这个值必须匹配下列字符串中的一个
return ['message', 'warning', 'danger'].includes(value);
},
},
// 具有默认值的函数
C: {
type: Function,
// 与对象默认值不同,这不是一个工厂函数-----这是一个用作默认值的函数,这个函数就是默认值,而非返回值
default() {
return 'Default function';
},
},
},
};
props 的大小写命名: HTML 中的 attribute
名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符;这意味着当你使用 DOM 中的模板时,attribute
使用驼峰命名和短横线连接命名都可以,但props
接收处,必须是驼峰命名
<InfoItem messageInfo="张三" />
<!-- 等价于 -->
<InfoItem message-info="张三" />
什么是非 Prop 的 Attribute 呢?当我们传递给一个组件某个属性,但是该属性并没有定义对应的 props
或者 emits
时,就称之为 非 Prop 的 Attribute;常见的包括 class、style、id
属性等;
禁用 Attribute 继承禁止非 props 的 attribute 添加到子组件根节点上,可以在使用inheritAttrs: false
来禁止
export default {
inheritAttrs: false,
// props: ["name", "sex", "age"]
};
指定某个节点继承attribute
若要指定某个节点继承父组件的非 props 的 attribute
,可以使用 $attrs
来访问所有的 非 props 的 attribute
属性
<!-- <h2 age="18"></h2> -->
<h2 :age="$attrs.age"></h2>
<!-- <h2 name="张三"></h2> -->
<h2 :name="$attrs.name"></h2>
<!-- <h2 class="name age sex"></h2> -->
<h2 :class="$attrs"></h2>
当子组件有一些事件触发,比如子组件中发生了点击,父组件需要切换不同的内容,或者子组件有一些内容需要传递给父组件时,使用子传父。
**案例:**父组件显示counter
值,子组件内部的按钮点击时,父组件需要更新counter
值 **思路:**给按钮绑定点击事件,当点击时,我们需要去通知父组件更改counter
的值,父组件在得到通知后,需要做出对应的操作去完成此次响应。
@click="btnClick(1)
,当点击时,我们可以拿到点击需要加减的值count
count
,使用$emit
$emit
可以接收两个参数,(参数 1:自定义事件,参数 2:传递给父组件的值)
<!-- AddCount.vue -->
<template>
<div class="add">
<h4>Add子组件</h4>
<button @click="btnClick(1)">点击+1</button>
<button @click="btnClick(5)">点击+5</button>
<button @click="btnClick(10)">点击+10</button>
</div>
</template>
<script>
export default {
methods: {
btnClick(count) {
// 让子组件发出一个自定义事件
// 参数1:自定义的事件名称,参数2:传递的参数
// 当触发 addClick 方法时,addClick会得到 count
this.$emit('addClick', count);
},
},
};
</script>
$emit
的第一个参数addClick
,书写格式和@click=""
基本一致@click
一样,是一个需要触发的事件handleClick
,毕竟子组件通知父组件后,父组件需要做出相应的操作handleClick
会默认得到子组件传递过来的count
,可以直接拿到count
值,此时就可以进行加减操作<!-- App.vue -->
<template>
<div class="parent">
<h1>父组件Count:{{ counter }}</h1>
<!-- addClick 就是 $emit 中的第一个参数 handleClick 就是 需要做出操作的函数 -->
<AddCount @addClick="handleClick" />
</div>
</template>
<script>
import AddCount from './components/AddCount.vue';
export default {
name: 'App',
data() {
return {
counter: 0,
};
},
components: { AddCount },
methods: {
// 父组件接收子组件传递过来的数据,并作出操作完成此次通知
handleClick(count) {
console.log('子组件传递过来的count', count);
this.counter += count;
},
},
};
</script>
内部其实是监听按钮的点击,点击之后通过
this.$emit
的方式发出去事件
[!ATTENTION] 切勿通过 props 将 count 值传递给子组件,让子组件进行修改,这违反了单向数据流原则
emits
是在 Vue3 中新增的,我们可以把所有需要通知的事件写在emits
中,可以提高代码的可读性,在 Vscode 中在子组件标签上写这些自定义事件会有代码提示
export default {
// 1、数组语法
// emits: ["addClick"],
// 2、对象语法,可以对参数进行验证
emits: {
addClick: function (count) {
if (count <= 5) return true;
return false;
},
},
methods: {
btnClick(count) {
this.$emit('addClick', count);
},
},
};
当自定义验证生效时,控制台会报出警告,说是一个校验失败的自定义事件
在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信。传递的层级过深时,使用Provide
和Inject
如果Provide
中提供的一些数据是来自 data,那么我们可能会想要通过 this 来获取
provide 一般都写成函数写法
当我们使用 provide 给子组件传递数据时,我们更希望该数据是响应式的
解决方法:使用computed
computed
是 vue3 的新特性,且computed
返回的是一个ref
对象,需要取出其中的value
来使用
在 Vue2 中,官方内置了eventbus事件总线
,但 Vue3 从实例中移除了 $on、$off、 $once
方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库:
mitt
或 tiny-emitter
初始化
// main.js
// 入口文件加载时,直接在 VueComponent.propotype 上添加 $bus 实例
// 为了方便起见,可以把 EventBus 实例化为 $bus
var EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
$bus: {
get: function () {
return EventBus;
},
},
});
发送事件 A 页面点击,通知 B 页面
<!-- A.vue -->
<template>
<button @click="sendMsg()">-</button>
</template>
<script>
export default {
methods: {
sendMsg() {
this.$bus.$emit('toBMsg', '来自A页面的消息');
},
},
};
</script>
接收事件
<!-- B.vue -->
<template>
<p>{{ msg }}</p>
</template>
<script>
export default {
data() {
return {
msg: null,
};
},
mounted() {
// mounted 页面挂载完成就开始监听事件
// 监听的事件触发,则会接收到 发送过来的 数据
ths.$bus.$on('toBMsg', (msg) => {
// A发送来的消息
this.msg = msg;
});
},
};
</script>
移除事件监听通常在当前页面销毁时,移除该页面的事件监听this.$bus.$off('aMsg')
$emit
接收两个参数 this.$bus.$emit(event: string, callback: Function)
,参数一:自定义事件名,参数二:回调函数,若无特殊处理,可简写传递参数$on
接收两个参数 this.$bus.$on(eventName: string, callback: Function)
,参数一:需要监听的自定义事件名,参数二:回调函数,函数内部可接收发送方的数据。一般用在created | mounted
生命周期中$off
接收两个参数 this.$bus.$off(event: string | Array<string>, callback: Function)
,参数一:需要移除的自定义事件名,参数二:回调函数。一般用在unmounted
生命周期中
推荐在写
on 和
off 搭配使用。
Vue 中将 <slot>
元素作为承载分发内容的出口;在封装组件中,使用 特殊的元素 <slot>
就可以为封装组件开启一个插槽;该插槽插入什么内容取决于父组件如何使用;
当有一个组件有多个插槽时,为了防止插槽使用混乱,需要对其进行命名
<slot>
元素有一个特殊的 attribute:name
。v-slot
可以简写为#
v-slot
必须写在<template></template>
标签上最常用的是第三种写法
我们可以通过 v-slot:[dynamicSlotName]
方式动态绑定一个名称
为什么要使用作用域插槽?举例:
list 数组
进行循环时,如果使用插槽,内部的循环显示的标签是固定的,<template>
<div class="nav-bar">
<template v-for="item in list" :key="item.id">
<div class="item">
<!-- 正常写法 -->
<span>{{ item.title }}</span>
<!-- 插槽写法 -->
<!-- 这种写法是不成功的,父组件插入插槽中的内容会循环,导致的结果就是每一个 item 的内容一样 -->
<!-- 原因是:这里的 item 父组件是访问不到的,父组件传递过来的内容,会直接把 slot 的默认内容(循环项)替换掉 -->
<!-- <slot><span>{{ item.title }}</span></slot> -->
</div>
</template>
</div>
</template>
提问: 在这种情况下,该如何在子组件有插槽的循环中插入循环项的自定义内容 思路: 将插槽的当前项传递给父组件,供父组件访问
slot
标签绑定自定义attribute
将当前循环项插槽的数据传递给父组件,父组件通过v-slot:default="xxx"
(简写#default="xxx"
)接收,xxx
代表的是传递给父组件的所有数据,所以是一个对象,可以通过xxx.id、xxx.title
的格式访问这些数据。如果我们的插槽是默认插槽default
,那么在使用的时候 v-slot:default="props"
可以简写为 v-slot="props"
如果我们的插槽只有默认插槽时,组件的标签可以被当做插槽的模板来使用,我们就可以将 v-slot 直接用在组件上
如果我们有默认插槽和具名插槽,那么按照完整的 template 来编写
只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template>
的语法。具名插槽
<!-- 正常写法 -->
<NavBar :list="list">
<template v-slot:default="props">
<button>{{ props.title }}--{{ props.id }}</button>
</template>
</NavBar>
<!-- v-slot 通用简写 --------最常用 -->
<NavBar :list="list">
<template #default="props">
<button>{{ props.title }}--{{ props.id }}</button>
</template>
</NavBar>
<!-- 插槽时默认插槽 default,v-slot:default="props" 简写为 v-slot="props" -->
<NavBar :list="list">
<template v-slot="props">
<button>{{ props.title }}--{{ props.id }}</button>
</template>
</NavBar>
<!-- 只有默认插槽时,可以省略 template 模板,将 v-slot 写在组件上 -->
<NavBar :list="list" v-slot="props">
<button>{{ props.title }}--{{ props.id }}</button>
</NavBar>
多组件 lifecycle 执行顺序:
父子组件挂载阶段
父子组件更新阶段
父子组件卸载阶段
ref
的 attribute
属性$refs
属性:它一个对象 Object,持有注册过 ref attribute 的所有 DOM 元素和组件实例。ref="xxx"
绑定ref
,this.$refs.xxx
访问到对应的原生 DOM 节点<template>
<!-- ref 绑定元素,获取到的是原生DOM节点 -->
<h1 ref="titleRef">{{ title }}</h1>
<!-- ref 绑定组件,获取到的是组件实例 -->
<MyRef ref="componentRef" />
<button @click="btnClick">点击</button>
</template>
<script>
import MyRef from './components/MyRef.vue';
export default {
name: 'App',
data() {
return {
title: 'Hello World',
};
},
components: { MyRef },
methods: {
btnClick() {
// 获取到 元素节点,可以通过 .style、.innerHTML 等操作原生 DOM 的方式进行操作
console.log(this.$refs.titleRef);
// this.$refs.componentRef 就是组件实例,即 组件的 this
console.log(this.$refs.componentRef);
// 所以可以直接访问 data 中的数据
console.log(this.$refs.componentRef.message);
// 绑定组件可以通过 this.$refs.componentRef.$el 拿到组件的根元素
console.log(this.$refs.componentRef.$el);
// 若组件由多个根节点,获取到的是第一个 node 节点
// console.log(this.$parent) // 访问当前组件的父组件
// console.log(this.$root) // 获取根组件
// Vue3 中移除了 $children
},
},
};
</script>
<style></style>
当
ref
绑定元素时,this.$refs.xxx
获取到的是 DOM 节点。当ref
绑定 组件时,this.$ref.xxx
获取的是该组件的组件实例,可以直接访问内部的数据(data 中的数据,methods 中的方法等所有能够通过 this 访问的)
当我们需要动态显示组件(组件的切换)时,首先想到的就是v-if
,这种当组件判断过多时会显得臃肿,可以尝试第二种component
动态组件标签
component
标签内部通过特殊的attribute
:is
,来动态展示组件,is
内部就是需要显示的组件名称,前提是组件必须是全局组件或已被注册的局部组件
<template>
<div id="app">
<div class="navbar">
<template v-for="(item, index) in nav" :key="index">
<div :class="{ active: currntIndex === index }" @click="btnClick(index)">
{{ item.title }}
</div>
</template>
</div>
<!-- 方式一:普通写法,通过 v-if 判断当前需要显示的组件 -->
<MyHome v-if="currntIndex === 0" />
<MyAbout v-else-if="currntIndex === 1" />
<MyCenter v-else />
<!-- 方式二:component 动态组件 -->
<component :is="nav[currntIndex].component" />
</div>
</template>
<script>
import MyAbout from './components/MyAbout.vue';
import MyCenter from './components/MyCenter.vue';
import MyHome from './components/MyHome.vue';
export default {
name: 'App',
components: { MyHome, MyAbout, MyCenter },
data() {
return {
nav: [
{ title: '首页', component: 'MyHome' },
{ title: '中心', component: 'MyCenter' },
{ title: '关于', component: 'MyAbout' },
],
currntIndex: 0,
};
},
methods: {
btnClick(index) {
this.currntIndex = index;
},
},
};
</script>
组件通信,可以直接在 <component />
标签中传值,此时component
标签可以当做组件进行通信
<component :is="nav[currntIndex].component" @sendMsg="getMsg" :msg="msg" />
在切换组件后,切换前的组件会被销毁掉,再次回来时会重新创建组件,所有的元素及状态都会重新编译,若需要在切换组件时依旧让组件不被销毁,可以使用keep-alive
keep-alive
内部包裹的组件默认都会被缓存
<KeepAlive>
<!-- 这里的组件当切换时,不会销毁和重新创建 -->
<component :is="nav[currntIndex].component" />
</KeepAlive>
include
只有名称匹配的组件会被缓存,写法:string | RegExp | Array<string>
exclude
任何名称匹配的组件都不会被缓存,用法和 include
一致
最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁。写法:number | string
created
或者 mounted
等生命周期函数的:
activated
和 deactivated
这两个生命周期钩子函数来监听;对于某些组件若需要通过异步的方式来进行加载(目的是可以对其进行分包处理),Vue 中提供了一个函数:defineAsyncComponent
。
defineAsyncComponent 接受两种类型的参数:
Promise
对象import { defineAsyncComponent } from 'vue';
const AsyncMyHome = defineAsyncComponent(() => import('@/components/MyHome.vue'));
export default {
name: 'App',
components: { AsyncMyHome },
};
const AsyncMyHome = defineAsyncComponent({
// 工厂函数
loader:()=>import("@/components/MyHome.vue"),
// 加载过程中显示的组件
loadingComponent: Loading,
// 加载失败时显示的组件
errorComponent: Error,
// 在显示 loading 组件之前的延迟 默认值:200ms
delay: 2000,
// 如果提供了 timeout,并且 加载组件 的时间超过了设定值,将显示 错误组件
// 默认值:Infinity (永不超时,单位ms)
timeout:1000 * 60,
// 定义组价是否可挂起 默认值:true
suspensible: true
});
在 input 中可以使用v-model
来完成双向绑定。vue 也支持在组件上使用v-model
;
<MyHome v-model="message" />
<!-- 等价于 -->
<MyHome :modelValue="message" @update:modelValue="message = $event" />
<UserName v-model:firstName="first" v-model:lastName="last" />
<template>
<input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" />
<input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
</template>
<script>
export default {
props: ["firstName","lastName"]
emits: ["update:firstName", "update:lastName"],
};
</script>
mixins
对相同的代码逻辑进行抽取mixins
对象的选项将被 混合 进入该组件本身的选项当mixins
对象中的选项和组件对象中的选项发生了冲突,Vue 分成不同的情况来进行处理
methods、components 和 directives
,将被合并为同一个对象
mixins
的methods
中定义了btnClick
,组件的methods
中也定义了btnClick
,则最终会使用组件内部的btnClick
组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的 mixin
const app = createApp(App);
app.mixin({
create() {
console.log('global mixin created');
},
});
全局混入,一旦注册,将会影响到所有的组件