组件样式的特性
Scoped CSS之局部样式的泄露
示例(vue3):
父组件:
<template>
<h4>App Title</h4>
<hello-world></hello-world>
</template>
<script>
import HelloWorld from './HelloWorld.vue';
export default {
name: 'App',
components: {
HelloWorld
}
};
</script>
<style scoped>
h4 {
text-decoration: underline;
}
</style>
HelloWorld子组件:
<template>
<h4>Hello World1</h4>
</template>
<script>
export default {
name: 'HelloWorld'
}
</script>
<style scoped></style>
结果如图所示:
结论:子组件的根节点会同时受到父组件的作用域样式和子组件的作用域样式的影响。
为了避免这种局部样式泄露的问题,可以采用如下方式:
- 1.尽量减少标签选择器的使用,多使用class选择器。
- 2.在每个子组件的根元素中添加唯一的class选择器。
- 3.在子组件中使用多个根元素,也可以在template中添加多个根元素,Vue.js 3已经支持这种方式
上面HelloWorld子组件调整下代码如下:
<template>
<div class="hello-world">
<h4>Hello World1</h4>
</div>
</template>
<script>
export default {
name: 'HelloWorld'
}
</script>
<style scoped></style>
结果如图所示:
这样父组件h4的样式就不会影响子组件的h4标签样式。
Scoped CSS之深度选择器
有时候需要在父组件的局部样式中修改子组件的某个元素的样式,这时可以使用深度选择器:deep()
这个伪类实现。
我们在HelloWorld.vue组件中添加一个class为msg的元素,示例代码如下:
<template>
<div class="hello-world">
<h4 class="msg">Hello World1</h4>
</div>
</template>
<script>
export default {
name: 'HelloWorld'
}
</script>
<style scoped></style>
在父组件中添加如下样式:
<template>
<h4>App Title</h4>
<hello-world></hello-world>
</template>
<script>
import HelloWorld from './HelloWorld.vue';
export default {
name: 'App',
components: {
HelloWorld
}
};
</script>
<style scoped>
h4 {
text-decoration: underline;
}
/* 深度选择器:选中子组件class为msg的元素 */
:deep(.msg) {
text-decoration: underline;
}
</style>
结果如图所示:
CSS Modules
当组件的<style>
标签中带有module属性时,标签会被编译为CSS Modules
,并将生成的CSS类
作为$style
对象的键暴露给组件。
<template>
<div class="hello-world">
<p :class="$style.red">This should be red</p>
</div>
</template>
<script>
export default {
name: 'HelloWorld'
}
</script>
<style module>
/* red CSS 类会作为$style对象的键,即$style.red */
.red {
color: red;
}
</style>
CSS Modules 这种方式在vue3项目中用得比较少。
在CSS中使用v-bind
在vue.js 3.2版本之前,v-bind语法是一个实验性的功能,在vuejs 3.2版本之后,v-bind功能已经稳定。
示例如下:
<template>
<div class="example">
<h4 class="red">hello should be red</h4>
<h4 class="green">hello should be green</h4>
<h4 class="yellow">hello should be yellow</h4>
</div>
</template>
<script>
export default {
name: 'example',
data () {
return {
color1: 'red',
color2: 'green'
}
},
computed: {
color3 () {
return 'yellow'
}
}
}
</script>
<style>
/* 动态绑定样式,也属于局部样式。与style标签是否绑定 scoped 属性没有关系 */
.red {
color: v-bind(color1)
}
.green {
color: v-bind(color2)
}
.yellow {
color: v-bind(color3)
}
</style>
页面渲染结果如图:
DOM渲染截图:
实际上,他们的值会被编译成hash的CSS自定义property,CSS本身仍然是静态的。自定义property会通过内联样式的方式应用到组件的根元素上,如上截图所示,并且在源值变更时响应式更新。和前面的属性一样,它的CSS只会应用到当前组件的元素上。
非props属性继承
例如像id,name,class这样没有定义的props属性,在组件中没有通过props传递,但是这对应的属性也继承到了子组件的根元素上,示例代码:
父组件:
<template>
<no-prop-attribute id="coder" class="why" name="codername"></no-prop-attribute>
</template>
<script>
import NoPropAttribute from './NoPropAttribute.vue';
export default {
name: 'App',
components: {
NoPropAttribute
}
};
</script>
<style scoped></style>
子组件:
<template>
<div class="no-prop-attribute">
该子组件没有定义任何的props属性
</div>
</template>
<script>
export default {
name: 'NoPropAttribute'
}
</script>
结果渲染如图所示:
如果不希望组件的根元素继承属性,那么在组件中设置inheritAttrs: false
即可。
调整子组件代码:
<template>
<div class="no-prop-attribute">
该子组件没有定义任何的props属性
</div>
</template>
<script>
export default {
name: 'NoPropAttribute',
inheritAttrs: false
}
</script>
渲染结果如图所示:
我们可以在子组件中通过$attr访问所有非props的属性,子组件示例代码如下:
<template>
<div class="no-prop-attribute">
该子组件没有定义任何的props属性
<h4 :class="$attrs.class" :id="$attrs.id">{{ $attrs.name }}</h4>
</div>
</template>
<script>
export default {
name: 'NoPropAttribute',
inheritAttrs: false
}
</script>
渲染结果如图所示:
组件通信
父子组件的相互通信props/$emit
父组件传递数据给子组件
略
子组件传递数据给父组件
自定义事件参数与自定义事件验证示例。
父组件:
<template>
<div>
<h4>当前计数:{{ counter }}</h4>
<counter-operation @add="addOne" @sub="subOne" @addN="addNNum"></counter-operation>
</div>
</template>
<script>
import CounterOperation from './CounterOperation.vue';
export default {
components: {
CounterOperation
},
data () {
return {
counter: 0
}
},
methods: {
addOne () {
this.counter++;
},
subOne () {
this.counter--;
},
addNNum (num, name, age) {
console.log(name, age);
this.counter += num;
}
}
}
</script>
子组件CounterOperation.vue:
<template>
<div>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<input type="text" v-model.number="num">
<button @click="incrementN">+n</button>
</div>
</template>
<script>
export default {
// 1.数组写法
// emits: ['add', 'sub', 'addN'],
// 2.对象写法
emits: {
add: null,
sub: null,
addN: (num, name, age) => {
if (num > 10) {
// 如果num大于10,则验证通过
return true;
}
// 如果num小于10,则返回false,控制台会出现参数验证不通过的警告,但是不影响程序的运行。
return false;
}
},
data () {
return {
num: 0
}
},
methods: {
increment () {
this.$emit('add');
},
decrement () {
this.$emit('sub');
},
incrementN () {
this.$emit('addN', this.num, "why", 18);
}
}
}
</script>
非父子组件的相互通信
Provide/inject
如下示例,
新建三个vue文件,App.vue(根组件)、Home.vue(子组件)、HomeContent.vue(孙子组件)。
HomeContent.vue组件:
<!-- 孙子组件 -->
<template>
<div class="home-content">
home-content
<p>{{ name }} - {{ age }} - {{ friends }}</p>
</div>
</template>
<script>
export default {
inject: ['name', 'age', 'friends']
}
</script>
Home.vue:
<!-- 子组件 -->
<template>
<div class="home">
home
<home-content></home-content>
</div>
</template>
<script>
import HomeContent from './HomeContent.vue'; // 子组件中导入孙子组件
export default {
name: 'Home',
components: {
HomeContent
}
}
</script>
App.vue:
<!-- 根组件 -->
<template>
<div id="app">
App
<home></home>
<button @click="addFriend">新增朋友</button>
</div>
</template>
<script>
import Home from './Home.vue';
export default {
name: 'App',
components: {
Home
},
provide () {
return {
name: 'why',
age: 20,
friends: this.friends
}
},
data () {
return {
friends: ["jerry", "tom"]
}
},
methods: {
addFriend () {
this.friends.push("jack");
console.log(this.friends);
}
}
}
</script>
运行结果:
当我们点击“新增朋友”,视图效果:
从图上可以知道提供的friends属性是响应式的数据。
如果我们在孙子组件中获取friends的长度,孙子组件跟根组件代码。
孙子组件HomeContent.vue:
<!-- 孙子组件 -->
<template>
<div class="home-content">
home-content
<p>{{ name }} - {{ age }} - {{ friends }} - {{ friendLength }}</p>
</div>
</template>
<script>
export default {
inject: ['name', 'age', 'friends', "friendLength"]
}
</script>
根组件App.vue:
<!-- 根组件 -->
<template>
<div id="app">
App
<home></home>
<button @click="addFriend">新增朋友</button>
</div>
</template>
<script>
import Home from './Home.vue';
export default {
name: 'App',
components: {
Home
},
provide () {
return {
name: 'why',
age: 20,
friends: this.friends,
friendLength: this.friends.length
}
},
data () {
return {
friends: ["jerry", "tom"]
}
},
methods: {
addFriend () {
this.friends.push("jack");
console.log(this.friends);
}
}
}
</script>
初始渲染结果如图:
当我们点击了“新增朋友”,渲染结果如图:
从图上可以知道,当我们修改了friends后,孙子组件中注入的friendLength属性并未随之改变。这是因为修改了friends之后,之前在provide中映入的this.friends.length属性本身并不是响应式数据。
如果想要响应式数据,我们使用vuejs 3提供的computed API,修改App.vue组件,让friendLength属性接收一个计算属性,代码如下调整:
<!-- 根组件 -->
<template>
<div id="app">
App
<home></home>
<button @click="addFriend">新增朋友</button>
</div>
</template>
<script>
import { computed } from 'vue';
import Home from './Home.vue';
export default {
name: 'App',
components: {
Home
},
provide () {
return {
name: 'why',
age: 20,
friends: this.friends,
friendLength: computed(() => this.friends.length)
}
},
data () {
return {
friends: ["jerry", "tom"]
}
},
methods: {
addFriend () {
this.friends.push("jack");
console.log(this.friends);
}
}
}
</script>
最终效果如图,当我们点击了“新增朋友”,对应的长度也变化了:
全局事件总线
事件总线(mitt)是对发布/订阅模式的一种实现,它是一种集中式事件处理机制,允许 vue.js 3 应用程序中的不同组件之间相互通信,无需相互依赖,就可以达到解耦的目的。
在vue.js 3中,可以使用事件总线作为组件之间传递数据的桥梁。所有组件都可以共用同一个事件中心,从而向其他任意组件发送或者接收事件,实现上下同步通知。
Vue.js 3中移除了实例中的on
、off
、$once
方法。如果需要继续使用全局事件总线,则官方推荐第三方库来实现,如mitt或tiny-emitter。
这儿以mitt为例。
首先,安装mitt,执行如下命令:
npm i mitt -S
其次,可以封装一个工具eventbus.js,用于同一导出emitter对象,代码如下所示:
import mitt from 'mitt';
// 1.创建emitter对象
const emitter = mitt();
// 2.也可以创建多个emitter对象
const emitter2 = mitt();
export default emitter;
emitter对象常用的API如下:
- 1.发送(或触发)事件的API
// 参数1:事件名称(string|symbol类型)
// 参数2:发送事件时传递的数据(any类型,推荐对象)
emitter.emit('why',{name: 'why',age: 18});
- 2.监听事件的API。注意:监听的事件名需要和触发的事件名一致
// 这里监听全局的why事件
// 参数1:事件名称
// 参数2:监听事件的回调函数,data是触发事件时传递过来的参数
emitter.on('why', (data) => {
console.log("why:", data);
});
-
3.如果在某些情况下需要取消事件,那么可以使用下面的API
- 3.1 取消emitter中所有的监听
emitter.all.clear();
- 3.2 取消某一个事件,但需要先定义一个函数
function onFoo() {} emitter.on('foo', onFoo) // listen emitter.off('foo', onFoo) // unlisten
使用示例
实现如下图所示的跨组件的通信。
我们分别新建App.vue、Home.vue、HomeContent.vue和About.vue组件以及utils/eventbus.js文件。
utils/eventbus.js文件,用于封装事件总线工具,代码如下:
import mitt from 'mitt';
// 创建emitter对象
const emitter = mitt();
export default emitter;
About.vue组件,负责发送全局事件,代码如下:
<!-- About.vue 子组件 -->
<template>
<div class="about">
About
<button @click="btnClick">单击按钮 触发事件</button>
</div>
</template>
<script>
import emitter from './utils/eventbus';
export default {
name: 'About',
methods: {
btnClick () {
console.log('1. About页面的:单击按钮-》触发全局why事件');
emitter.emit('why', { name: 'why', age: 20 });
console.log('2. About页面的:单击按钮-》触发全局kobe事件');
emitter.emit('kobe', { name: 'kobe', age: 20 });
}
}
}
</script>
HomeContent.vue组件,负责监听全局的事件,代码如下:
<!-- HomeContent.vue 孙子组件 -->
<template>
<div class="home-content">
homeContent
</div>
</template>
<script>
import emitter from './utils/eventbus';
export default {
created () {
// 监听全局的why事件
emitter.on('why', (data) => {
console.log('why:', data);
});
// 监听全局的kobe事件
emitter.on('kobe', (data) => {
console.log('kobe:', data);
});
// 监听all事件
emitter.on('*', (type, data) => {
console.log('* listener:', type, data);
});
}
}
</script>
Home.vue组件,负责导入HomeContent.vue组件,代码如下:
<!-- Home.vue 子组件 -->
<template>
<div class="home">
home
<home-content></home-content>
</div>
</template>
<script>
import HomeContent from './HomeContent.vue'; // 子组件中导入孙子组件
export default {
name: 'Home',
components: {
HomeContent
}
}
</script>
App.vue组件,负责导入、注册和使用Home.vue、About.vue组件,代码如下:
<!-- App.vue 根组件 -->
<template>
<div id="app">
App
<home />
<about />
</div>
</template>
<script>
import Home from './Home.vue';
import About from './About.vue';
export default {
name: 'App',
components: {
Home,
About
}
}
</script>
组件插槽
作用域插槽
示例代码,ShowNames.vue子组件,示例代码如下:
<template>
<div class="show-names">
<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>
父组件App.vue,示例代码如下:
<!-- App.vue 根组件 -->
<template>
<div id="app">
<show-names :names="names">
<template v-slot:default="slotProps">
<span>{{ slotProps.item }} {{ slotProps.index }} - </span>
</template>
</show-names>
</div>
</template>
<script>
import ShowNames from './ShowNames.vue';
export default {
name: 'App',
components: {
ShowNames
},
data () {
return {
names: ['why', 'kobe', 'jeck', 'tom']
}
}
}
</script>
展示效果:
独占默认插槽
对于默认插槽(即name="default"),在使用时可以将v-slot:default="slotProps"
简写为v-slot="slotProps"
我们把上面父组件App.vue代码调整下修改下,如下所示:
<!-- App.vue 根组件 -->
<template>
<div id="app">
<show-names :names="names">
<template v-slot="slotProps">
<span>{{ slotProps.item }} {{ slotProps.index }} - </span>
</template>
</show-names>
</div>
</template>
最终渲染结果跟上面是一样的。
在只有默认插槽时,组件的标签可以被当做插槽的模板(template)使用,这样就可以将v-slot
直接用在组件上,即省略template
元素,修改App.vue组件,将v-slot="slotProps"
写到组件上,代码如下:
<!-- App.vue 根组件 -->
<template>
<div id="app">
<show-names :names="names" v-slot="slotProps">
<span>{{ slotProps.item }} {{ slotProps.index }} - </span>
</show-names>
</div>
</template>
但是如果组件同时具备默认插槽和具名插槽,那么必须按照template
的语法来编写。
动态组件
动态组件的实现与传参
实现原理就是通过内置的动态组件实现,即使用<component>
组件,并通过其特殊属性is
动态渲染不同的组件,这里的is
属性用于指定组件的名称。
新建App.vue(父组件)、page/Home.vue(子组件)、page/About.vue(子组件)、page/Category.vue(子组件).
App.vue示例代码:
<template>
<div>
<button v-for="item in tabs" :key="item" @click="itemClick(item)" :class="{ active: item === currentTab }">
{{ item }}
</button>
<component :is="currentTab" name="coder" :age="20" @pageClick="pageClick"></component>
</div>
</template>
<script>
import Home from "./page/Home.vue";
import About from "./page/About.vue";
import Category from "./page/Category.vue";
export default {
components: {
Home,
About,
Category
},
data () {
return {
tabs: ['home', 'about', 'category'],
currentTab: 'home'
}
},
methods: {
itemClick (item) {
this.currentTab = item
},
pageClick (value) {
console.log(value);
}
}
}
</script>
<style scoped>
.active {
color: red;
}
</style>
page/Home.vue:
<template>
<div @click="divClick">Home组件: {{ name }} - {{ age }}</div>
</template>
<script>
export default {
name: 'home',
props: {
name: {
type: String,
default: ''
},
age: {
type: Number,
default: 0
}
},
emits: ['pageClick'], // 该组件触发了pageClick事件
methods: {
divClick () {
this.$emit('pageClick', 'Home组件触发了单击')
}
}
}
</script>
page/About.vue:
<template>
<div @click="divClick">About组件: {{ name }} - {{ age }}</div>
</template>
<script>
export default {
name: 'about',
props: {
name: {
type: String,
default: ''
},
age: {
type: Number,
default: 0
}
},
emits: ['pageClick'], // 该组件触发了pageClick事件
methods: {
divClick () {
this.$emit('pageClick', 'About组件触发了单击')
}
}
}
</script>
page/Category.vue:
<template>
<div @click="divClick">Category组件: {{ name }} - {{ age }}</div>
</template>
<script>
export default {
name: 'category',
props: {
name: {
type: String,
default: ''
},
age: {
type: Number,
default: 0
}
},
emits: ['pageClick'], // 该组件触发了pageClick事件
methods: {
divClick () {
this.$emit('pageClick', 'Category组件触发了单击')
}
}
}
</script>
实现效果:
如上面示例home组件
的名称为字符串home
,当currentTab
为home
字符串时,显示<home>
组件。