@FarmerZ
2016-08-31T05:21:34.000000Z
字数 13092
阅读 530
组件是vue最有威力的特性之一。他帮助你扩展基础的html元素,并且包含可重用的代码。甚至可以说组件是你自定义的元素,vue的编译器可用来让他执行。在某些情况下他就是html元素,只不过拥有一个特殊的is属性。
从我们先前学过的一些知识中,我们知道可以用下面的方法创建一个vue实例。
new Vue({
el: '#some-element',
//options..
})
想要创建一个全局的组件,我们可以使用Vue.component(tagName,options)方法。eg
Vue.component('my-component',{
//options
})
注意,vue不强制实行w3c 规则来创建自定义标签的名字,(都小写、包含连字符),但是根据这一标准来创建的话是一个好的实践。
注册完组件,就可以用它创建一个模版,并当做自定义元素放在页面中。
,确保在实例化一个vue之前,一个组件已经被注册了。下面有个完整的例子。
html
<div id='example'>
<my-component></my-componeng>
</div>
js
//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>
你不必把每个组件都注册为全局性的,你可以使用component这个实例选项在其他的组件或者实例中注册一个本地组件,该组件仅仅在其父级下是可用的。
var child = {
template : '<div>a custom component</div>'
};
new Vue({
//..
components:{
'my-component': child
}
})
这种封装特性也适用于其他可注册的vue属性,如指令。
大多数可用在vue构造函数中的选项也可用在组件中,但是有两个比较特殊的例外————data和el,这两个必须用函数。如果你使用这样的写法
js
Vue.componeng('my-component',{
template:'<span>{{message}}</span>',
data:{
message:'hello'
}
})
vue将会停止并且在控制台发出警告,告诉你'data'在组件实例中必须使用一个函数来返回。这很容易理解,如果我们试下上面的组件
html
<div id='example'>
<simple-counter></simple-counter>
<simple-counter></simple-counter>
<simple-counter></simple-counter>
</div>
js
var data = {counter: 0};
Vue.component('simple-counter', {
template:'button v-on:click="counter += 1">{{counter}}</button>',
//data是一个技术上的函数,所以vue不会有什么抱怨,但是我们返回了同一个对象,这个对象被每一个组件所引用。
data: function(){
return data;
}
})
new Vue({
el: '#example-2'
})
因此,三个组件实例共享一个data对象,当其中一个增加是,其他两个也跟着增加。额,这不是我们想要的,我们可以做下面的更改来达到我们独立增加计数的目的。
js
data:function(){
return:{counter:0}
}
经过上面的说做更改,事情会变的是我们想要的。
el选项也要求是个函数,道理相同。
只要你使用字符串模版(,内联模版,或者vue组件,),你就不收html父级元素的限制。但是,如果你使用el选项来使用现成的元素当做模版。
在实际中,这些限制会导致意外结果。尽管在简单的情况下他可能可以工作,但是你不能依赖自定义组件在浏览器验证之前的展开结果。
另一个结果是你不能用自定义标签()包含ul,select,table以及其他首先得元素。自定义的标签会被提升,最终渲染或出错。
对于自定义元素应当使用is属性
html
<table>
<tr is='my-component'><tr>
</table>
不能使用在table内,这时应该是用,table可使用多个tbody
html
<table>
<tbody v-for='item items'>
<tr>Even row</tr>
<tr>Odd row</tr>
</tbody>
</table>
再次提醒,这些限制不能够应用在string模板中。
每个组件都有自己独立的数据域,这意味着在自己的模版里不能直接访问父级数据。数据可以使用props向下传递给子模版。
一个prop是一个属性,用来传递父级膜组件的信息。一个子组件需要使用props选项显示地声明想要接收的数据。(因为父级是向下传的)。
js
Vue.component('child', {
//声明props
props:['message'],
template:'<span>{{{message}}</span>'
})
然后我们可以传递数据了
html
<child message='hello'></child>
HTML的属性是类型迟钝型,所以使用非字符串、驼峰命名时需要使用他们的相应的横杠连接名字。
类似于绑定一个平常的属性到表达式,我们也可以使用v-bind命令来动态绑定父级的数据到props上。当父级的数据更新时,子级的数据也会更新。
html
<div>
<input v-bind='parentMsg'>
<br>
<child v-bind:my-message='parentMsg'></child>
</div>
当然我们经常使用简写模式
<child :my-message="parentMsg"></child>
一个常见的错误是给文本数据传递数字类型数据。
html
<!--这个传递一个纯文本‘1’-->
<comp some-prop='1'></comp>
如果我们想要传递一个真是的数字,我们需要使用v-bind,这样传递的数据会被重新赋值为js的一个表达式,也就可以让数字存在。
html
<!--这个传递的是一个数字-->
<comp v-bind:some-prop='1'></comp>
props传递的数据在父级和子级之间是单方面的,父级可以更改相应的数据,而子级不能。这会防止子级无意更改父级数据,某些方面来说,这会使数据流更坚实。
这意味着你最好不要去更改props,如果尝试那样,控制台会发出警告。如果你需要更改,你可以使用计算属性,或者定义出事属性data。
注意,如果prop是一个对象或数组,时按引用传递。在子组件内修改它**会**影响父组件的状态,不管是使用哪种类型绑定。
eg
html
<div id='parent-object-mutation-demo'>
<p>Parent:{{message.text}}</p>
<p>Child:<my-component v-bind:parent-message='message'></my-component></p>
</div>
js
Vue.compoment('my-component', {
template:'<input v-model="message-text">',
props:['parentMessage'],
data: function(){
return {message:this.partentMessage}
}
})
new Vue({
el: '#parent-object-mutation-demo',
data:{
message:{
text:'hello'
}
}
})
要想解决这个问题,你可以克隆这个对象。
js
data:function(){
return {
message:JSON.parse(JSON.stringify(this.parentMessage))
}
}
或者使用babel的es2015,这个更简单
js
data(){
return{
message:{...this.parentMessage}
}
}
porp可以被定义接受那些数据。如果定义的验证没有通过,控制台就会抛出警示。在一个需要被其他应用的组件中,这是非常有用的。
props选项在验证方面,我们使用对象替代了文字数组。
eg
js
Vue.component('example', {
props:{
//基本的类型核对(‘null’标识所有类型都可以通过)
propA:Number,
//多种可行类型
propB:[String, Number],
//只要求string字符串
propC:{
type:String,
required: true
},
//一个带有默认值的数值类型
propD:{
type:NUmber,
default: 100
},
//对象/数组,默认返回类型应该是一个工厂函数
propE:{
type: Object,
default: function(){
return {message: 'hello'}
}
},
//自定义验证函数
propF:{
validator: function(value){
return value > 10
}
}
}
})
type可以是一下的本地构造函数
* String
* Number
* Boolean
* Function
* Object
* Array
另外,type也可以是用户自定义的构造函数并且使用instanceof检测。
如果prop验证失败了,Vue将拒绝在子组件上设置此值,如果使用的是开发版本则会抛出一条警告。
子组件拥有能够访问父组件的途径——this.root。每个父组件都有一个数组——this.$children,其包含着所有的子组件。
!这些特性可以被当做逃生舱来应对极端情况。他们不是用来访问和修改大量组件的好方法,如果滥用会使你的组件难以理解。
我们应该优先明确的使用props来明确地传递数据。如果数据真的需要被分享并且很多组件可以修改,可以使用父组件来管理状态,控制所有。改变父组件的状态,自定义的事件能够被发送,然后父组件选择监听或者传递更改方法给子组件。
为了能够在复杂的应用中更好的管理状态,vuex是个官方推荐的第三方库。
你可以使用一个类似于Node.js的事件发射器————事件集中管理器,允许组件之间通信,不用考虑他们身处组件树的那个地方。因为Vue是事件发射器的实例,你可以使用一个空Vue来实现这个目的。
js
vue bus = new Vue();
//在组建A的方法
bus.$emit('id-selected',1)
//在组件B中的钩子(引发事件)
bus.$on('id-selected', function(id){
//...
})
正如你所见,事件系统做的是:
上述例子看起来十分的简单,但是当查看组件B的代码会发现,很难对‘id-selected’事件定位。这就是为什么我们推荐显式的使用v-on来在父级和子级之间进行通信。
另一个例子:
html
<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>
js
Vue.component('button-counter', {
template:'<button v-on:click="increment">{{counter}}</button>',
data: function(){
return{
counter: 0
}
},
methods:{
increment: function(){
this.counter += 1;
this.$emit('increment');
}
}
});
new Vue({
el:'#counter-event-example',
data:{
tota: 0
},
methods:{
incrementTotal: function(){
this.total += 1;
}
}
})
在这例子中,需要注意到子组件相对于外面发生的动作是松散解耦的,他所做的只是发出自己动作的信息,有可能这些信息会触发父级的一些动作。
有时候你也许需要监听一个组件的根元素的本地事件。在这种情况下,你可以在v-on后面使用.native修饰符。例子:
html
<my-component v-on:click.native='doTheThing'></my-component>
该策略(自定义事件)也可以用在创建自定义表单输入上,协同v-model工作。
注意:
html
<input v-model='something'>
语法糖
html
<input v-bind:value='something' v-on:input='something=$event.target.value'>
当使用组件,可简化为
html
<input v-bind:value='something' v-on:input="something=arguments[0]">
所以,一个组件需要使用v-model,它必须:
让我们在实际中看一下:
html
<div id="v-model-example">
<p>{{message}}</p>
<my-input label="message" v-model='message'></my-input>
</div>
js
Vue.component('my-input', {
template: "\<div class='form-group'>\
<label v-bind:for='randomId'>{{label}}:</label>\
<input v-bind:id='randomId' v-bind:value='value' v-on:input='onInput'>\
</div>\",
props:['value','label'],
data: function(){
return:{
randomId:'input-'+Math.random()
}
},
methods:{
onInput: function(){
this.$emit('input',event.target.value)
}
},
})
new Vue({
el:'#v-model-example',
data:{
message:'hello'
}
})
这个实例不仅能够用来连接组件内部的输入内容,也可以容易的集成你自己发明的输入类型。如下例子:
html
<voice-recognizer v-model="question"></voice-recognizer>
<webcam-gesture-reader v-model="gueture"></webcam-gesture-reader>
<webcam-retinal-scanner v-model='retinalImage'></webcam-retinal-scanner>
如果我们不考虑props和event的存在,我仍然想要在javascript中访问子组件的话,可以给子组件上使用ref指定一个reference ID。例子:
html
<div id="parent">
<user-profile ref="profile"></user-profile>
</div>
js
var parent = new Vue({el:'#parent'});
//访问子组件的接口
var child = parent.$refs.profile;
当ref和v-for一块使用,你将得到一个数组或者一个包含所有组件的对象。这些组件是原始组件的镜像。
使用组件的时候,我们经常想用下面的方式组合他们。
js
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>
有两点需要注意:
1 组件不知道他的挂载点会有什么内容,挂载点的内容是由的父组件决定的。
2 组件很可能有它自己的模版。
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个处理称为内容分发(或 “transclusion”,如果你熟悉 Angular)。Vue.js 实现了一个内容分发 API,参照了当前 Web 组件规范草稿,使用特殊的 元素作为原始内容的插槽。
在深入api之前,先让我们弄清内容的编译作用域。假定有如下模版:
js
<child-component>
{{message}}
</child-component>
message是父级的数据还是子级的数据呢?答案是父级。一个简单的经验是:
**父组件模版的内容在父组件作用域内编译,组件模版的内容在子组件作用域内编译。**
一个常见的错误是在父组件模版内讲一个指令绑定到子组件的属性/方法。
html
//无效
<child-template v-show='someChildProperty'></child-template>
假设someChildProperty是子组件的一个属性,那么上面的例子不会有效。父组件不清楚子组件的状态。
如果你需要在根节点绑定子组件数据,你应该在子组件的模版里这样绑定。
js
Vue.component('child-component', {
//这个是有效的,因为我们在子组件的模版里添加绑定
template: '<div v-show="somechildProperty">Child</div>',
data:function(){
return {
somechildProperty: true
}
}
})
类似的,分发内容是在父组件作用域内。
父组件的内容将被抛弃,除非子组件模板包含 。如果子组件模板只有一个没有特性的 slot,父组件的整个内容将插到 slot 所在的地方并替换它。
标签的内容视为回退内容。回退内容在子组件的作用域内编译,当宿主元素为空并且没有内容供插入时显示这个回退内容。
假设我们有一叫做my-component的组件,模版具体如下:
html
<div>
<h2>i'm the child title</h2>
<slot>
This will only be displayed if there is no content to be distributed.
</slot>
</div>
添加一父组件
html
<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>
最终的渲染结果是:
html
<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>
元素可以用一个特殊特性name配置如何分发内容。多个slot可以有不同的名字。具名slot将匹配内容片断中有对应slot特性的元素。
仍然可以有一个匿名slot,他是默认slot,作为找不到匹配的内容片断的回退插槽,如果没有默认slot,这些找不到匹配的内容片断将被抛弃。
假如,我们有一个multi-insertion组件,他的模版为:
html
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
父模版
html
<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>
渲染结果是
html
<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>
内容分发是用来组合组件内容的一个非常好的机制。
多个组件可以使用同一个挂载点,然后动态地在他们之间切换。使用保留的元素,动态地绑定到is特性。
js
new Vue({
el: 'body',
data:{
currentView:'home'
},
components:{
home:{},
posts:{},
archive:{}
}
})
html
<component v-bind:is='currentView'>
//当vm.currentView变化时,模版也会变化
</component>
如果你喜欢,你也可以直接绑定组件对象。
js
var Home = {
template:'<p>Welcome home!</p>'
};
new Vue.({
el:'body',
data:{
currentView: Home
}
})
如果把切出去的组件保留在内存中,可以保留他们的状态或避免重新渲染。为此可以添加一个keep-alive指令参数:
html
<keep-alive>
<component :is='currentView'>
<!--inactive 状态的组件将保留在内存中-->
</component>
</keep-alive>
你可以直接在定义组件上使用v-for,跟其他元素一样。
html
<my-component v-for='intem of items'></my-component>
然而,它不会之间传递数据给组件,因为组件是有着自己的独立的数据域。为了达到传递数据的目的,我们需要使用props:
html
<my-component
v-for="item in items"
v-bind:item='item'
v-bind:index='index'>
</my-component>
不自动把 item 注入组件的原因是这会导致组件跟当前v-for紧密耦合。显式声明数据来自哪里可以让组件复用在其它地方。
这有一个完整的todu列表例子:
html
<div id='todo-list-example'>
<input
v-model='newTodoText'
v-on:keyup.enter='addNewTodo'
placeholder='Add a todo'
>
<ul>
<li
is='todo-item'
v-for='(todo,index) in todo'
v-bind:title='todo'
v-on:remove='todos.splice(index,1)'
></lic>
</ul>
</div>
js
Vue.component('todo-item', {
template:'\
<li>\
{{title}}\
<button v-on:click=$emit(\'remove\')"X</button>\
<\li>\
',
props:['title']
});
new Vue.({
el:'#todo-list-example',
data:{
newTodoText:'',
todos:[
'Do the dishes',
'Take out the trash',
'Now the lawn'
]},
methods:{
addNewTodo: function(){
this.todos.push(this.newTodoText);
this.newTodoText='';
}
}
})
当编写一个组件,一个好的做法是时刻考虑到这个组件的再后面代码的可重用性,如果是使用一次就不用了,那么可以采用紧密的写法。如果需要重用那么应该给其一个干净的接口,并且尽量不要只适合特殊情况。
组建的接口API包含三个部分:props、events、slots。
使用专用的v-bind 和v-on的简写形式可以清晰简短地传达出你的想法。
html
<my-component
:foo='baz'
:bar="quz"
@event-a='doThis'
@event-b='dothat'
>
<img slot='icon' src='...'>
<p slot='main-text'>hello</p>
</my-component>
在大型应用中,我们需要把我们的应用分成小的部分。当需要一个组件时,我们可以向服务器发送请求并加载。为了容易些,Vue允许你以工厂函数的形式定义你的组件,以便动态的加载他们。Vue只有在一个组件需要渲染时候才会触发这个工厂函数,并且会缓存函数的执行结果,一方后面再次使用。例如:
js
Vue.component('async-example', function(resolve, reject){
setTimeout(function(){
resolve({
template:'<div>I am async!</dvi>'
})
},1000)
})
工厂函数接受一个resolve回调函数,它会在你向服务器请求(检索)时返回并执行。你也可以调用reject(reosn)函数来说明,当加载失败是的一些情况。至于setTimeout函数仅仅是模拟说明。怎样调用你的组件,完全取决于你。一个建议是集中使用动态组件,通过webpack's code-splitting feature
js
Vue.component('async-webpack-example', function(resolve){
//这是一个特殊的require语法,他会通知webpack自动分离你的代码到bundle中,在需要时通过ajax请求进行加载。
require(['./my-async-component'], resolve)
})
当注册一个组件时,你可以使用驼峰习惯命名也可以使用烤串习惯,vue没有限制。
js
//定义一个组件时
component:{
//定义使用驼峰习惯
'kabab-cased-component':{},
'cameCasedComponent':{},
"TitleCasedCommponent":{}
}
在HTML转化模版,你必须使用相应的烤串形式。
html
<!--在HTML中只能使用烤串模式-->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<title-cased-component></title-cased-component>
然而使用文字模版时,我们不用必须使用烤串模式,你可以使用喜欢换的任何模式,因为这些可以在HTML中自动转化为烤串模式。
html
<!-- use whatever you want in string templates! -->
<my-component></my-component>
<myComponent></myComponent>
<MyComponent></MyComponent>
如果你的组件不包含slot元素,你甚至可以使用在名字后面使用直接关闭。
html
<my-component/>
组件可以在自己的模板中递归地调用自己,前提是必须有一个名字选项。
js
name:'stack-overflow',
template:'<div><stack-overflow></stack-overflow></div>'
像上面的组件执行的结果会得到一个'max stack size exceeded'错误,所以,必须有一个调用条件(例如使用v-if,并且最终有一个false结果来结束递归)。当你使用Vue.component注册一个全剧组件。全局id自动设置为组建的name选项。
当inline-template属性被用在一个子组件上,组件会把他们包括的元素当做模板的一部分。这允许更灵活的模版编写。
html
<my-component inline-component>
<p>这个将被编译成组件模版的一部分</p>
<p>是当前模版而不是父模版</p>
</my-component>
但是,inline-template会使编译数据域显得不那么容易理解。更好的做法是,在模版内部使用template选项或者在一个.vue文件里使用template元素。
另一个做法是在标签内部定义type为'text/x-template',然后通过id引用模版。例子:
html
<script type="text/x-template" id="hello-world-template">
<p>hello world</p>
</script>
在大的模板中,或者非常小的应用中示例时很有用,但是其他的会被忽视,因为他把模版从组件中分开了。
vue渲染纯html元素的速度是非常快的。但是有时候你的组件包含了很多静态内容(图片等),在这种情况下,你可以使它们只被渲染一次便被缓存起来,方式通过在在外层的标签使用v-once属性。例子
js
Vue.component("term-of-serveic", {
template:'\
<div v-once>\
<h1>Terms of Service</h1>\
...其他静态资源...\
</div>\',
})