Vue.js 创建一个 CNODE 社区(4)
表单与 v-model / Component
内容包含:v-model / component(全局注册/局部注册/父子组件之间传递数据/slot/动态组件等..)
哈哈,又是 demo 和基础概念理解。
边写 demo 边理解总结,花费两天终于完成,我要缓一天去学学 webpack ,接着回来再战 Vue。
这边博文会略长,因为有关于组件的各个例子的 demo~全部敲一遍才能理解得更快嘛。
v-model
你可以用 v-model 指令在表单<input>、<textarea> 及 <select> 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。
Vue 的官方文档:v-model
v-model 的 demo :JSbin
Component
Vue 关于 组件的官方文档:Component
定义:组件是可复用的 Vue 实例
作用:提高代码的复用性
关于 component 的 demo:JSbin
全局注册
<div id="app">
<!-- 2.引入组件 -->
<div-component></div-component>
</div>
// 1.注册组件
Vue.component('div-component',{
template:'<div>I\'m component</div>'
})
var app = new Vue({
el:'#app',
data:{}
})
优点:所有的 Vue实例都可以使用
缺点:权限太大,容错率低
局部注册
<div id="app">
<!-- 引入局部组件 -->
<app-component></app-component>
</div>
var app = new Vue({
el:'#app',
data:{},
// 注册局部组件
components:{
'app-component':{
template:'<div>I\'m 局部 component</div>'
}
}
})
在 app实例 中创建的组件 只有在 app 挂载的 HTML 中才可以使用。
注意 HTML 中的某些标签受限:
比如 table 标签中嵌入组件的话将不会生效,因为他里面只有 tr/td/tbody 等属性,除非使用 is 将组件传递进去:
<div id="app">
<!-- 引入局部组件 -->
<table>
<tbody :is='app-component'></tbody>
</table>
</div>
使用技巧
1.命名规则:推荐使用小写加 - 的形式命名
2.template 中的内容必须要用一个 DOM 元素包裹,也可以嵌套
3.在组件的定义中,还可以使用除了 template 之外其他的属性,如 data / computed / methods 等
4.一个组件中的 data 必须是一个函数,返回一个对象(每个实例可以维护一份被返回对象的独立的拷贝)
如:
<div id="app">
<!-- 引入局部组件 -->
<app-component></app-component>
<app-component></app-component>
</div>
var app = new Vue({
el:'#app',
data:{},
// 注册局部组件
components:{
'app-component':{
template:`<div><div>I\'m 局部 component</div><button @click='countNum'>已点击{{ count }}次</button></div>`,
data:function(){
return {
msg:'component 中的 属性',
count:0
}
},
methods:{
countNum:function(){
this.count += 1
}
}
}
}
})
两个按钮每次点击,都会维护各自的 count,而不会影响到其他所有实例。
父组件传递数据给子组件(使用 props )
demo1:
<div id="app">
<!-- 引入局部组件 -->
<!-- 父组件传递数据 -->
<app-component msg='这是父组件给子组件传递的信息'></app-component>
<app-component msg='msg'></app-component>
</div>
var app = new Vue({
el:'#app',
data:{},
// 注册局部组件
components:{
'app-component':{
// 在子组件页面中渲染数据
template:`<div><div>I\'m 局部 component</div>{{ msg }}</div>`,
// 子组件通过 props 接收参数
props:[
'msg'
]
data:function(){
return {
msg:'component 中的 属性',
count:0
}
},
}
}
})
1.在组件中使用props来从父组件接收参数,注意,在props中定义的属性,都可以在组件中直接使用
2.propps来自父级,而组件中data return的数据就是组件自己的数据,两种情况作用域就是组件本身,可以在template,computed,methods中直接使用
3.props的值有两种,一种是字符串数组,一种是对象
4.可以使用v-bind动态绑定父组件来的内容:
// 1.创建 Vue 实例
var app = new Vue({
el:'#app',
data:{
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
},
// 2.注册子组件
components:{
'app-component':{
// 3.子组件通过 props 接收参数
props:[
'id',
'titel'
],
// 4.创建模板,渲染组件
template:`<div>{{ id }} - {{ title }}</div>`
}
}
})
<div id='app'>
<!-- 5.在父组件中引入子组件 -->
<!-- 6.动态绑定属性,传递到子组件 -->
<app-component
v-for='item in posts'
v-bind:id='item.id'
v-bind:title='item.title'></app-component>
</div>
单向数据流
解释 : 通过 props 传递数据 是单向的了, 也就是父组件数据变化时会传递给子组件,但是反过来不行。
目的 :是尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态。
应用场景: 业务中会经常遇到两种需要改变 prop 的情况
- 1
父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明一个数据,引用父组件的 prop
步骤一:注册组件
步骤二:将父组件的数据传递进来,并在子组件中用props接收
步骤三:将传递进来的数据通过 初始值 保存起来:
components:{
'app-component':{
props:['msg'],
data:function(){
return {
Message:this.msg
}
},
template:`<div>{{ Message }}</div>`
}
}
- 2
prop 作为需要被转变的原始值传入。这种情况用计算属性就可以了
步骤一:注册组件
步骤二:将父组件的数据传递进来,并在子组件中用props接收
步骤三:将传递进来的数据通过 计算属性 进行重新计算
<div id='app'>
<input text='text' v-model='width'>
<app-component :width='width'></app-component>
</div>
var app = new Vue({
el:'#app',
data:{
width:0
},
components:{
'app-component':{
props:['width'],
template:`<div :style='style'></div>`,
computed:{
style:function(){
return {
width:this.width +'px',
background:'yellow',
height:'20px'
}
}
}
}
}
})
因为父组件中的 input 用 v-model 绑定了父组件 data 中的 width,所以当在 input 中输入数字时,width 也会跟着变化;而子组件中使用 :width 传递着父组件的 width,template 中使用了动态绑定 :style 更新 div 的样式,所以当 width 变化时,计算属性 style 开始计算,返回一个对象,重新渲染 div 的宽度 width。
命名规则
vue组件中camelCased (驼峰式) 命名与 kebabcase(短横线命名)
在html中, myMessage 和 mymessage 是一致的,因此在组件中的html中使用必须使用kebabcase(短横线)命名方式。在html中不允许使用驼峰!:
<div id='app'>
<app-component my-msg='xxx'></app-component>
</div>
在组件中, 父组件给子组件传递数据必须用短横线。在template中,必须使用驼峰命名方式,若为短横线的命名方式。则会直接保错。
components:{
props:['myMsg'],
template:`<div>{{ myMsg }}</div>`
}
在组件的data中,用this.XXX引用时,只能是驼峰命名方式。若为短横线的命名方式,则会报错。
components:{
props:['myMsg'],
data:function(){
return {
name = this.myMsg
}
},
template:`<div>{{ myMsg }}</div>`
}
数据验证
验证父组件传递给子组件的数据的类型:
String / Number / Boolean / Object / Array / Function
<div id='app'>
<app-component :a='a' :b='b' :c='c' :d='d' :e='e' :f='f'></app-component>
</div>
var app = new Vue({
el:'#app',
data:{
a:'1',
b:1,
c:true,
d:{'name':'sgt'},
e:[],
f:console.log(),
g:67,
},
components:{
'app-component':{
// required 必传 / default 默认 / type 类型
// 如果父组件没有向子组件 传递数据,则使用 default 值
props:{
// 传入的 a 的值需要是 String/Number 类型,如果不是,则报错
a:[String,Number]
// 必须传入 b 且 b 的值是 Number类型,如果不传入(即没有 :b='b'),则报错;如果传入是其他类型,则报错
b:{type:Number,required:true},
// 传入的值需为 Boolean 类型,如果不传入(即没有 :c='c'),则使用 default 值,true;如果传入值不为 Boolean,则报错
c:{type:Boolean,default:true},
// 传入的值需为 Object 类型,如果不传入(即没有 :d='d'),则使用 default 值;如果传入值不为 Object,则报错
d:{type:Object,defult:function(){return {'name':'xxx'}}},
// 传入的值需为 Array 类型,如果不传入(即没有 :e='e'),则使用 default 值;如果传入值不为 Array,则报错
e:{type:Array,defult:function(){return [666]}},
// 传入的值需为 Function 类型
f:{type:Function}
// 自定义验证函数
g:{validator:function(value){return value>10}},
},
template:`<div>{{a}}-{{b}}-{{c}}-{{d}}-{{e}}-{{f}}-{{g}}</div>`
}
}
})
子组件传递给父组件
利用自定义事件,子组件传递数据给父组件
子组件用$emit()来触发事件 ,父组件用$on()来监昕子组件的事件
<div id="app">
<!-- 引入局部组件 -->
<!-- 自定义事件 -->
<app-component @change='handleTotal'></app-component>
</div>
var app = new Vue({
el:'#app',
data:{
// 父组件中的数据
total:1000
},
methods:{
handleTotal:function(value){
this.total = value
}
},
components:{
'app-component':{
template:`<div>
<button @click='handleCrease'>点击 +1000</button>
<button @click='handleReduce'>点击 -1000</button>
</div>`,
data:function(){
return {
count:1000
}
},
methods:{
handleCrease:function(){
this.count += 1000
this.$emit('change',this.count)
},
handleReduce:function(){
this.count -= 1000
this.$emit('change',this.count)
}
}
}
}
})
过程解析:在子组件中定义了一个数据 count 和两个监听事件 handleCrease / handleReduce,每当点击按钮,触发监听事件,则改变 count 的值,并且把改变后的 count 作为参数通过 $emit 传给自定义事件 change,change 绑定的是父组件中的方法 handleTotal,所以他执行了父组件中的方法 handleTotal,把父组件中的 total 值修改成通过自定义事件传递进来的参数 count,实现子组件向父组件传递信息。
在组件中使用 v-model
其实使用 v-model 可以用更少的代码实现上面的功能,而且不需要自定义事件:
<div id="app">
<!-- 引入局部组件 -->
<!-- 自定义事件 -->
<app-component v-model='total'></app-component>
</div>
var app = new Vue({
el:'#app',
data:{
// 父组件中的数据
total:1000
},
components:{
'app-component':{
template:`<div>
<button @click='handleCrease'>点击 +1000</button>
<button @click='handleReduce'>点击 -1000</button>
</div>`,
data:function(){
return {
count:1000
}
},
methods:{
handleCrease:function(){
this.count += 1000
this.$emit('input',this.count)
},
handleReduce:function(){
this.count -= 1000
this.$emit('input',this.count)
}
}
}
}
})
$emit的代码,实际上会触发一个input事件, ‘input’后的参数就是传递给v-model绑定的属性的值
v-model 其实是一个语法糖,这背后其实做了两个操作:
1.v-bind 绑定一个 value 属性
2.v-on 指令给当前元素绑定 input 事件
<input v-model="searchText">
<!-- 等价于 -->
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
要使用v-model,要做到:
接收一个 value 属性。
在有新的 value 时触发 input 事件
非父组件之间的通信
有时候两个组件也需要通信(非父子关系),在简单的场景下,可以使用一个空的Vue实例作为中央事件总线
<div id="app">
<app-component></app-component>
<bpp-component></bpp-component>
</div>
var app = new Vue({
el:'#app',
data:{
// 1.创建空的 Vue 实例,this.$root 代表着当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
bus:new Vue()
}
components:{
'app-component':{
data:function(){
return {
a:'通讯信息 SOS'
}
},
template:`<div><button @click='handleA2B'>点击我从 A 组件发送到 B 组件</button></div>`,
methods:{
// 2.点击执行函数,触发 $emit ,把 this.a 当成参数传递给 aEvent事件
handleA2B:function(){
this.$root.bus.$emit('aEvent',this.a)
}
}
},
'bpp-component':{
template:`<div>我是 B 组件</div>`
// 3.B 组件在实例创建的时候就监听 aEvent 事件,一旦监听到变化,即值传入函数进行处理
created:function(){
this.$root.bus.$on('aEvent',function(value){
alert('我是 B 组件,已接收到 A 组件发出的信息:' + value)
})
}
}
}
})
父链
父实例,如果当前实例有的话。子组件可以拿到父组件中的内容
<div id="app">
{{ msg }}
<app-component></app-component>
</div>
var app = new Vue({
el:'#app',
data:{
msg:'我是父组件中的 msg'
},
components:{
'app-component':{
template:`<div><button @click='changeFather'>点击设置父组件中的 msg </button></div>`,
methods:{
changeFather:function(){
this.$parent.msg = '哈哈,我已经改变了这个 msg'
}
}
}
}
})
子链
提供了为子组件提供索引的方法,用特殊的属性ref为其增加一个索引,拿到子组件中的内容
<div id="app">
{{ msg }}
<button @click='changeChild'>点击拿到子组件中的 msg</button>
<app-component ref='a'></app-component>
<bpp-component ref='b'></bpp-component>
</div>
var app = new Vue({
el:'#app',
data:{
msg:'这是父组件原始的 msg'
}
methods:{
changeChild:function(){
// 这里是 refs,而不是 ref
this.msg = this.$refs.a.msg
}
},
components:{
'app-component':{
data:function(){
return {
msg:'这是子组件中的 msg'
}
},
}
}
})
slot(插槽)
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发。Vue.js 实现了一个内容分发 API,用特殊的 ‘slot’ 元素作为原始内容的插槽。
父组件的内容与子组件相混合,从而弥补了视图的不足。
插槽嘛,顾名思义,就是在子组件中的 template 中开一个槽,槽里面可以不放数据,也可以用放默认的数据;当父组件往子组件里面插数据的时候,这个数据就会安放在槽里;如果父组件没有网子组件里面插数据,则显示槽里面的默认内容。
- 单个插槽
<div id="app">
<app-component>
<span>这是父组件安插在子组件中的内容</span>
</app-component>
</div>
var app = new Vue({
el:'#app',
components:{
'app-component':{
template:`<div>我是组件 A <slot>如果父组件没有插入内容,则显示此条消息</slot></div>`
}
}
})
- 具名插槽
<div id="app">
<app-component>
<h2 slot='header'>具名插槽</h2>
<span>这是父组件安插在子组件中的内容</span>
<div slot='footer'>插槽尾部</div>
</app-component>
</div>
var app = new Vue({
el:'#app',
components:{
'app-component':{
template:`
<div>我是组件 A
<div class='header'>
<slot name='header'></slot>
</div>
<div class='container'>
<slot>如果父组件没有插入内容,则显示此条消息</slot>
</div>
<div class='footer'>
<slot name='footer'></slot>
</div>
</div>`
}
}
})
关于 slot 注意事项
有对应的插槽,则使用对应插槽(slot='abc'/ name='abc')
插槽名称不对应,则不显示(slot='abc'/name='cba')
插入数据没有指定具名 slot,则插入到默认插槽(节点/slot)
节点没有指定具名 slot,插槽有名字(name='abc'),则不会渲染节点内容,显示插槽默认内容
作用域插槽
作用域插槽是一种特殊的slot,使用一个可以复用的模板来替换已经渲染的元素 - 从子组件获取数据
template模板是不会被渲染的,在 Vue 2.5 之后可以直接用 标签 + slot-scope
获取 slot 中的数组,而不是指定的 template 模板了
<div id="app">
<app-component>
<p slot='abc' slot-scope='temp'>
这是 slot 中的内容
{{ temp.text }}
{{ temp.prop }}
</p>
</app-component>
</div>
var app = new Vue({
el:'#app',
components:{
'app-component':{
template:`
<div>
<slot name='abc' text='这是 slot 中的 text' prop='这是 slot 中的 prop'></slot>
</div>`
}
}
})
访问 slot
通过this.$slots.(插槽名称)可以访问 slot
<div id="app">
<app-component>
<p slot='abc'>
slot is ready.
</p>
</app-component>
</div>
var app = new Vue({
el:'#app',
components:{
'app-component':{
template:`
<div>
<slot name='abc'></slot>
</div>`,
// 在 Vue 实例挂载后
mounted:function(){
let abc = this.$slots.abc // 获取了一个 Vue 所特有的 VNODE 节点
let text = abc[0].ele.innerHTML // 根据节点 API 获取内容
console.log(text) // slot is ready.
}
}
}
})
组件高级用法–动态组件
Vue 给我们提供 了一个元素叫 component
作用是: 用来动态的挂载不同的组件
实现:使用is特性来进行实现的
<div id="app">
<!-- :is 绑定的是组件名 -->
<component :is='thisView'></component>
<button @click='changeTo("A")'>点击跳到组件A</button>
<button @click='changeTo("B")'>点击跳到组件B</button>
</div>
var app = new Vue({
el:'#app',
data:{
// 默认显示的组件
thisView:'componentA'
},
methods:{
// 点击后显示改变后的组件
changeTo:function(value){
this.thisView = 'component' + value
}
},
components:{
'componentA':{
template:`
<div>
<slot name='abc'>这是组件A</slot>
</div>`,
},
'componentB':{
template:`
<div>
<slot name='cba'>这是组件B</slot>
</div>`,
}
}
})
关于 v-model 和 component 的内容就到这里啦有更深入的理解的话会回来更新的