什么是组件?html
组件(Component)是 Vue.js 最强大的功能之一。组件能够扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些状况下,组件也能够是原生 HTML 元素的形式,以 is 特性扩展。vue
使用组件java
注册node
以前说过,咱们能够用 Vue.extend() 建立一个组件构造器:webpack
1
2
3
|
var
MyComponent = Vue.extend({
// 选项...
})
|
要把这个构造器用做组件,须要用 `Vue.component(tag, constructor)` **注册** : web
1
2
|
// 全局注册组件,tag 为 my-component
Vue.component(
'my-component'
, MyComponent)
|
<p class="tip">对于自定义标签名字,Vue.js 不强制要求遵循 W3C 规则(小写,而且包含一个短杠),尽管遵循这个规则比较好。ajax
组件在注册以后,即可以在父实例的模块中以自定义元素 <my-component> 的形式使用。要确保在初始化根实例以前注册了组件:vue-router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<
div
id
=
"example"
>
<
my-component
></
my-component
>
</
div
>
// 定义
var MyComponent = Vue.extend({
template: '<
div
>A custom component!</
div
>'
})
// 注册
Vue.component('my-component', MyComponent)
// 建立根实例
new Vue({
el: '#example'
})
|
渲染为: json
1
2
3
|
<
div
id
=
"example"
>
<
div
>A custom component!</
div
>
</
div
>
|
注意组件的模板替换了自定义元素,自定义元素的做用只是做为一个挂载点。能够用实例选项 replace 决定是否替换。数组
局部注册
不须要全局注册每一个组件。可让组件只能用在其它组件内,用实例选项 components 注册:
1
2
3
4
5
6
7
8
9
|
var
Child = Vue.extend({
/* ... */
})
var
Parent = Vue.extend({
template:
'...'
,
components: {
// <my-component> 只能用在父组件模板内
'my-component'
: Child
}
})
|
这种封装也适用于其它资源,如指令、过滤器和过渡。
注册语法糖
为了让事件更简单,能够直接传入选项对象而不是构造器给 Vue.component() 和 component 选项。Vue.js 在背后自动调用 Vue.extend():
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 在一个步骤中扩展与注册
Vue.component(
'my-component'
, {
template:
'<div>A custom component!</div>'
})
// 局部注册也能够这么作
var
Parent = Vue.extend({
components: {
'my-component'
: {
template:
'<div>A custom component!</div>'
}
}
})
|
组件选项问题
传入 Vue 构造器的多数选项也能够用在 Vue.extend() 中,不过有两个特例: data 和 el。试想若是咱们简单地把一个对象做为 data 选项传给 Vue.extend():
1
2
3
4
|
var
data = { a: 1 }
var
MyComponent = Vue.extend({
data: data
})
|
这么作的问题是 `MyComponent` 全部的实例将共享同一个 `data` 对象!这基本不是咱们想要的,所以咱们应当使用一个函数做为 `data` 选项,让这个函数返回一个新对象:
1
2
3
4
5
|
var
MyComponent = Vue.extend({
data:
function
() {
return
{ a: 1 }
}
})
|
同理,`el` 选项用在 `Vue.extend()` 中时也须是一个函数。
模板解析
Vue 的模板是 DOM 模板,使用浏览器原生的解析器而不是本身实现一个。相比字符串模板,DOM 模板有一些好处,可是也有问题,它必须是有效的 HTML 片断。一些 HTML 元素对什么元素能够放在它里面有限制。常见的限制:
•a 不能包含其它的交互元素(如按钮,连接)
•ul 和 ol 只能直接包含 li
•select 只能包含 option 和 optgroup
•table 只能直接包含 thead, tbody, tfoot, tr, caption, col, colgroup
•tr 只能直接包含 th 和 td
在实际中,这些限制会致使意外的结果。尽管在简单的状况下它可能能够工做,可是你不能依赖自定义组件在浏览器验证以前的展开结果。例如 <my-select><option>...</option></my-select> 不是有效的模板,即便 my-select 组件最终展开为 <select>...</select>。
另外一个结果是,自定义标签(包括自定义元素和特殊标签,如 <component>、<template>、 <partial> )不能用在 ul, select, table 等对内部元素有限制的标签内。放在这些元素内部的自定义标签将被提到元素的外面,于是渲染不正确。
对于自定义元素,应当使用 is 特性:
1
2
3
|
<
table
>
<
tr
is
=
"my-component"
></
tr
>
</
table
>
|
`` 不能用在 `` 内,这时应使用 ``,`
` 能够有多个 ``:
1
2
3
4
5
6
|
<
table
>
<
tbody
v-for
=
"item in items"
>
<
tr
>Even row</
tr
>
<
tr
>Odd row</
tr
>
</
tbody
>
</
table
>
|
Props
使用 Props 传递数据
组件实例的做用域是孤立的。这意味着不能而且不该该在子组件的模板内直接引用父组件的数据。可使用 props 把数据传给子组件。
“prop” 是组件数据的一个字段,指望从父组件传下来。子组件须要显式地用 props 选项 声明 props:
1
2
3
4
5
6
7
|
Vue.component(
'child'
, {
// 声明 props
props: [
'msg'
],
// prop 能够用在模板内
// 能够用 `this.msg` 设置
template:
'<span>{{ msg }}</span>'
})
|
而后向它传入一个普通字符串:
<child msg="hello!"></child>
驼峰式vs.横杠式
HTML 特性不区分大小写。名字形式为 camelCase 的 prop 用做特性时,须要转为 kebab-case(短横线隔开):
1
2
3
4
5
6
7
8
|
Vue.component(
'child'
, {
// camelCase in JavaScript
props: [
'myMessage'
],
template:
'<span>{{ myMessage }}</span>'
})
<!-- kebab-
case
in
HTML -->
<child my-message=
"hello!"
></child>
|
动态 Props
相似于用 v-bind 绑定 HTML 特性到一个表达式,也能够用 v-bind 绑定动态 Props 到父组件的数据。每当父组件的数据变化时,也会传导给子组件:
1
2
3
4
5
|
<
div
>
<
input
v-model
=
"parentMsg"
>
<
br
>
<
child
v-bind:my-message
=
"parentMsg"
></
child
>
</
div
>
|
使用 `v-bind` 的缩写语法一般更简单:
<child :my-message="parentMsg"></child>
字面量语法 vs. 动态语法
初学者常犯的一个错误是使用字面量语法传递数值:
<!-- 传递了一个字符串 "1" -->
<comp some-prop="1"></comp>
由于它是一个字面 prop,它的值以字符串 `”1”` 而不是以实际的数字传下去。若是想传递一个实际的 JavaScript 数字,须要使用动态语法,从而让它的值被看成 JavaScript 表达式计算:
<!-- 传递实际的数字 -->
<comp :some-prop="1"></comp>
Prop 绑定类型
prop 默认是单向绑定:当父组件的属性变化时,将传导给子组件,可是反过来不会。这是为了防止子组件无心修改了父组件的状态——这会让应用的数据流难以理解。不过,也可使用 .sync 或 .once 绑定修饰符显式地强制双向或单次绑定:
比较语法:
1
2
3
4
5
6
7
8
|
<!-- 默认为单向绑定 -->
<
child
:msg
=
"parentMsg"
></
child
>
<!-- 双向绑定 -->
<
child
:msg.sync
=
"parentMsg"
></
child
>
<!-- 单次绑定 -->
<
child
:msg.once
=
"parentMsg"
></
child
>
|
双向绑定会把子组件的 msg 属性同步回父组件的 parentMsg 属性。单次绑定在创建以后不会同步以后的变化。
注意若是 prop 是一个对象或数组,是按引用传递。在子组件内修改它会影响父组件的状态,不论是使用哪一种绑定类型。
Prop 验证
组件能够为 props 指定验证要求。当组件给其余人使用时这颇有用,由于这些验证要求构成了组件的 API,确保其余人正确地使用组件。此时 props 的值是一个对象,包含验证要求:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
Vue.component(
'example'
, {
props: {
// 基础类型检测 (`null` 意思是任何类型均可以)
propA: Number,
// 多种类型 (1.0.21+)
propM: [String, Number],
// 必需且是字符串
propB: {
type: String,
required:
true
},
// 数字,有默认值
propC: {
type: Number,
default
: 100
},
// 对象/数组的默认值应当由一个函数返回
propD: {
type: Object,
default
:
function
() {
return
{ msg:
'hello'
}
}
},
// 指定这个 prop 为双向绑定
// 若是绑定类型不对将抛出一条警告
propE: {
twoWay:
true
},
// 自定义验证函数
propF: {
validator:
function
(value) {
return
value > 10
}
},
// 转换函数(1.0.12 新增)
// 在设置值以前转换值
propG: {
coerce:
function
(val) {
return
val +
''
// 将值转换为字符串
}
},
propH: {
coerce:
function
(val) {
return
JSON.parse(val)
// 将 JSON 字符串转换为对象
}
}
}
})
|
type 能够是下面原生构造器:
•String
•Number
•Boolean
•Function
•Object
•Array
type 也能够是一个自定义构造器,使用 instanceof 检测。
当 prop 验证失败了,Vue 将拒绝在子组件上设置此值,若是使用的是开发版本会抛出一条警告。
父子组件通讯
父链
子组件能够用 this.$parent 访问它的父组件。根实例的后代能够用 this.$root 访问它。父组件有一个数组 this.$children,包含它全部的子元素。
尽管能够访问父链上任意的实例,不过子组件应当避免直接依赖父组件的数据,尽可能显式地使用 props 传递数据。另外,在子组件中修改父组件的状态是很是糟糕的作法,由于:
1.这让父组件与子组件紧密地耦合;
2.只看父组件,很难理解父组件的状态。由于它可能被任意子组件修改!理想状况下,只有组件本身能修改它的状态。
自定义事件
Vue 实例实现了一个自定义事件接口,用于在组件树中通讯。这个事件系统独立于原生 DOM 事件,用法也不一样。
每一个 Vue 实例都是一个事件触发器:
•使用 $on() 监听事件;
•使用 $emit() 在它上面触发事件;
•使用 $dispatch() 派发事件,事件沿着父链冒泡;
•使用 $broadcast() 广播事件,事件向下传导给全部的后代。
不一样于 DOM 事件,Vue 事件在冒泡过程当中第一次触发回调以后自动中止冒泡,除非回调明确返回 true。
简单例子:
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
33
34
35
36
37
38
39
40
41
42
43
44
|
<!-- 子组件模板 -->
<template id=
"child-template"
>
<input v-model=
"msg"
>
<button v-on:click=
"notify"
>Dispatch Event</button>
</template>
<!-- 父组件模板 -->
<div id=
"events-example"
>
<p>Messages: {{ messages | json }}</p>
<child></child>
</div>
// 注册子组件
// 将当前消息派发出去
Vue.component(
'child'
, {
template:
'#child-template'
,
data:
function
() {
return
{ msg:
'hello'
}
},
methods: {
notify:
function
() {
if
(
this
.msg.trim()) {
this
.$dispatch(
'child-msg'
,
this
.msg)
this
.msg =
''
}
}
}
})
// 初始化父组件
// 将收到消息时将事件推入一个数组
var
parent =
new
Vue({
el:
'#events-example'
,
data: {
messages: []
},
// 在建立实例时 `events` 选项简单地调用 `$on`
events: {
'child-msg'
:
function
(msg) {
// 事件回调内的 `this` 自动绑定到注册它的实例上
this
.messages.push(msg)
}
}
})
|
使用 v-on 绑定自定义事件
上例很是好,不过从父组件的代码中不能直观的看到 "child-msg" 事件来自哪里。若是咱们在模板中子组件用到的地方声明事件处理器会更好。为此子组件能够用 v-on 监听自定义事件:
<child v-on:child-msg="handleIt"></child>
这样就很清楚了:当子组件触发了 `”child-msg”` 事件,父组件的 `handleIt` 方法将被调用。全部影响父组件状态的代码放到父组件的 `handleIt` 方法中;子组件只关注触发事件。
子组件索引
尽管有 props 和 events,可是有时仍然须要在 JavaScript 中直接访问子组件。为此可使用 v-ref 为子组件指定一个索引 ID。例如:
1
2
3
4
5
6
7
|
<
div
id
=
"parent"
>
<
user-profile
v-ref:profile></
user-profile
>
</
div
>
var parent = new Vue({ el: '#parent' })
// 访问子组件
var child = parent.$refs.profile
|
v-ref 和 v-for 一块儿用时,ref 是一个数组或对象,包含相应的子组件。
使用 Slot 分发内容
在使用组件时,经常要像这样组合它们:
1
2
3
4
|
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>
|
注意两点:
1.<app> 组件不知道它的挂载点会有什么内容,挂载点的内容是由 <app> 的父组件决定的。
2.<app> 组件极可能有它本身的模板。
为了让组件能够组合,咱们须要一种方式来混合父组件的内容与子组件本身的模板。这个处理称为内容分发(或 “transclusion”,若是你熟悉 Angular)。Vue.js 实现了一个内容分发 API,参照了当前 Web 组件规范草稿,使用特殊的 <slot> 元素做为原始内容的插槽。
编译做用域
在深刻内容分发 API 以前,咱们先明确内容的编译做用域。假定模板为:
<child-component>
{{ msg }}
</child-component>
msg 应该绑定到父组件的数据,仍是绑定到子组件的数据?答案是父组件。组件做用域简单地说是:
父组件模板的内容在父组件做用域内编译;子组件模板的内容在子组件做用域内编译
一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:
<!-- 无效 -->
<child-component v-show="someChildProperty"></child-component>
假定 someChildProperty 是子组件的属性,上例不会如预期那样工做。父组件模板不该该知道子组件的状态。
若是要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么作:
1
2
3
4
5
6
7
8
9
|
Vue.component(
'child-component'
, {
// 有效,由于是在正确的做用域内
template:
'<div v-show="someChildProperty">Child</div>'
,
data:
function
() {
return
{
someChildProperty:
true
}
}
})
|
相似地,分发内容是在父组件做用域内编译。
单个 Slot
父组件的内容将被抛弃,除非子组件模板包含 <slot>。若是子组件模板只有一个没有特性的 slot,父组件的整个内容将插到 slot 所在的地方并替换它。
<slot> 标签的内容视为回退内容。回退内容在子组件的做用域内编译,当宿主元素为空而且没有内容供插入时显示这个回退内容。
假定 my-component 组件有下面模板:
1
2
3
4
5
6
|
<
div
>
<
h1
>This is my component!</
h1
>
<
slot
>
若是没有分发内容则显示我。
</
slot
>
</
div
>
|
父组件模板:
<my-component>
<p>This is some original content</p>
<p>This is some more original content</p>
</my-component>
渲染结果:
1
2
3
4
5
|
<
div
>
<
h1
>This is my component!</
h1
>
<
p
>This is some original content</
p
>
<
p
>This is some more original content</
p
>
</
div
>
|
具名 Slot
<slot> 元素能够用一个特殊特性 name 配置如何分发内容。多个 slot 能够有不一样的名字。具名 slot 将匹配内容片断中有对应 slot 特性的元素。
仍然能够有一个匿名 slot,它是默认 slot,做为找不到匹配的内容片断的回退插槽。若是没有默认的 slot,这些找不到匹配的内容片断将被抛弃。
例如,假定咱们有一个 multi-insertion 组件,它的模板为:
1
2
3
4
5
|
<
div
>
<
slot
name
=
"one"
></
slot
>
<
slot
></
slot
>
<
slot
name
=
"two"
></
slot
>
</
div
>
|
父组件模板:
1
2
3
4
5
|
<
multi-insertion
>
<
p
slot
=
"one"
>One</
p
>
<
p
slot
=
"two"
>Two</
p
>
<
p
>Default A</
p
>
</
multi-insertion
>
|
渲染结果为:
1
2
3
4
5
|
<
div
>
<
p
slot
=
"one"
>One</
p
>
<
p
>Default A</
p
>
<
p
slot
=
"two"
>Two</
p
>
</
div
>
|
在组合组件时,内容分发 API 是很是有用的机制。
动态组件
多个组件可使用同一个挂载点,而后动态地在它们之间切换。使用保留的 <component> 元素,动态地绑定到它的 is 特性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
new Vue({
el: 'body',
data: {
currentView: 'home'
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
<
component
:is
=
"currentView"
>
<!-- 组件在 vm.currentview 变化时改变 -->
</
component
>
|
keep-alive
若是把切换出去的组件保留在内存中,能够保留它的状态或避免从新渲染。为此能够添加一个 keep-alive 指令参数:
<component :is="currentView" keep-alive>
<!-- 非活动组件将被缓存 -->
</component>
activate 钩子
在切换组件时,切入组件在切入前可能须要进行一些异步操做。为了控制组件切换时长,给切入组件添加 activate 钩子:
1
2
3
4
5
6
7
8
9
|
Vue.component(
'activate-example'
, {
activate:
function
(done) {
var
self =
this
loadDataAsync(
function
(data) {
self.someData = data
done()
})
}
})
|
注意 `activate` 钩子只做用于动态组件切换或静态组件初始化渲染的过程当中,不做用于使用实例方法手工插入的过程当中。
transition-mode
transition-mode 特性用于指定两个动态组件之间如何过渡。
在默认状况下,进入与离开平滑地过渡。这个特性能够指定另外两种模式:
•in-out:新组件先过渡进入,等它的过渡完成以后当前组件过渡出去。
•out-in:当前组件先过渡出去,等它的过渡完成以后新组件过渡进入。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<!-- 先淡出再淡入 -->
<component
:is=
"view"
transition=
"fade"
transition-mode=
"out-in"
>
</component>
.fade-transition {
transition: opacity .3s ease;
}
.fade-enter, .fade-leave {
opacity: 0;
}
|
杂项
组件和 v-for
自定义组件能够像普通元素同样直接使用 v-for:
<my-component v-for="item in items"></my-component>
可是,不能传递数据给组件,由于组件的做用域是孤立的。为了传递数据给组件,应当使用 props:
<my-component
v-for="item in items"
:item="item"
:index="$index">
</my-component>
不自动把 item 注入组件的缘由是这会致使组件跟当前 v-for 紧密耦合。显式声明数据来自哪里可让组件复用在其它地方。
编写可复用组件
在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合不要紧,可是可复用组件应当定义一个清晰的公开接口。
Vue.js 组件 API 来自三部分——prop,事件和 slot:
•prop 容许外部环境传递数据给组件;
•事件 容许组件触发外部环境的 action;
•slot 容许外部环境插入内容到组件的视图结构内。
使用 v-bind 和 v-on 的简写语法,模板的缩进清楚且简洁:
<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat">
<!-- content -->
<img slot="icon" src="...">
<p slot="main-text">Hello!</p>
</my-component>
异步组件
在大型应用中,咱们可能须要将应用拆分为小块,只在须要时才从服务器下载。为了让事情更简单,Vue.js 容许将组件定义为一个工厂函数,动态地解析组件的定义。Vue.js 只在组件须要渲染时触发工厂函数,而且把结果缓存起来,用于后面的再次渲染。例如:
1
2
3
4
5
6
7
|
Vue.component(
'async-example'
,
function
(resolve, reject) {
setTimeout(
function
() {
resolve({
template:
'<div>I am async!</div>'
})
}, 1000)
})
|
工厂函数接收一个 resolve 回调,在收到从服务器下载的组件定义时调用。也能够调用 reject(reason) 指示加载失败。这里 setTimeout 只是为了演示。怎么获取组件彻底由你决定。推荐配合使用 Webpack 的代码分割功能:
1
2
3
4
5
6
|
Vue.component(
'async-webpack-example'
,
function
(resolve) {
// 这个特殊的 require 语法告诉 webpack
// 自动将编译后的代码分割成不一样的块,
// 这些块将经过 ajax 请求自动下载。
require([
'./my-async-component'
], resolve)
})
|
资源命名约定
一些资源,如组件和指令,是以 HTML 特性或 HTML 自定义元素的形式出如今模板中。由于 HTML 特性的名字和标签的名字不区分大小写,因此资源的名字一般需使用 kebab-case 而不是 camelCase 的形式,这不大方便。
Vue.js 支持资源的名字使用 camelCase 或 PascalCase 的形式,而且在模板中自动将它们转为 kebab-case(相似于 prop 的命名约定):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 在组件定义中
components: {
// 使用 camelCase 形式注册
myComponent: {
/*... */
}
}
<!-- 在模板中使用 kebab-
case
形式 -->
<my-component></my-component>
ES6 对象字面量缩写 也没问题:
// PascalCase
import TextBox from
'./components/text-box'
;
import DropdownMenu from
'./components/dropdown-menu'
;
export
default
{
components: {
// 在模板中写做 <text-box> 和 <dropdown-menu>
TextBox,
DropdownMenu
}
}
|
递归组件
组件在它的模板内能够递归地调用本身,不过,只有当它有 name 选项时才能够:
1
2
3
4
5
6
7
8
|
var
StackOverflow = Vue.extend({
name:
'stack-overflow'
,
template:
'<div>'
+
// 递归地调用它本身
'<stack-overflow></stack-overflow>'
+
'</div>'
})
|
上面组件会致使一个错误 “max stack size exceeded”,因此要确保递归调用有终止条件。当使用 Vue.component() 全局注册一个组件时,组件 ID 自动设置为组件的 name 选项。
片段实例
在使用 template 选项时,模板的内容将替换实例的挂载元素。于是推荐模板的顶级元素始终是单个元素。
不这么写模板:
<div>root node 1</div>
<div>root node 2</div>
推荐这么写:
<div>
I have a single root node!
<div>node 1</div>
<div>node 2</div>
</div>
下面几种状况会让实例变成一个片段实例:
1.模板包含多个顶级元素。
2.模板只包含普通文本。
3.模板只包含其它组件(其它组件多是一个片断实例)。
4.模板只包含一个元素指令,如 <partial> 或 vue-router 的 <router-view>。
5.模板根节点有一个流程控制指令,如 v-if 或 v-for。
这些状况让实例有未知数量的顶级元素,它将把它的 DOM 内容看成片段。片段实例仍然会正确地渲染内容。不过,它没有一个根节点,它的 $el 指向一个锚节点,即一个空的文本节点(在开发模式下是一个注释节点)。
可是更重要的是,组件元素上的非流程控制指令,非 prop 特性和过渡将被忽略,由于没有根元素供绑定:
<!-- 不能够,由于没有根元素 -->
<example v-show="ok" transition="fade"></example>
<!-- props 能够 -->
<example :prop="someData"></example>
<!-- 流程控制能够,可是不能有过渡 -->
<example v-if="ok"></example>
固然片段实例有它的用处,不过一般给组件一个根节点比较好。它会保证组件元素上的指令和特性能正确地转换,同时性能也稍微好些。
内联模板
若是子组件有 inline-template 特性,组件将把它的内容看成它的模板,而不是把它看成分发内容。这让模板更灵活。
<my-component inline-template>
<p>These are compiled as the component's own template</p>
<p>Not parent's transclusion content.</p>
</my-component>
可是 inline-template 让模板的做用域难以理解,而且不能缓存模板编译结果。最佳实践是使用 template 选项在组件内定义模板。