指令
指令是 Vue 模板语法里的特殊标记,在使用上和 HTML 的 data-* 属性十分相似,统一以 v- 开头( e.g. v-html )。
它以简单的方式实现了常用的 JavaScript 表达式功能,当表达式的值改变的时候,响应式地作用到 DOM 上。
内置指令
vue 提供了一些内置指令可以直接使用,例如:
<template>
<!-- 渲染一段文本 -->
<span v-text="msg"></span>
<!-- 渲染一段 HTML -->
<div v-html="html"></div>
<!-- 循环创建一个列表 -->
<ul v-if="items.length">
<li v-for="(item, index) in items" :key="index">
<span>{{ item }}</span>
</li>
</ul>
<!-- 一些事件( `@` 等价于 `v-on` ) -->
<button @click="hello">Hello</button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const msg = ref<string>('Hello World!')
const html = ref<string>('<p>Hello World!</p>')
const items = ref<string[]>(['a', 'b', 'c', 'd'])
function hello() {
console.log(msg.value)
}
return {
msg,
html,
items,
hello,
}
},
})
</script>
内置指令在使用上都非常的简单,可以在官方文档的 内置指令 一章查询完整的指令列表和用法,在模板上使用时,请了解 指令的模板语法 。
TIP
有两个指令可以使用别名:
v-on 的别名是 @ ,使用 @click 等价于 v-on:click
v-bind 的别名是 : ,使用 :src 等价于 v-bind:src
自定义指令
如果 Vue 的内置指令不能满足业务需求,还可以开发自定义指令。
相关的 TS 类型
在开始编写代码之前,先了解一下自定义指令相关的 TypeScript 类型。
自定义指令有两种实现形式,一种是作为一个对象,其中的写法比较接近于 Vue 组件,除了 getSSRProps 和 deep 选项 外,其他的每一个属性都是一个 钩子函数 ,下一小节会介绍钩子函数的内容。
// 对象式写法的 TS 类型
// ...
export declare interface ObjectDirective<T = any, V = any> {
created?: DirectiveHook<T, null, V>
beforeMount?: DirectiveHook<T, null, V>
mounted?: DirectiveHook<T, null, V>
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
updated?: DirectiveHook<T, VNode<any, T>, V>
beforeUnmount?: DirectiveHook<T, null, V>
unmounted?: DirectiveHook<T, null, V>
getSSRProps?: SSRDirectiveHook
deep?: boolean
}
// ...
另外一种是函数式写法,只需要定义成一个函数,但这种写法只在 mounted 和 updated 这两个钩子生效,并且触发一样的行为。
// 函数式写法的 TS 类型
// ...
export declare type FunctionDirective<T = any, V = any> = DirectiveHook<
T,
any,
V
>
// ...
这是每个钩子函数对应的类型,它有 4 个入参:
// 钩子函数的 TS 类型
// ...
export declare type DirectiveHook<
T = any,
Prev = VNode<any, T> | null,
V = any
> = (
el: T,
binding: DirectiveBinding<V>,
vnode: VNode<any, T>,
prevVNode: Prev
) => void
// ...
钩子函数第二个参数的类型:
// 钩子函数第二个参数的 TS 类型
// ...
export declare interface DirectiveBinding<V = any> {
instance: ComponentPublicInstance | null
value: V
oldValue: V | null
arg?: string
modifiers: DirectiveModifiers
dir: ObjectDirective<any, V>
}
// ...
可以看到自定义指令最核心的就是 “钩子函数” 了,接下来来了解这部分的知识点。
钩子函数
和 组件的生命周期 类似,自定义指令里的逻辑代码也有一些特殊的调用时机,在这里称之为钩子函数:
TIP
因为自定义指令的默认写法是一个对象,
所以在代码风格上是遵循 Options API 的生命周期命名,
而非 Vue 3 的 Composition API 风格。
钩子函数在用法上就是这样子:
const myDirective = {
created(el, binding, vnode, prevVnode) {
// ...
},
mounted(el, binding, vnode, prevVnode) {
// ...
},
// 其他钩子...
}
在 相关的 TS 类型 已了解,每个钩子函数都有 4 个入参:
其中用的最多是 el 和 binding 了。
el 的值就是通过 document.querySelector 拿到的那个 DOM 元素。
binding 是一个对象,里面包含了以下属性:
在了解了指令的写法和参数作用之后,来看看如何注册一个自定义指令。
局部注册
自定义指令可以在单个组件内定义并使用,通过和 setup 函数 同级别的 directives 选项进行定义,可以参考下面的例子和注释:
<template>
<!-- 这个使用默认值 `unset` -->
<div v-highlight>{{ msg }}</div>
<!-- 这个使用传进去的黄色 -->
<div v-highlight="`yellow`">{{ msg }}</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
// 自定义指令在这里编写,和 `setup` 同级别
directives: {
// `directives` 下的每个字段名就是指令名称
highlight: {
// 钩子函数
mounted(el, binding) {
el.style.backgroundColor =
typeof binding.value === 'string' ? binding.value : 'unset'
},
},
},
setup() {
const msg = ref<string>('Hello World!')
return {
msg,
}
},
})
</script>
上面是对象式的写法,也可以写成函数式:
export default defineComponent({
directives: {
highlight(el, binding) {
el.style.backgroundColor =
typeof binding.value === 'string' ? binding.value : 'unset'
},
},
})
TIP
局部注册的自定义指令,默认在子组件内生效,子组件内无需重新注册即可使用父组件的自定义指令。
全局注册
自定义指令也可以注册成全局,这样就无需在每个组件里定义了,只要在入口文件 main.ts 里启用它,任意组件里都可以使用自定义指令。
请查看 开发本地 Vue 专属插件 一节的内容了解如何注册一个全局的自定义指令插件。
deep 选项
除了 钩子函数 ,在 相关的 TS 类型 里还可以看到有一个 deep 选项,它是一个布尔值,作用是:
如果自定义指令用于一个有嵌套属性的对象,并且需要在嵌套属性更新的时候触发 beforeUpdate 和 updated 钩子,那么需要将这个选项设置为 true 才能够生效。
<template>
<div v-foo="foo"></div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
export default defineComponent({
directives: {
foo: {
beforeUpdate(el, binding) {
console.log('beforeUpdate', binding)
},
updated(el, binding) {
console.log('updated', binding)
},
mounted(el, binding) {
console.log('mounted', binding)
},
// 需要设置为 `true` ,如果是 `false` 则不会触发
deep: true,
},
},
setup() {
// 定义一个有嵌套属性的对象
const foo = reactive({
bar: {
baz: 1,
},
})
// 2s 后修改其中一个值,会触发 `beforeUpdate` 和 `updated`
setTimeout(() => {
foo.bar.baz = 2
console.log(foo)
}, 2000)
return {
foo,
}
},
})
</script>
插槽
Vue 在使用子组件的时候,子组件在 template 里类似一个 HTML 标签,可以在这个子组件标签里传入任意模板代码以及 HTML 代码,这个功能就叫做 “插槽” 。
默认插槽
默认情况下,子组件使用 标签即可渲染父组件传下来的插槽内容,例如:
在父组件这边:
<template>
<Child>
<!-- 注意这里,子组件标签里面传入了 HTML 代码 -->
<p>这是插槽内容</p>
</Child>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Child from '@cp/Child.vue'
export default defineComponent({
components: {
Child,
},
})
</script>
在子组件这边:
<template>
<slot />
</template>
默认插槽非常简单,一个 就可以了。
具名插槽
有时候可能需要指定多个插槽,例如一个子组件里有 “标题” 、 “作者”、 “内容” 等预留区域可以显示对应的内容,这时候就需要用到具名插槽来指定不同的插槽位。
子组件通过 name 属性来指定插槽名称:
<template>
<!-- 显示标题的插槽内容 -->
<div class="title">
<slot name="title" />
</div>
<!-- 显示作者的插槽内容 -->
<div class="author">
<slot name="author" />
</div>
<!-- 其他插槽内容放到这里 -->
<div class="content">
<slot />
</div>
</template>
父组件通过 template 标签绑定 v-slot:name 格式的属性,来指定传入哪个插槽里:
<template>
<Child>
<!-- 传给标题插槽 -->
<template v-slot:title>
<h1>这是标题</h1>
</template>
<!-- 传给作者插槽 -->
<template v-slot:author>
<h1>这是作者信息</h1>
</template>
<!-- 传给默认插槽 -->
<p>这是插槽内容</p>
</Child>
</template>
v-slot:name 有一个别名 #name 语法,上面父组件的代码也相当于:
<template>
<Child>
<!-- 传给标题插槽 -->
<template #title>
<h1>这是标题</h1>
</template>
<!-- 传给作者插槽 -->
<template #author>
<h1>这是作者信息</h1>
</template>
<!-- 传给默认插槽 -->
<p>这是插槽内容</p>
</Child>
</template>
TIP
在使用具名插槽的时候,子组件如果不指定默认插槽,
那么在具名插槽之外的内容将不会被渲染。
默认内容
可以给 slot 标签添加内容,例如 默认内容 ,当父组件没有传入插槽内容时,会使用默认内容来显示,默认插槽和具名插槽均支持该功能。
注意事项
有一条规则需要记住:
父组件里的所有内容都是在父级作用域中编译的
子组件里的所有内容都是在子作用域中编译的
CSS 样式与预处理器
Vue 组件的 CSS 样式部分,Vue 3 保留着和 Vue 2 完全一样的写法。
编写组件样式表
最基础的写法,就是在 .vue 文件里添加一个 style 标签,即可在里面写 CSS 代码了。
<template>
<div>
<!-- HTML 代码 -->
</div>
</template>
<script lang="ts">
// TypeScript 代码
</script>
<style>
/* CSS 代码 */
.msg {
width: 100%;
}
.msg p {
color: #333;
font-size: 14px;
}
</style>
动态绑定 CSS
动态绑定 CSS ,在 Vue 2 就已经存在了,在此之前常用的是 :class 和 :style ,现在在 Vue 3 ,还可以通过 v-bind 来动态修改了。
使用 :class 动态修改样式名
它是绑定在 DOM 元素上面的一个属性,跟 class=“class-name” 这样的属性同级别,它非常灵活!
TIP
使用 :class 是用来动态修改样式名,
也就意味着必须提前把样式名对应的样式表先写好!
假设已经提前定义好了这几个变量:
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const activeClass = 'active-class'
const activeClass1 = 'active-class1'
const activeClass2 = 'active-class2'
const isActive = true
return {
activeClass,
activeClass1,
activeClass2,
isActive,
}
},
})
</script>
如果只想绑定一个单独的动态样式,可以传入一个字符串:
<template>
<p :class="activeClass">Hello World!</p>
</template>
如果有多个动态样式,也可以传入一个数组:
<template>
<p :class="[activeClass1, activeClass2]">Hello World!</p>
</template>
还可以对动态样式做一些判断,这个时候传入一个对象:
<template>
<p :class="{ 'active-class': isActive }">Hello World!</p>
</template>
多个判断的情况下,记得也用数组套起来:
<template>
<p :class="[{ activeClass1: isActive }, { activeClass2: !isActive }]">
Hello World!
</p>
</template>
那么什么情况下会用到 :class 呢?
最常见的场景,应该就是导航、选项卡了,比如要给一个当前选中的选项卡做一个突出高亮的状态,那么就可以使用 :class 来动态绑定一个样式。
<template>
<ul class="list">
<li
class="item"
:class="{ cur: index === curIndex }"
v-for="(item, index) in 5"
:key="index"
@click="curIndex = index"
>
{{ item }}
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const curIndex = ref<number>(0)
return {
curIndex,
}
},
})
</script>
<style scoped>
.cur {
color: red;
}
</style>
这样就简单实现了一个点击切换选项卡高亮的功能。
使用 :style 动态修改内联样式
如果觉得使用 :class 需要提前先写样式,再去绑定样式名有点繁琐,有时候只想简简单单的修改几个样式,那么可以通过 :style 来处理。
默认的情况下,都是传入一个对象去绑定:
1.key 是符合 CSS 属性名的 “小驼峰式” 写法,或者套上引号的短横线分隔写法(原写法),例如在 CSS 里,定义字号是 font-size ,那么需要写成 fontSize 或者 ‘font-size’ 作为它的键。
2.value 是 CSS 属性对应的 “合法值”,比如要修改字号大小,可以传入 13px 、0.4rem 这种带合法单位字符串值,但不可以是 13 这样的缺少单位的值,无效的 CSS 值会被过滤不渲染。
<template>
<p
:style="{
fontSize: '13px',
'line-height': 2,
color: '#ff0000',
textAlign: 'center',
}"
>
Hello World!
</p>
</template>
如果有些特殊场景需要绑定多套 style,需要在 script 先定义好各自的样式变量(也是符合上面说到的那几个要求的对象),然后通过数组来传入:
<template>
<p :style="[style1, style2]">Hello World!</p>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const style1 = {
fontSize: '13px',
'line-height': 2,
}
const style2 = {
color: '#ff0000',
textAlign: 'center',
}
return {
style1,
style2,
}
},
})
</script>
使用 v-bind 动态修改 style
当然,以上两种形式都是关于 script 和 template 部分的操作,如果觉得会给模板带来一定的维护成本的话,不妨考虑这个新方案,将变量绑定到 style 部分去。
TIP
请注意这是一个在 3.2.0 版本之后才被归入正式队列的新功能!
如果需要使用它,请确保的 vue 的版本号在 3.2.0 以上,
最好是保持最新版本。
先来看看基本的用法:
<template>
<p class="msg">Hello World!</p>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const fontColor = ref<string>('#ff0000')
return {
fontColor,
}
},
})
</script>
<style scoped>
.msg {
color: v-bind(fontColor);
}
</style>
如上面的代码,将渲染出一句红色文本的 Hello World!
这其实是利用了现代浏览器支持的 CSS 变量来实现的一个功能(所以如果打算用它的话,需要提前注意一下兼容性噢,点击查看:CSS Variables 兼容情况 )。
它渲染到 DOM 上,其实也是通过绑定 style 来实现,可以看到渲染出来的样式是:
<p class="msg" data-v-7eb2bc79="" style="--7eb2bc79-fontColor:#ff0000;">
Hello World!
</p>
对应的 CSS 变成了:
.msg[data-v-7eb2bc79] {
color: var(--7eb2bc79-fontColor);
}
理论上 v-bind 函数可以在 Vue 内部支持任意的 JavaScript 表达式,但由于可能包含在 CSS 标识符中无效的字符,因此官方是建议在大多数情况下,用引号括起来,如:
.text {
font-size: v-bind('theme.font.size');
}
由于 CSS 变量的特性,因此对 CSS 响应式属性的更改不会触发模板的重新渲染(这也是和 :class 与 :style 的最大不同)。
TIP
不管有没有开启 <style scoped> ,使用 v-bind 渲染出来的 CSS 变量,都会带上 scoped 的随机 hash 前缀,避免样式污染(永远不会意外泄漏到子组件中),所以请放心使用!
如果对 CSS 变量的使用还不是很了解的话,可以先阅读一下相关的基础知识点。
相关阅读:使用 CSS 自定义属性(变量) - MDN
样式表的组件作用域
CSS 不像 JS ,是没有作用域的概念的,一旦写了某个样式,直接就是全局污染。所以 BEM 命名法 等规范才应运而生。
但在 Vue 组件里,有两种方案可以避免出现这种污染问题:一个是 Vue 2 就有的 style scoped ,一个是 Vue 3 新推出的 style module 。
Style Scoped
Vue 组件在设计的时候,就想到了一个很优秀的解决方案,通过 scoped 来支持创建一个 CSS 作用域,使这部分代码只运行在这个组件渲染出来的虚拟 DOM 上。
使用方式很简单,只需要在 style 上添加 scoped 属性:
<!-- 注意这里多了一个 `scoped` -->
<style scoped>
.msg {
width: 100%;
}
.msg p {
color: #333;
font-size: 14px;
}
</style>
编译后,虚拟 DOM 都会带有一个 data-v-xxxxx 这样的属性,其中 xxxxx 是一个随机生成的 Hash ,同一个组件的 Hash 是相同并且唯一的:
<div class="msg" data-v-7eb2bc79>
<p data-v-7eb2bc79>Hello World!</p>
</div>
而 CSS 则也会带上与 HTML 相同的属性,从而达到样式作用域的目的。
.msg[data-v-7eb2bc79] {
width: 100%;
}
.msg p[data-v-7eb2bc79] {
color: #333;
font-size: 14px;
}