组件注册
在注册一个组件的时候,我们始终需要给它一个名字。比如:
Vue.component('my-component-name', { /* ... */ })
该组件名就是 Vue.component
的第一个参数.
当直接在DOM中使用一个组件(而不是在字符串模板或单文件组件)的时候,我们强烈推荐遵循w3c规范中的自定义组件名(字母全小写且必须包含一个连字符)。
组件名大小写
定义组件名的方式有两种:
使用kebab-case
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>
。
使用pascalCase
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说<my-component-name>
和 <MyComponentName>
都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
Vue.component('my-component-name', {
// ... 选项 ...
})
这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中.
<div id="app">
<component-a></component-a>
<component-b></component-b>
<component-c></component-c>
</div>
<script>
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
new Vue({ el: '#app' })
</script>
在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用。
全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:var ComponentA = { /* ... */ }
然后在 components 选项中定义你想要使用的组件:
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentA 在 ComponentB 中可用,则你需要这样写:
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}
如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
基础组件的自动化全局注册
可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把他们称为基础组件,它们会在各个组件中被频繁的用到。
如果你使用了 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.replace(/^\.\/(.*)\.\w+$/, '$1')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
prop
prop的大小写
HTML 中的特性名是大小写不敏感的,当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名。如果你使用字符串模板,那么这个限制就不存在了。
Vue.component('blog-post', {
// 在 JavaScript 中是 camelCase 的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>
prop类型
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object
}
这不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的 JavaScript 控制台提示用户。
单向数据流
所有的 prop
都使得其父子prop
之间形成了一个单向下行绑定:父级prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的 prop
都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue
会在浏览器的控制台中发出警告。
这里有两种常见的试图改变一个 prop
的情形:
1、这个 prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop
数据来使用。在这种情况下,最好定义一个本地的 data
属性并将这个 prop
用作其初始值:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
2、这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
- 注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的
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 将会产生一个控制台的警告。
- 注意那些
prop
会在一个组件实例创建之前进行验证,所以实例的属性 (如 data、computed 等) 在 default 或 validator 函数中是不可用的。
类型检查
type
可以是下列原生构造函数中的一个:
String
Number
Boolean
Array
Object
Date
Function
-
Symbol
额外的,type
还可以是一个自定义的构造函数,并且通过instanceof
来进行检查确认。例如,给定下列现成的构造函数:
function Person (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
// 使用
Vue.component('blog-post', {
props: {
author: Person
}
})
来验证 author prop
的值是否是通过 new Person
创建的。
非Prop的特性
一个非 prop 特性是指传向一个组件,但是该组件并没有相应 prop 定义的特性。
例如:
<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>
然后这个 data-date-picker="activated"
特性就会自动添加到 <bootstrap-date-input>
的根元素上。
替换/合并以有的特性
想象一下 <bootstrap-date-input> 的模板是这样的:
<input type="date" class="form-control">
为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:
<bootstrap-date-input
data-date-picker="activated"
class="date-picker-theme-dark"
></bootstrap-date-input>
在这种情况下,我们定义了两个不同的 class 的值:
-
form-control
,这是在组件的模板内设置好的 -
date-picker-theme-dark
,这是从组件的父级传入的
对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入type="text"
就会替换掉type="date"
并把它破坏!庆幸的是,class
和style
特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark
。
禁用特性继承
如果你不希望组件的根元素继承特性,你可以在组件的选项中设置 inheritAttrs: false。
这尤其适合配合实例的 attrs`,你就可以手动决定这些特性会被赋予哪个元素。在撰写基础组件的时候是常会用到的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`
})
- 注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。
- $attrs--继承所有的父组件属性(除了prop传递的属性、class 和 style )
自定义事件
触发的事件名要和监听这个事件所用的名称完全匹配,大小写一致。
推荐你始终使用 kebab-case 的事件名。
自定义组件的v-model
一个组件上的 v-model
默认会利用名为 value
的 prop 和名为 input
的事件,但是像单选框、复选框等类型的输入控件可能会将 value
特性用于[不同的目的]。model
选项可以用来避免这样的冲突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
将原生事件绑定到组件
你可能有很多次想要在一个组件的根元素上直接监听一个原生事件,这时,你可以使用 v-on
的 .native
修饰符:
<base-input v-on:focus.native="onFocus"></base-input>
在有的时候这是很有用的,不过在你尝试监听一个类似 <input>
的非常特定的元素时,这并不是个好主意。为了解决这个问题,vue提供了一个 $listeners
属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:
{
focus: function(event) { /*...*/ }
input: function(value) { /*...*/ }
}
有了这个 $listeners
属性, 你就可以配合 v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 <input>
的你希望它也可以配合 v-model
工作的组件来说,为这些监听器创建一个类似下述 inputListeners
的计算属性通常是非常有用的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})
现在 <base-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input> 元素一样使用了:所有跟它相同的特性和监听器的都可以工作。
- 首先,在computed 计算属性里,创建一个 "inputListeners" 方法。这个方法返回的是这个组件的
监听器。用 object.assign( ) 方法,合并了 this.emit('input',event.target.value)) } ( input 就是新创建的事件/监听器)
- 在 template 模板中,子元素 <input> 用 v-on 直接绑定 "inputListeners" 方法 : <input v-on="inputListeners" >
这样,"inputListeners" 方法里面返回的所有监听器,都被绑在了这个 <input> 这个子元素上了。
$listeners属性
Vue提供了一个 $listeners
属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。
这时,可以使用 listeners 属性里面写好所有将会需要的监听器(原生事件),这些监听器就可以直接绑定在组件的子元素上(不是根元素)
网友评论