11. Components(组件)
组件
什么是组件?
组件是Vue最强大的功能之一。它们帮助您扩展基本的HTML元素以封装可重用的代码。在高层次上,组件是Vue编译器将行为附加到的自定义元素。在某些情况下,它们也可能会显示为使用特殊is
属性扩展的原生HTML元素。
使用组件
全局注册
在前面的章节中我们已经了解到,我们可以创建一个新的Vue实例:
new Vue{
el: '#some-element',
// options
})
要注册全局组件,您可以使用Vue.component(tagName, options)
。例如:
Vue.component('my-component', {
// options
})
请注意,Vue不强制自定义标记名称(全小写,必须包含连字符)的W3C规则,但按照此惯例被认为是良好的做法。
一旦注册,一个组件可以在实例的模板中用作自定义元素,<my-component></my-component>。在实例化根Vue实例之前,确保组件已注册。以下是完整的示例:
<div id="example">
<my-component></my-component>
</div>
// register
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// create a root instance
new Vue{
el: '#example'
})
这将渲染:
<div id="example">
<div>A custom component!</div>
</div>
本地注册
您无需在全局范围内注册每个组件。通过使用components
实例选项注册组件,您可以仅在另一个实例/组件的范围内使组件可用:
var Child = {
template: '<div>A custom component!</div>'
}
new Vue{
// ...
components: {
// <my-component> will only be available in parent's template
'my-component': Child
}
})
相同的封装适用于其他可注册Vue功能,例如指令。
DOM模板解析警告
当使用DOM作为模板时(例如,使用该el选项来挂载具有现有内容的元素),您将受到HTML工作原理固有的一些限制,因为Vue只能在浏览器解析后才能检索模板内容,规范化它。最值得注意的是,某些元素(例如<ul>,<ol>)<table>以及<select>对哪些元素可能出现在它们内部有限制,以及某些元素(如<option>只能出现在某些其他元素内)。
这会在使用具有这种限制的元素的自定义组件时导致问题,例如:
<table>
<my-row>...</my-row>
</table>
自定义组件<my-row>将作为无效内容被吊起,从而导致最终呈现的输出中出现错误。解决方法是使用is特殊属性:
<table>
<tr is="my-row"></tr>
</table>
应该注意的是,如果您使用来自以下某个来源的字符串模板,则这些限制不适用
:
- <script type="text/x-template">
- JavaScript内联模板字符串
.vue
组件
因此,尽可能使用字符串模板。
data 必须是函数
可以传递给Vue构造函数的大多数选项都可以在组件中使用,但有一个特例:data
必须是函数。事实上,如果你尝试这样做:
Vue.component('my-component', {
template: '<span>{{ message }}</span>',
data: {
message: 'hello'
}
})
然后,Vue会在控制台中停止并发出警告,告诉你data
必须是组件实例的函数。理解规则存在的原因很好,所以让我们来欺骗吧。
<div id="example-2">
<simple-counter></simple-counter>
<simple-counter></simple-counter>
<simple-counter></simple-counter>
</div>
var data = { counter: 0 }
Vue.component('simple-counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
// data is technically a function, so Vue won't
// complain, but we return the same object
// reference for each component instance
data: function () {
return data
}
})
new Vue{
el: '#example-2'
})
由于所有三个组件实例共享同一个data
对象,因此递增一个计数器会将它们全部递增。我们通过返回一个新的数据对象来解决这个问题:
data: function () {
return {
counter: 0
}
}
现在我们所有的计数器都有自己的内部状态:
构成组件
组件意味着一起使用,最常用的是父子关系:组件A可以在其自己的模板中使用组件B. 他们不可避免地需要彼此沟通:父母可能需要将数据传递给孩子,孩子可能需要通知父母发生在孩子身上的事情。然而,通过明确定义的界面尽可能使父母和孩子尽可能地分离也是非常重要的。这可以确保每个组件的代码可以相对独立地进行编写和推理,从而使它们更易于维护并可能更易于重用。
在Vue中,父子成分关系可以概括为道具向下,事件发生
。父母通过道具
将数据传递给孩子,孩子通过事件
向父母发送消息。我们来看看他们接下来的工作。
Props
用props传递数据
每个组件实例都有其独立的范围
。这意味着您不能(也不应该)直接引用子组件模板中的父数据。数据可以通过props传递给子组件。
props
是用于从父组件传递信息的自定义属性。子组件需要使用该props
选项显式声明它期望收到的道具:
Vue.component('child', {
// declare the props
props: ['message'],
// like data, the prop can be used inside templates and
// is also made available in the vm as this.message
template: '<span>{{ message }}</span>'
})
然后我们可以像这样传递一个简单的字符串:
<child message="hello!"></child>
结果:
camelCase 与 kebab-case
HTML属性不区分大小写,因此使用非字符串模板时,camelCased prop名称需要使用它们的kebab-case(连字符分隔)等价物:
Vue.component('child', {
// camelCase in JavaScript
props: ['myMessage'],
template: '<span>{{ myMessage }}</span>'
})
<!-- kebab-case in HTML -->
<child my-message="hello!"></child>
同样,如果您使用字符串模板,则此限制不适用。
动态Props
与将常规属性绑定到表达式类似,我们也可以使用v-bind
动态绑定props来与父级上的数据绑定。无论何时数据在父组件中更新,它也会流向子组件:
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
您还可以使用简写语法v-bind
:
<child :my-message="parentMsg"></child>
结果:
如果您想以道具的形式传递对象中的所有属性,则可以v-bind
不使用参数(v-bind
而不使用v-bind:prop-name
)。例如,给定一个todo
对象:
todo: {
text: 'Learn Vue',
isComplete: false
}
然后:
<todo-item v-bind="todo"></todo-item>
将等同于:
<todo-item
v-bind:text="todo.text"
v-bind:is-complete="todo.isComplete"
></todo-item>
文字与动态
初学者常会犯的一个错误是试图用字面语法来传递一个数字:
<!-- this passes down a plain string "1" -->
<comp some-prop="1"></comp>
然而,由于这是一个字面上的props,它的价值是作为一个普通的字符串"1"
而不是实际的数字传递下来的。如果我们想要传递一个实际的JavaScript数字,我们需要使用v-bind
它的值作为JavaScript表达式进行评估:
<!-- this passes down an actual number -->
<comp v-bind:some-prop="1"></comp>
单向数据流
所有props形成子属性和父属性之间的单向
绑定:当父组件的属性更新时,它将流向子组件,但不是相反方向。这可以防止子组件意外地改变父项的状态,这会使您的应用程序的数据流难以理解。
另外,每次更新父组件时,子组件中的所有道具都将刷新为最新值。这意味着你应该不是
试图突变子组件内的道具。如果你这样做,Vue会在控制台中发出警告。
通常有两种情况会诱发变形道具:
1. 道具用于传递初始值; 子组件想在之后将其用作本地数据属性。
2. 该道具作为需要转换的原始值传入。
这些用例的正确答案是:
1. 定义一个使用道具初始值作为初始值的本地数据属性:props:'initialCounter',data:function(){return {counter:this.initialCounter}}
2. 定义从prop值计算出来的计算属性:
道具:'size',计算公式:{normalizedSize:function(){ return this.size.trim().toLowerCase() }}
请注意,JavaScript中的对象和数组是通过引用传递的,所以如果prop是数组或对象,则在子对象内部改变对象或数组本身会
影响父对象的状态。
道具验证
组件可以指定它正在接收的道具的需求。如果不满足要求,Vue将发出警告。当您创作一个打算供其他人使用的组件时,这是特别有用的。
您可以使用具有验证要求的对象,而不是将道具定义为字符串数组:
Vue.component('example', {
props: {
// basic type check (`null` means accept any type)
propA: Number,
// multiple possible types
propB: [String, Number],
// a required string
propC: {
type: String,
required: true
},
// a number with default value
propD: {
type: Number,
default: 100
},
// object/array defaults should be returned from a
// factory function
propE: {
type: Object,
default: function () {
return { message: 'hello' }
}
},
// custom validator function
propF: {
validator: function (value) {
return value > 10
}
}
}
})
该type
可以是以下JavaScript本身的构造函数:
- String
- Number
- Boolean
- Function
- Object
- Array
- Symbol
另外,type
也可以是一个自定义的构造函数,这个断言将通过一个instanceof
检查来完成。
当道具验证失败时,Vue将产生一个控制台警告(如果使用开发版本的话)。注意,道具被验证之前
创建组件实例,所以在default
或validator
函数中,实例属性如data
,computed
或者methods
将不可用。
非道具属性
非prop属性是传递给组件的属性,但没有定义相应的prop。
虽然明确定义的道具是将信息传递给子组件的首选,但组件库的作者并不总是能够预见其组件的使用环境。这就是为什么组件可以接受随意添加到组件根元素的属性。
例如,假设我们正在使用bs-date-input
带有Bootstrap插件的第三方组件,该插件需要一个data-3d-date-picker
属性input
。我们可以将此属性添加到我们的组件实例中:
<bs-date-input data-3d-date-picker="true"></bs-date-input>
该data-3d-date-picker="true"
属性将自动添加到根元素中的bs-date-input
。
用现有属性替换/合并
想象一下,这是bs-date-input
的模板:
<input type="date" class="form-control">
要为我们的日期选择器插件指定一个主题,我们可能需要添加一个特定的类,如下所示:
<bs-date-input
data-3d-date-picker="true"
class="date-picker-theme-dark"
></bs-date-input>
在这种情况下,class
定义了两个不同的值:
form-control
,它被模板中的组件设置
date-picker-theme-dark
,它由父级传递给组件
对于大多数属性,提供给组件的值将替换组件设置的值。因此,例如,传值type="large"
将取代type="date"
并覆盖打破它!幸运的是,class
和style
属性有点聪明,所以两个值都合并了,最终的值:form-control date-picker-theme-dark
。
自定义事件
我们了解到,父组件可以使用道具将数据传递给子组件,但是如果发生什么事,我们如何与父组件沟通?这就是Vue的自定义事件系统的由来。
使用带有自定义事件的v-on
每个Vue实例都实现一个事件接口,这意味着它可以:
- 使用
$on(eventName)
监听活动
- 使用触发事件
$emit(eventName)
请注意,Vue的事件系统与浏览器的EventTarget API不同。虽然他们的工作同样,$on
和$emit
不是addEventListener
和dispatchEvent
的别名。
另外,父组件可以使用v-on
直接在子组件的模板中侦听从子组件发出的事件。
你不能将$on
用来监听子节点发出的事件。您必须v-on
直接在模板中使用,如下例所示。
这有一个例子:
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
incrementCounter: function () {
this.counter += 1
this.$emit('increment')
}
},
})
new Vue{
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
})
在这个例子中,重要的是要注意子组件仍然完全脱离了它所发生的事情。它所做的只是报告有关其自身活动的信息,以防父组件可能会被影响。
将本地事件绑定到组件
有时候您想要在组件的根元素上监听本地事件。在这些情况下,您可以使用v-on
的.native
修饰符。例如:
<my-component v-on:click.native="doTheThing"></my-component>
.sync 修饰符
2.3.0+
在某些情况下,我们可能需要“双向绑定”来支持 - 实际上,在Vue 1.x中,这正是.sync
修改器提供的内容。当一个子组件改变了一个prop的时候.sync
,这个值的变化会反映到父项中。这很方便,但是从长远来看它会导致维护问题,因为它打破了单向数据流的假设:变异子道具的代码隐含地影响父状态。
这就是为什么我们在2.0发布时删除了.sync
修饰符。但是,我们发现有些情况下可能有用,特别是在运送可重用组件时。我们需要改变的是让影响父状态的子代中的代码更加一致和明确。
在2.3.0+中,我们重新引入了.sync
道具的修饰符,但是这次它只是语法糖,会自动扩展为一个额外的v-on
监听器:
下列
<comp :foo.sync="bar"></comp>
扩展为:
<comp :foo="bar" @update:foo="val => bar = val"></comp>
为了让子组件更新foo
值,它需要显式地发出一个事件,而不是改变prop:
this.$emit('update:foo', newValue)
使用自定义事件来输入组件
自定义事件也可以用来创建自定义输入v-model
。记得:
<input v-model="something">
是语法糖:
<input
v-bind:value="something"
v-on:input="something = $event.target.value">
与组件一起使用时,它简化为:
<custom-input
:value="something"
@input="value => { something = value }">
</custom-input>
所以对于一个组件来说v-model
,它应该(这些可以在2.2.0+中配置):
- 接受
value
道具
- 用新值发出一个
input
事件
让我们用一个简单的货币输入来看它的运作情况:
<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)">\
</span>\
',
props: ['value'],
methods: {
// Instead of updating the value directly, this
// method is used to format and place constraints
// on the input's value
updateValue: function (value) {
var formattedValue = value
// Remove whitespace on either side
.trim()
// Shorten to 2 decimal places
.slice(
0,
value.indexOf('.') === -1
? value.length
: value.indexOf('.') + 3
)
// If the value was not already normalized,
// manually override it to conform
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// Emit the number value through the input event
this.$emit('input', Number(formattedValue))
}
}
})
上面的实现虽然很简单。例如,用户有时可以输入多个句号甚至字母 - 哎呀!因此对于那些希望看到一个不平凡的例子的人来说,这里有一个更强大的货币过滤器:
定制组件 v-model
New in 2.2.0+
默认情况下,v-model
组件value
用作prop和input
事件,但某些输入类型(如复选框和单选按钮)可能希望将value
prop用于不同的目的。model
在这种情况下使用该选项可以避免冲突:
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean,
// this allows using the `value` prop for a different purpose
value: String
},
// ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>
以上内容相当于:
<my-checkbox
:checked="foo"
@change="val => { foo = val }"
value="some value">
</my-checkbox>
请注意,您仍然需要checked
明确声明道具。
非亲子沟通
有时两个组件可能需要彼此通信,但它们不是彼此的父/子。在简单场景中,您可以使用空的Vue实例作为中央事件总线:
var bus = new Vue()
// in component A's method
bus.$emit('id-selected', 1)
// in component B's created hook
bus.$on('id-selected', function (id) {
// ...
})
在更复杂的情况下,您应该考虑采用专用的状态管理模式。
内容分发与插槽
使用组件时,通常需要像这样编写它们:
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>
有两点需要注意:
1. <app>组件不知道它会收到什么内容。它由组件使用决定<app>。
2. 该<app>组件很可能有自己的模板。
为了使组合成功,我们需要一种方法来交织父“内容”和组件自己的模板。这是一个称为内容分发的过程(如果您熟悉Angular,则称为“transclusion”)。Vue.js实现了一个内容分发API,该API根据当前的Web组件规范草图建模,使用特殊<slot>元素作为原始内容的分发插口。
编译范围
在深入研究API之前,我们首先澄清内容编译的范围。想象一下这样的模板:
<child-component>
{{ message }}
</child-component>
是否应该message
绑定到父数据或子数据?答案是父母。组件范围的简单经验法则是:
父模板中的所有内容都在父范围内编译; 子模板中的所有内容都在子范围内编译。
常见的错误是试图将一个指令绑定到父模板中的子属性/方法:
<!-- does NOT work -->
<child-component v-show="someChildProperty"></child-component>
假设someChildProperty
是子组件上的一个属性,上面的示例将不起作用。父母的模板不知道子组件的状态。
如果您需要在组件根节点上绑定子范围指令,则应该在子组件自己的模板中执行此操作:
Vue.component('child-component', {
// this does work, because we are in the right scope
template: '<div v-show="someChildProperty">Child</div>',
data: function () {
return {
someChildProperty: true
}
}
})
同样,分布式内容将在父范围内编译。
单槽
父项内容将被丢弃,除非子组件模板至少包含一个<slot>插座。当只有一个没有属性的插槽时,整个内容片段将被插入到DOM中的位置,替换插槽本身。
最初在<slot>标签内的任何东西都被认为是后备内容。备用内容编译在子作用域中,并且只有在主机元素为空且没有要插入的内容时才会显示。
假设我们有一个my-component
使用以下模板调用的组件:
<div>
<h2>I'm the child title</h2>
<slot>
This will only be displayed if there is no content
to be distributed.
</slot>
</div>
并且使用该组件的父级:
<div>
<h1>I'm the parent title</h1>
<my-component>
<p>This is some original content</p>
<p>This is some more original content</p>
</my-component>
</div>
呈现的结果将是:
<div>
<h1>I'm the parent title</h1>
<div>
<h2>I'm the child title</h2>
<p>This is some original content</p>
<p>This is some more original content</p>
</div>
</div>
命名的插槽
<slot>元素有一个特殊的属性,name它可以用来进一步定制内容如何分配。您可以有多个不同名称的插槽。命名空位将与slot内容片段中具有相应属性的任何元素相匹配。
仍然可以有一个未命名的插槽
,这是默认插槽
,可用作任何不匹配内容的全部插槽
。如果没有默认插槽
,则不匹配的内容将被丢弃。
例如,假设我们有一个app-layout
包含以下模板的组件:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
父标记:
<app-layout>
<h1 slot="header">Here might be a page title</h1>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<p slot="footer">Here's some contact info</p>
</app-layout>
呈现的结果将是:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
在设计旨在组合在一起的组件时,内容分发API是非常有用的机制。
范围内的插槽
New in 2.1.0+
作用域插槽是一种特殊类型的插槽,用作可重复使用的模板(可以将数据传递给)而不是已经呈现的元素。
在子组件中,将数据传递到插槽中,就像将道具传递给组件一样:
<div class="child">
<slot text="hello from child"></slot>
</div>
在父项中,<template>具有特殊属性的元素slot-scope必须存在,表明它是范围插槽的模板。该值slot-scope将用作保存从子项传递的道具对象的临时变量的名称:
<div class="parent">
<child>
<template slot-scope="props">
<span>hello from parent</span>
<span>{{ props.text }}</span>
</template>
</child>
</div>
如果我们渲染上面的内容,输出将是:
<div class="parent">
<div class="child">
<span>hello from parent</span>
<span>hello from child</span>
</div>
</div>
在2.5.0+中,slot-scope不再局限于<template>任何元素或组件,并且可以使用它们。
作用域插槽的更典型用例是列表组件,它允许组件使用者自定义列表中每个项目的呈现方式:
<my-awesome-list :items="items">
<!-- scoped slot can be named too -->
<li
slot="item"
slot-scope="props"
class="my-fancy-item">
{{ props.text }}
</li>
</my-awesome-list>
和列表组件的模板:
<ul>
<slot name="item"
v-for="item in items"
:text="item.text">
<!-- fallback content here -->
</slot>
</ul>
解构
slot-scope
其值实际上是一个有效的JavaScript表达式,可以出现在函数签名的参数位置。这意味着在受支持的环境中(在单文件组件或现代浏览器中),您还可以在表达式中使用ES2015解构:
<child>
<span slot-scope="{ text }">{{ text }}</span>
</child>
动态组件
您可以使用相同的挂载点并使用保留的<component>元素在多个组件之间动态切换,并动态绑定到其is属性:
var vm = new Vue{
el: '#example',
data: {
currentView: 'home'
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
<component v-bind:is="currentView">
<!-- component changes when vm.currentView changes! -->
</component>
如果您愿意,也可以直接绑定到组件对象:
var Home = {
template: '<p>Welcome home!</p>'
}
var vm = new Vue{
el: '#example',
data: {
currentView: Home
}
})
keep-alive
如果您想将切换出来的组件保存在内存中,以便保留它们的状态或避免重新渲染,可以将动态组件包装在一个<keep-alive>元素中:
<keep-alive>
<component :is="currentView">
<!-- inactive components will be cached! -->
</component>
</keep-alive>
<keep-alive>在API参考中查看更多详细信息。
Misc
创作可重用组件
在编写组件时,最好记住是否打算在以后的其他地方重用它。可以将一次性组件紧密耦合,但可重用组件应该定义一个干净的公共接口,并且不会对其使用的上下文进行假设。
Vue组件的API分为三部分 - 道具,事件和插槽:
道具
允许外部环境将数据传递到组件
事件
允许组件在外部环境中触发副作用
- 插槽允许外部环境组成具有额外内容的组件。使用v-bind和v-on的专用速记语法,可以在模板中清晰简洁地传达意图:<my-component :foo="baz" :bar="qux" @event-a="doThis" @event-b="doThat" > <img slot="icon" src="..."> <p slot="main-text">Hello!</p> </my-component>子组件Refs尽管存在道具和事件,但有时您仍然需要直接使用JavaScript访问子组件。 要达到此目的,您必须使用ref为子组件分配参考ID。 例如:<div id="parent"> <user-profile ref="profile"></user-profile> </div>var parent = new Vue{ el: '#parent' }) // access child component instance var child = parent.$refs.profile当ref与v-for一起使用时,您得到的ref将是一个包含镜像数据源的子组件的数组。$ refs仅在组件被渲染后填充,并且不会被反应。 它只是作为直接子操作的逃生舱口 - 您应该避免在模板或计算属性中使用$ refs。异步组件在大型应用程序中,我们可能需要将应用程序分成更小的块,并且只在服务器 实际上需要。 为了简化起见,Vue允许您将组件定义为工厂函数,以异步方式解析组件定义。 当组件实际需要渲染时,Vue将只触发工厂函数,并将缓存未来重新渲染的结果。 例如:Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // Pass the component definition to the resolve callback resolve{ template: '<div>I am async!</div>' }) }, 1000) })厂函数接收一个解析回调函数,当你从服务器检索到你的元件定义时应该调用它。 您也可以调用拒绝(原因)来指示加载失败。 setTimeout这里是为了演示; 如何检索组件取决于您。 一个推荐的方法是将异步组件与Webpack的代码分割功能一起使用:Vue.component('async-webpack-example', function (resolve) { // This special require syntax will instruct Webpack to // automatically split your built code into bundles which // are loaded over Ajax requests. require(['./my-async-component'], resolve) })You can also return a Promise in the factory function, so with Webpack 2 + ES2015 syntax you can do:Vue.component( 'async-webpack-example', // The `import` function returns a `Promise`. () => import('./my-async-component') )When using local registration, you can also directly provide a function that returns a Promise:new Vue{ // ... components: { 'my-component': () => import('./my-async-component') } })如果你是一位喜欢使用异步组件的Browserify用户,它的创建者不幸地明确表示异步加载“并不是Browserify永远不会支持的东西”。至少官方是这样。 Browserify社区发现了一些解决方法,这可能对现有的和复杂的应用程序有所帮助。 对于所有其他场景,我们建议使用Webpack进行内置的一流异步支持。高级异步组件2.3.0+从2.3.0+开始,异步组件工厂还可以返回以下格式的对象const AsyncComp = () => { // The component to load. Should be a Promise component: import('./MyComp.vue'), // A component to use while the async component is loading loading: LoadingComp, // A component to use if the load fails error: ErrorComp, // Delay before showing the loading component. Default: 200ms. delay: 200, // The error component will be displayed if a timeout is // provided and exceeded. Default: Infinity. timeout: 3000 })请注意,当用作vue-router中的路由组件时,这些属性将被忽略,因为在路由导航发生之前,会先向前解析异步组件。 如果您希望为路由组件使用上述语法,则还需要使用vue-router 2.4.0+。组件命名约定在注册组件(或道具)时,可以使用kebab-case,camelCase或PascalCase.// 组件定义组件:{ // register using kebab-case 'kebab-cased-component': { /* ... */ }, // register using camelCase 'camelCasedComponent': { /* ... */ }, // register using PascalCase 'PascalCasedComponent': { /* ... */ } }Within HTML templates though, you have to use the kebab-case equivalents:<!-- always use kebab-case in HTML templates --> <kebab-cased-component></kebab-cased-component> <camel-cased-component></camel-cased-component> <pascal-cased-component></pascal-cased-component>但是,当使用字符串模板时,我们不受HTML不区分大小写限制的约束。 这意味着即使在模板中,您也可以使用以下参考组件:
- kebab-case
- 如果使用camelCase定义了组件,则使用camelCase或kebab-case
- kebab-case,camelCase或PascalCase,如果该组件已使用PascalCase定义
components: {
'kebab-cased-component': { /* ... */ },
camelCasedComponent: { /* ... */ },
PascalCasedComponent: { /* ... */ }
}
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<camelCasedComponent></camelCasedComponent>
<pascal-cased-component></pascal-cased-component>
<pascalCasedComponent></pascalCasedComponent>
<PascalCasedComponent></PascalCasedComponent>
这意味着PascalCase是最普遍的声明约定
,kebab-case是最普遍的使用约定
。
如果你的组件没有通过slot
元素传递内容,你甚至可以/
在名称后面自动关闭:
<my-component/>
同样,这只能
在字符串模板中使用,因为自闭合自定义元素不是有效的HTML,而且您的浏览器的本地解析器无法理解它们。
递归组件
组件可以递归调用自己的模板。但是,他们只能用name
选择这样做:
name: 'unique-name-of-my-component'
当您使用全局注册组件时Vue.component
,全局ID会自动设置为组件的name
选项。
Vue.component('unique-name-of-my-component', {
// ...
})
如果你不小心,递归组件也会导致无限循环:
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
像上面这样的组件会导致“最大堆栈大小超出”错误,所以确保递归调用是有条件的(即使用v-if
最终会使用的false
)。
组件之间的循环引用
假设您正在构建文件目录树,就像在Finder或File Explorer中一样。你可能有一个tree-folder
包含这个模板的组件:
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children"/>
</p>
然后使用此模板的tree-folder-contents
组件:
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>
当你仔细观察时,你会发现这些组件在渲染树中实际上是彼此的后代和
祖先 - 这是一个悖论!在全球范围内注册组件时Vue.component
,这个悖论会自动解决。如果那是你,你可以在这里停止阅读。
但是,如果您要求使用模块系统
/导入组件,例如通过Webpack或Browserify,则会出现错误:
Failed to mount component: template or render function not defined.
为了解释发生了什么,让我们调用我们的组件A和B.模块系统认为它需要A,但是第一个A需要B,但是B需要A,但是A需要B等等。它被困在循环中,不知道如何在不先解决其他问题的情况下完全解决任何一个组件。为了解决这个问题,我们需要给模块系统一个点,它可以说:“ 最终
需要B ,但不需要首先解决B.”
在我们的案例中,让我们将这一点作为tree-folder
组件。我们知道创建悖论的孩子是tree-folder-contents
组件,所以我们要等到beforeCreate
生命周期挂钩注册它:
beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue')
}
问题解决了。
内联模板
当inline-template
特殊属性出现在子组件上时,该组件将使用其内部内容作为其模板,而不是将其视为分布式内容。这允许更灵活的模板创作。
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>
但是,inline-template
使模板的范围难以推理。作为最佳实践,更喜欢使用template
选项或.vue
文件中的template
元素在组件内定义模板。
X-模板
定义模板的另一种方法是在具有text/x-template
类型的脚本元素中,然后通过id引用模板。例如:
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
template: '#hello-world-template'
})
这些对于具有大型模板的演示或极小型应用程序非常有用,但应该避免使用它们,因为它们将模板与组件定义的其余部分分开。
带有v-once的廉价的静态组件
在Vue中渲染纯HTML元素非常快,但有时您可能会有一个包含大量
静态内容的组件。在这些情况下,您可以确保只评估一次,然后通过向v-once
根元素添加指令进行缓存,如下所示:
Vue.component('terms-of-service', {
template: '\
<div v-once>\
<h1>Terms of Service</h1>\
... a lot of static content ...\
</div>\
'
})