@FarmerZ
2016-08-31T05:21:34.000000Z
字数 13092
阅读 573
组件是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
//registerVue.component('my-component',{template:'<div>A custom component!</div>'})//create a root instancenew 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', {//声明propsprops:['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-componentv-for="item in items"v-bind:item='item'v-bind:index='index'></my-component>
不自动把 item 注入组件的原因是这会导致组件跟当前v-for紧密耦合。显式声明数据来自哪里可以让组件复用在其它地方。
这有一个完整的todu列表例子:
html
<div id='todo-list-example'><inputv-model='newTodoText'v-on:keyup.enter='addNewTodo'placeholder='Add a todo'><ul><liis='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>\',})