前言
这篇文章是上一篇文章的续文,让我们一起了解组件吧。
组件注册
全局组件与局部组件
组件名称首字母大写命名(MyComponentName)或者短横线分隔命名(my-component-name)
全局组件:
1 | Vue.component('my-component-name', { /* ... */ }) |
局部组件
注意一点:局部注册的组件在其子组件中不可用(也就是说局部组件只在当前注册的组件中可用,当前注册的组件的子组件不可用,要在子组件重新注册)。
1 | var ComponentA = { /* ... */ } |
为什么需要局部组件?
全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
模块系统
在读下文之前,先来了解一些前端模块化的知识吧一文带你了解前端模块化
在模块系统中局部注册
在模块系统中,组件总是以单文件的形式存在,而这些单文件我们习惯把它们放在components
文件夹下。如果你是有vue-cli
构建的项目,你们就会发现此规范。
1 | import ComponentA from './ComponentA' |
基础组件的自动化全局注册
可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。
全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生。
如果你恰好使用了 webpack (或在内部使用了 webpack
的 Vue CLI 3+
),那么就可以使用 require.context
只全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件 (比如 src/main.js) 中全局导入基础组件的示例代码:
1 | import Vue from 'vue' |
require.context是啥?
一个webpack的api,通过执行require.context
函数获取一个特定的上下文,主要用来实现自动化导入模块
,在前端工程中,如果遇到从一个文件夹引入很多模块的情况,可以使用这个api,它会遍历文件夹中的指定文件,然后自动导入,使得不需要每次显式的调用import导入模块;
require.context函数接受三个参数:
- directory {String} -读取文件的路径
- useSubdirectories {Boolean} -是否遍历文件的子目录
- regExp {RegExp} -匹配文件的正则
Prop
Prop的大小写
驼峰命名和短横线分隔命名,两个是可以互用的,相互等价。如果使用字符串模板就没有这个限制。
1 | Vue.component('blog-post', { |
1 | <!-- 在 HTML 中是 kebab-case 的 --> |
Prop类型
没有指定类型的情况:
1 | props: ['title', 'likes', 'isPublished', 'commentIds', 'author'] |
通常你希望每个 prop
都有指定的值类型。这时,你可以以对象形式列出 prop
,这些 property
的名称和值分别是 prop
各自的名称和类型,type
还可以是一个自定义的构造函数,并且通过 instanceof
来进行检查确认。
1 | props: { |
传递静态或动态 Prop
静态传值
1 | <blog-post title="My journey with Vue"></blog-post> |
动态态传值
1 | <blog-post :title="post.title"></blog-post> |
可传值的类型有 数字、布尔值、数组、对象等
Prop的单向数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。这里有两种常见的试图变更一个 prop 的情形:
- 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。
1
2
3
4
5
6props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
} - 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
1
2
3
4
5
6props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
Prop验证
多种验证方式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})非 Prop 的 Attribute
如果在一个组件中传递数据,二组件没有用prop去接收,那么这个数据就会成为非 Prop 的 Attribute。而这些 attribute 会被添加到这个组件的根元素上。
替换/合并已有的 Attribute
想象一下 <bootstrap-date-input>
的模板是这样的:
1 | <input type="date" class="form-control"> |
为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:
1 | <bootstrap-date-input |
在这种情况下,我们定义了两个不同的 class 的值:form-control
和 date-picker-theme-dark
;
对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入 type=”text” 就会替换掉 type=”date”,但是class
和 style
attribute 两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark
。
禁用 Attribute 继承
如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false。例如:
1 | Vue.component('my-component', { |
这尤其适合配合实例的 $attrs property 使用,该 property 包含了传递给一个组件的 attribute 名和 attribute 值,例如:
1 | { |
有了 inheritAttrs: false 和 $attrs,你就可以手动决定这些 attribute 会被赋予哪个元素。在撰写基础组件的时候是常会用到的:
1 | Vue.component('base-input', { |
注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。
自定义事件
事件名
事件名遵循完全匹配的机制,v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent
将会变成 v-on:myevent
——导致 myEvent
不可能被监听到。因此,我们推荐你始终使用 kebab-case
的事件名。
自定义v-model
一个组件上的 v-model
默认会利用名为 value
的 prop 和名为 input
的事件,但是像单选框、复选框等类型的输入控件可能会将 value
attribute 用于不同的目的。model 选项可以用来避免这样的冲突:
1 | Vue.component('base-checkbox', { |
现在在这个组件上使用 v-model 的时候:
1 | <base-checkbox v-model="lovingVue"></base-checkbox> |
这里的 lovingVue 的值将会传入这个名为 checked
的 prop。同时当 <base-checkbox>
触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。
注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。
将原生事件绑定到组件
你可能有很多次想要在一个组件的根元素
上直接监听一个原生事件。这时,你可以使用 v-on
的 .native
修饰符:
1 | <base-input v-on:focus.native="onFocus"></base-input> |
v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素
1 | Vue.component('base-input', { |
.sync
修饰符
prop的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。
我们推荐以 update:myPropName
的模式触发事件取而代之。举个例子,在一个包含 title
prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:
1 | this.$emit('update:title', newTitle) |
然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。例如:
1 | <text-document |
为了方便起见,我们为这种模式提供一个缩写,即 .sync
修饰符:
1 | <text-document v-bind:title.sync="doc.title"></text-document> |
注意带有
.sync
修饰符的v-bind
不能和表达式一起使用 (例如v-bind:title.sync=”doc.title + ‘!’”
是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似v-model
。
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:
1 | <text-document v-bind.sync="doc"></text-document> |
这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。
将
v-bind.sync
用在一个字面量的对象上,例如v-bind.sync=”{ title: doc.title }”
,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
插槽
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即
v-slot
指令)。它取代了slot
和slot-scope
这两个目前已被废弃但未被移除且仍在文档中的 attribute。
插槽内容
Vue 实现了一套内容分发的 API,将 <slot>
元素作为承载分发内容的出口。
它允许你像这样合成组件:
1
2
3<navigation-link url="/profile">
Your Profile
</navigation-link>
然后你在 <navigation-link>
的模板中可能会写为:
1
2
3
4
5
6<a
v-bind:href="url"
class="nav-link"
>
<slot></slot>
</a>
当组件渲染的时候,<slot></slot>
将会被替换为“Your Profile”。插槽内可以包含任何模板代码,包括 HTML。
如果 <navigation-link>
的 template 中没有包含一个 <slot>
元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
编译作用域
当你想在一个插槽中使用数据时,例如:
1 | <navigation-link url="/profile"> |
该插槽跟模板的其它地方一样可以访问相同的实例 property (也就是相同的“作用域”),而不能访问 <navigation-link>
的作用域。例如 url 是访问不到的。
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
后备内容(默认内容)
有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 <submit-button>
组件中:
1 | <button type="submit"> |
现在当我在一个父级组件中使用 <submit-button>
并且不提供任何插槽内容时:
1 | <submit-button></submit-button>//内容默认为Submit |
具名插槽
具名插槽:有时我们需要多个插槽,这样就可以使用<slot>
元素中的一个特殊的 attribute:name
,这个 attribute 可以用来定义额外的插槽。
一个不带 name
的 <slot>
出口会带有隐含的名字“default”。
1 | <div class="container"> |
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
1 | <base-layout> |
现在 <template>
元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot
的 <template>
中的内容都会被视为默认插槽的内容(也就是没有没有名称的插槽 name="default"
)。
注意
v-slot
只能添加在<template>
上 (只有一种例外情况),这一点和已经废弃的slot
attribute 不同。
作用域插槽
有时让插槽内容能够访问子组件中才有的数据是很有用的。例如,设想一个带有如下模板的 <current-user>
组件:
1 | <span> |
我们可能想换掉备用内容,用名而非姓来显示。如下:
1 | <current-user> |
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
然而上述代码不会正常工作,因为只有<current-user>
组件可以访问到user
而我们提供的内容是在父级渲染的。
为了让user
在父级的插槽内容中可用,我们可以将user
作为<slot>
元素的一个 attribute 绑定上去:
1 | <span> |
绑定在 <slot>
元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
1 | <current-user> |
独占默认插槽的缩写语法
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot
直接用在组件上:
1 | <current-user v-slot:default="slotProps"> |
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot
被假定对应默认插槽:
1 | <current-user v-slot="slotProps"> |
注意默认插槽的缩写语法不能和具名插槽混用
,因为它会导致作用域不明确:
1 | <!-- 无效,会导致警告 --> |
只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template>
的语法:
1 | <current-user> |
解构插槽 Prop
ES6的解构赋值语法
1 | //{ user: person } 可以将user重名为person ;{ user = { firstName: 'Guest' } } 定义默认值 |
动态插槽名
动态参数:从 2.6.0 开始,可以用方括号括起来的 JavaScript 表达式作为一个指令的参数.
比如,当 eventName 的值为 “focus” 时,v-on:[eventName] 将等价于 v-on:focus。
动态指令参数也可以用在 v-slot
上,来定义动态的插槽名:
1 | <base-layout> |
具名插槽的缩写
跟 v-on
和v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。例如 v-slot:header
可以被重写为 #header
:
1 | <base-layout> |
然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:
1 | <!-- 这样会触发一个警告 --> |
如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:
1 | <current-user #default="{ user }"> |
废弃了的语法
v-slot
指令自 Vue 2.6.0 起被引入,提供更好的支持slot
和slot-scope
attribute 的 API 替代方案。在接下来所有的 2.x 版本中slot
和slot-scope
attribute 仍会被支持,但已经被官方废弃且不会出现在 Vue 3 中。
带有slot
attribute 的具名插槽
1 | //slot |
有 slot-scope
attribute 的作用域插槽
在 <template>
上使用特殊的 slot-scope
attribute,可以接收传递给插槽的 prop
:
1 | <slot-example> |
slot-scope
attribute 也可以直接用于非 <template>
元素 (包括组件):
1 | <slot-example> |
动态组件 & 异步组件
在动态组件上使用 keep-alive
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。
多标签界面是典型的切换问题。次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent
实例。重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 <keep-alive>
元素将其动态组件包裹起来。
1 | <!-- 失活的组件将会被缓存!--> |
异步组件
异步从服务器获取组件:
1 | Vue.component('async-example', function (resolve, reject) { |
当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:
1 | new Vue({ |
处理加载状态
这里的异步组件工厂函数也可以返回一个如下格式的对象:
1 | const AsyncComponent = () => ({ |
写在最后
Vue开发指南之深入了解组件篇整理完了,下一篇过渡动画。