接上一节内容。
组件的意义是为了复用,因为组件是可复用的 Vue 实例,所以它们与 new Vue
接收相同的选项,例如 data
、computed
、watch
、methods
以及生命周期钩子等。仅有的例外是像 el
这样根实例特有的选项。
因此使用的每个组件实例可以维护自己的数据,而不会影响到其他组件的数据:
data: function () {
return {
count: 0
}
}
为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。至此,我们的组件都只是通过 Vue.component
全局注册的:
Vue.component('my-component-name', {
// ... options ...
})
全局注册的组件可以用在其被注册之后的任何 (通过 new Vue
) 新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中。
Prop 是在组件上注册的一些自定义 attribute(组件拥有的属性)。当一个值传递给 一个 prop的attribute 的时候,它就变成了那个组件实例property的值。
为了给博文组件传递一个标题,我们可以用一个 props
数组将标题包含在该组件可接受的 prop 列表中:
Vue.component('blog-post', {
props: ['title'],
template: '{{ title }}
'
})
一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data
中的值一样。
一个 prop 被注册之后,就可以使用了:
结果:
当构建一个
组件时,模板包含的东西远不止一个标题(title),可能会包含这篇博文的正文(content)、发布日期(publictime)、评论(comment)等等。为每个相关的信息定义一个 prop 会变得很麻烦。
重构一下这个
组件了,让它变成接受一个单独的 post
prop:【此时的post是一个对象】
Vue.component('blog-post', {
props: ['post'],
template: `
{{ post.title }}
`
})
现在,不论何时为 post
对象添加一个新的 property,它都会自动地在
内可用。
在我们开发
组件时,它的一些功能可能要求我们和父级组件进行沟通。例如方法子组件内的博文字体。
父级组件可以像处理 native DOM 事件一样通过 v-on
监听子组件实例的任意事件:
同时子组件可以通过调用内建的 $emit
方法并传入事件名称来触发一个事件:
自定义组件也可以使用 v-model,
当用在组件上时,v-model
则会这样:
为了让它正常工作,这个组件内的 必须:
value
attribute 绑定到一个名叫 value
的 prop 上。input
事件被触发时,将新的值通过自定义的 input
事件抛出。新注册的组件:
Vue.component('custom-input', {
props: ['value'],
template: `
`
})
现在 v-model
就应该可以在这个组件上完美地工作起来了:
自定义组件名时最好是 字母全小写且必须包含一个连字符。这会帮助避免和当前以及未来的 HTML 元素相冲突。
全局注册使用Vue.component
来创建组件:
Vue.component('my-component-name', {
// ... 选项 ...
})
注意:全局注册的行为必须在根 Vue 实例 (通过 new Vue
) 创建之前发生(即使用之前)。
全局注册往往是不够理想的。比如,使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便已经不再使用一个组件了,它仍然会被包含在最终的构建结果中。这造成了无谓的增加用户下载的 JavaScript 的。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
然后在 components
选项中定义你想要使用的组件:
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
对于 components
对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。
注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentA
在 ComponentB
中可用,则你需要这样写:
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}
或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
注意在 ES2015+ 中,在对象中放一个类似 ComponentA
的变量名其实是 ComponentA: ComponentA
的缩写,即这个变量名同时是:
(1)在模块系统中局部注册
如果项目是通过 import
/require
使用一个模块系统,说明使用了诸如 Babel 和 webpack 的模块系统。在这些情况下,推荐创建一个 components
目录,并将每个组件放置在其各自的文件中。
然后需要在局部注册之前导入每个想使用的组件。例如,在一个假设的 ComponentB.js
或 ComponentB.vue
文件中:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
},
// ...
}
现在 ComponentA
和 ComponentC
都可以在 ComponentB
的模板中使用了。
(2)基础组件的自动化全局注册 (不要求掌握)
可能许多组件只是包裹了一个输入框或按钮之类的元素,作为通用组件。我们把它们称为基础组件,它们会在其他组件中被频繁的使用。
所以会导致会有一个包含基础组件的长列表:
import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'
export default {
components: {
BaseButton,
BaseIcon,
BaseInput
}
}
但在其他模板中只占一小部分的代码(即使用频率不高):
如果恰好项目是使用 webpack (或在内部使用 webpack 的 Vue CLI 3+),那么就可以使用 require.context
全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件 (比如 src/main.js
) 中全局导入基础组件的示例代码:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:
Vue.component('blog-post', {
// 在 JavaScript 中是 camelCase 的
props: ['postTitle'],
template: '{{ postTitle }}
'
})
注意:如果使用字符串模板,那么这个限制就不存在了。
以字符串数组形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
如果希望每个 prop 都有指定的值类型。这时,可以使用对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型:
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}
这样在遇到错误的类型时从浏览器的 JavaScript 控制台就能看到相应的信息。
prop可以接受任何类型的值。例如:
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
可以为组件的 prop 指定验证要求,例如设定类型。如果有一个需求没有被满足,则 Vue 会在浏览器控制台中警告。
为了定制 prop 的验证方式,你可以为 props
中的值提供一个带有验证功能的对象,而不是一个字符串数组。例如:
Vue.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 设定的验证要求不满足时,(开发环境构建版本的) Vue 将会产生一个控制台的警告。
type
可以是下列原生构造函数中的一个:
String
Number
Boolean
Array
Object
Date
Function
Symbol
不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件:
this.$emit('myEvent')
则监听这个名字的 kebab-case 版本是不会有任何效果的:
v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent
将会变成 v-on:myevent
——导致 myEvent
不可能被监听到。
因此推荐始终使用 kebab-case 的事件名。
插槽就是子组件中提供给父组件使用的一个占位符,用<v-slot
>v-slot
> 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<v-slot
>v-slot
>标签。
在 2.6.x版本后,我们为具名插槽(起了名字的插槽)和作用域插槽引入了一个新的统一的语法 (即 v-slot
指令)。它取代了 slot
和 slot-scope
这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC。
目前最新版本的Vue是2.6.11。
如果对插槽的概念不是很理解,推荐先阅读一下:vue_插槽的理解和使用。
注意:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
后备内容:即给插槽设置默认内容,它只会在没有提供内容的时候被渲染。
具名插槽:即给插槽设置了名称。
使用场景:当子组件有多个插槽,且需要有所区分时使用。
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽:
注意:一个不带 name
的
出口会带有隐含的名字“default”。
在向具名插槽插入内容的时候,我们可以在一个 元素上使用
v-slot
指令,并以 v-slot
的参数的形式提供其名称:
Here might be a page title
A paragraph for the main content.
And another one.
Here's some contact info
这样 元素中的所有内容都将会被插入到相应的插槽。任何没有被包裹在带有
v-slot
的 中的内容都会被视为默认插槽的内容。
注意: v-slot
只能添加在 上 ,这一点和已经废弃的
slot
不同。
是为了让父级能够访问子组件插槽的内容。
为了让子组件插槽里的内容(user
)可以被父级使用用,我们可以将 user
作为
元素的一个 attribute 绑定上去:
{{ user.lastName }}
绑定在
元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来改变我们提供的插槽 prop 的名字:
{{ slotProps.user.firstName }}
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps
,但你也可以使用任意你喜欢的名字。
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。例如 v-slot:header
可以被重写为 #header
:
Here might be a page title
A paragraph for the main content.
And another one.
Here's some contact info
和其它指令一样,该缩写只在其有参数(即给插槽起了名字)的时候才可用。这意味着以下语法是无效的:
{{ user.firstName }}
主要是在多组件切换时,保留组态切换前的状态,以避免反复重渲染导致的性能问题。
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许以一个工厂函数的方式定义组件,这个工厂函数会异步解析组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})
(1)访问根实例:
在每个 new Vue
实例的子组件中,其根实例可以通过 $root
property 进行访问。例如,在这个根实例中:
// Vue 根实例
new Vue({
data: {
foo: 1
},
computed: {
bar: function () { /* ... */ }
},
methods: {
baz: function () { /* ... */ }
}
})
// 获取根组件的数据
this.$root.foo
// 写入根组件的数据
this.$root.foo = 2
// 访问根组件的计算属性
this.$root.bar
// 调用根组件的方法
this.$root.baz()
注意:对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,强烈推荐使用 Vuex 来管理应用的状态。
(2)访问父组件实例:(不推荐)
$parent
property 可以用来从一个子组件访问父组件的实例。
(3)访问子组件实例或子元素:
尽管存在 prop 和事件,但有的时候仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,可以通过 ref
这个 attribute 为子组件赋予一个 ID 引用。例如:
现在已经给这个组件定义了一个 ref
,可以使用:
this.$refs.usernameInput
来访问这个 <base-input>
实例。
当 ref
和 v-for
一起使用的时候,你得到的 ref 将会是一个包含了对应数据源的这些子组件的数组。
注意:$refs
只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs
。
(4)依赖注入:不推荐(建议使用Vuex管理状态)