代码下载
组件 (Component) 是 Vue.js 最强大的功能之一,组件可以扩展 HTML 元素,封装可重用的代码。
组件注册
全局注册:
Vue.component(组件名称, {
data: 组件数据,
template: 组件模板内容
});
// 全局组件
Vue.component('button-counter', {
data: function() {
return { count: 0 };
},
template: `<button @click="handle" v-text="'点击了(' + count + ')次'"></button>`,
methods: {
handle: function() {
console.log(this.count);
this.count++;
}
}
});
局部注册:只能在当前注册它的vue实例中使用
let vm = new Vue({
el: '#app',
data: {
},
// 局部组件
components: {
'myComponent': {
data: function() {
return { msg: 'hello' };
},
template: `<div v-text='msg'></div>`
}
}
});
组件使用:
<h3>全局组件</h3>
<button-counter></button-counter>
<h3>局部组件</h3>
<my-component></my-component>
注意事项:
- data必须是一个函数。
- 组件模板内容必须是单个跟元素,组件模板内容可以是模板字符串,模板字符串需要浏览器提供支持(ES6语法)。
- 组件命名方式有短横线方式(
Vue.component('my-component', { /* ... */ })
),驼峰方式(Vue.component('myComponent', { /* ... */ })
)但是必须使用短横线的方式使用组件。
Vue调试工具vue-devtools
安装
- 打开终端,进入指定目录,执行
mkdir 文件夹名
命令创建文件夹。 - 执行
cd 文件夹名
进入刚创建的文件夹目录,执行npm install vue-devtools
命令开始下载(前提:安装node.js)。 - 下载完成后,进入该文件下的 node_modules 文件,进入 vue-devtools 此文件夹,将其中vender 文件夹下的 manifest.json文件 进行编辑,修改 persistent 为 true 保存。
- 打开浏览器找到扩展程序(以谷歌为例),并勾选开发者模式,点击出现的“加载扩展程序”按钮,最后选择vender 文件,重启浏览器就可以使用了。
编写如下代码,谷歌浏览器打开,打开开发者工具,选择“vue”,即可使用了:
<body>
<div class="main">
<h2>调试工具(vue-devTools)</h2>
<div id="app">
<div v-text="root"></div>
<first-com></first-com>
</div>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('first-com', {
data: function() {
return { first: '一级组件' }
},
template: `
<div>
<div v-text='first'></div>
<second-com></second-com>
</div>
`
});
Vue.component('second-com', {
data: function() {
return { second: '二级组件' }
},
template: `
<div v-text='second'></div>
`
});
let vm = new Vue({
el: '#app',
data: {
root: '顶层根组件'
}
});
</script>
</body>
组件间数据交互
父组件向子组件传值
- 父组件发送的形式是以属性的形式绑定值到子组件身上。
- 然后子组件用属性props接收。
- 在props中使用驼峰形式,模板中需要使用短横线的形式,字符串形式的模板中没有这个限制。
<body>
<div class="main" id="app">
<h2>父组件向子组件传值</h2>
<son-com :parent-title="title" :parent-content="content"></son-com>
</div>
<script src="../js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
title: '标题',
content: '内容'
},
components: {
'son-com': {
props: ['parentTitle', 'parentContent'],
data: function() {
return { info: '我的信息' }
},
template: `<div v-text="'title: ' + parentTitle + 'content: ' + parentContent + 'info: ' + info"></div>`
}
}
});
</script>
</body>
props属性值类型有字符串 String、数值 Number、布尔值 Boolean、数组 Array、对象 Object;通过v-bind传递数据类型为其自身类型,直接属性赋值则为字符类型。
<type-com :pstr="123" :pnum="123" :pbool="true" :parr="[1, 2, 3]" :pobj="{ a: '123', b: '456' }"></type-com>
<type-com pstr="123" pnum="123" pbool="true" parr="[1, 2, 3]" pobj="{ a: '123', b: '456' }"></type-com>
components: {
'type-com': {
props: ['pstr', 'pnum', 'pbool', 'parr', 'pobj'],
template: `<div>
<div v-text='typeof pstr'></div>
<div v-text='typeof pnum'></div>
<div v-text='typeof pbool'></div>
<div v-text='typeof parr'></div>
<div v-text='typeof pobj'></div>
</div>`
}
}
// number
// number
// boolean
// object
// object
// string
// string
// string
// string
// string
子组件向父组件传值
- 子组件用 $emit() 自定义事件,第一个参数为 自定义的事件名称 第二个参数为需要传递的数据
- 父组件用v-on监听子组件的事件
<body>
<div class="main" id="app">
<h2>子组件向父组件传值</h2>
<div :style="{fontSize: fontSize + 'px'}">父组件内容</div>
<son-com :data="info" :size="fontSize" @large-text="handle"></son-com>
</div>
<script src="../js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
info: ['萝卜', '白菜', '土豆'],
fontSize: 18
},
components: {
'son-com': {
props: ['data'],
data: function() {
return { size: '' }
},
template: `<div>
<ul>
<li :key='index' v-for='(value, index) in data' v-text='value'></li>
</ul>
<input type='text' v-model.number='size'>
<button @click='$emit("large-text", size)'>增加字号</button>
</div>`
}
},
methods: {
handle: function(v) {
console.log('value: ', v);
this.fontSize += v;
console.log(this.fontSize);
}
}
})
</script>
</body>
兄弟组件的传值
- 兄弟组件传递数据需要借助于事件中心,通过事件中心传递数据,提供事件中心 var hub = new Vue()
- 传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据)
- 接收数据方,通过mounted(){} 钩子中 触发hub.$on()方法名
- 销毁事件通过hub.$off()方法名,销毁之后无法进行传递数据
<body>
<div class="main" id="app">
<h2>兄弟组件传值</h2>
<div>
<one-com></one-com>
<two-com></two-com>
<button @click="destroy">销毁</button>
</div>
</div>
<script src="../js/vue.js"></script>
<script>
// 事件中心
let hub = new Vue();
// 组件one
Vue.component('one-com', {
data: function() {
return { count: 0 };
},
template: `<div>
<div v-text='"one: " + count'></div>
<button @click='handle'>点击</button>
</div>`,
methods: {
handle: function() {
// 触发事件
hub.$emit('one-event', 1);
}
},
mounted: function() {
// 监听事件
hub.$on('two-event', (v) => {
this.count += v;
});
}
});
// 组件two
Vue.component('two-com', {
data: function() {
return { count: 0 };
},
template: `<div>
<div v-text='"two: " + count'></div>
<button @click='handle'>点击</button>
</div>`,
methods: {
handle: function() {
hub.$emit('two-event', 2);
}
},
mounted: function() {
hub.$on('one-event', (v) => {
this.count += v;
});
}
});
let vm = new Vue({
el: '#app',
data: {
},
methods: {
destroy: function() {
hub.$off('one-event');
hub.$off('two-event');
}
}
});
</script>
</body>
组件插槽
插槽可以为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。举例来说,这里有一个 组件,可以像这样使用:
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而 <FancyButton>
的模板是这样的:
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
最终渲染出的 DOM 是这样:
<button class="fancy-btn">Click me!</button>
默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如模板是这样的一个组件:
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
具名插槽
组件中包含多个插槽出口是很有用,对于这种场景,<slot>
元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID。举例来说,在一个 <BaseLayout>
组件中以确定每一处要渲染的内容:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default”。
在父组件中需要一种方式将多个插槽内容传入到各自目标插槽的出口,此时就需要用到具名插槽了。要为具名插槽传入内容,需要使用一个含 v-slot
指令的 <template>
元素,并将目标插槽的名字传给该指令,v-slot
有对应的简写 #
,因此 <template v-slot:header>
可以简写为 <template #header>
。其意思就是“将这部分模板片段传入子组件的 header 插槽中”:
<base-layout>
<template v-slot:header>
<h4>页眉</h4>
</template>
<template v-slot:default>
<p>主体1</p>
<p>主体2</p>
</template>
<template #footer>
<p>页脚</p>
</template>
</base-layout>
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。所以上面也可以写成:
<base-layout>
<template v-slot:header>
<h4>页眉</h4>
</template>
<p>主体1</p>
<p>主体2</p>
<template #footer>
<p>页脚</p>
</template>
</base-layout>
也可以通过slot属性来指定, 这个slot的值必须和下面slot组件得name值对应上,如果没有匹配到则放到匿名的插槽中:
<base-layout>
<h4 slot="header">页眉</h4>
<p>主体1</p>
<p>主体2</p>
<p slot="footer">页脚</p>
</base-layout>
作用域插槽
在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,需要一种方法来让子组件在渲染时将一部分数据提供给插槽。确实有办法这么做!可以像对组件传递 props
那样,向一个插槽的出口上传递 attributes
:
<div>
<slot :text='"你好"' v-bind:count='1'></slot>
</div>
当需要接收插槽 props
时,默认插槽和具名插槽的使用方式有一些小区别。先展示默认插槽如何接受 props
,通过子组件标签上的 v-slot
指令,直接接收到了一个插槽 props
对象:
<my-component v-slot="data">
{{data.text}}
{{data.count}}
</my-component>
子组件传入插槽的 props
作为了 v-slot
指令的值,可以在插槽内的表达式中访问。
具名作用域插槽的工作方式也是类似的,插槽 props
可以作为 v-slot
指令的值被访问到:v-slot:name=“data”。当使用缩写时是这样:
<one-component>
<template #header="data">
{{data}}
</template>
<template #default="data">
{{data}}
</template>
<template #footer="data">
{{data}}
</template>
</one-component>
向具名插槽中传入 props:
template: `<div>
<header>
<slot name='header' :msg='"早上好"' v-bind:uname='"张三"''></slot>
</header>
<main>
<slot :msg='"中午好"' v-bind:uname='"李四"'></slot>
</main>
<footer>
<slot name='footer' v-bind:msg='"晚上好"' :uname='"王五"'></slot>
</footer>
</div>`
同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template>
标签。尝试直接为组件添加 v-slot
指令将导致编译错误。这是为了避免因默认插槽的 props
的作用域而困惑:
<!-- 该模板无法编译 -->
<one-component>
<template #header="data">
{{ data }}
</template>
<p v-slot="data">{{ data }}</p>
<template #footer="data">
{{ data }}
</template>
</one-component>
组件化案例-购物车
略,具体见代码实现:
<body>
<div class="main" id="app">
<h2>组件化案例-购物车</h2>
<div class="container">
<my-cart></my-cart>
</div>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('MyCart', {
data: function() {
return {
title: '我的商品',
list: [{
id: 1,
name: 'TCL彩电',
price: 100,
num: 1,
img: 'img/a.jpg'
},{
id: 2,
name: '机顶盒',
price: 200,
num: 1,
img: 'img/b.jpg'
},{
id: 3,
name: '海尔冰箱',
price: 300,
num: 1,
img: 'img/c.jpg'
},{
id: 4,
name: '小米手机',
price: 400,
num: 1,
img: 'img/d.jpg'
},{
id: 5,
name: 'PPTV电视',
price: 500,
num: 2,
img: 'img/e.jpg'
}]
}
},
template: `
<div class="cart">
<cart-title :title='title'></cart-title>
<cart-list :list='list' @cart-del='deleteCart' @cart-change='changCartNum'></cart-list>
<cart-total :list='list'></cart-total>
</div>
`,
methods: {
deleteCart: function(i) {
this.list.splice(i, 1);
},
changCartNum: function(i, t, v) {
let item = this.list[i];
if (t === 'sub') {
if (item.num <= 0) return;
item.num--;
} else if (t === 'add') {
item.num++;
} else {
item.num = v;
}
}
},
components: {
'CartTitle': {
props: ['title'],
template: `
<div class="title" v-text='title'></div>
`
},
'CartList': {
props: ['list'],
template: `
<div>
<div class="item" :key="v.id" v-for="(v, i) in list">
<img :src="'../' + v.img" alt="">
<div class="name" v-text="v.name"></div>
<div class="change">
<a href="javascritp:;" @click.prevent='$emit("cart-change", i, "sub")'>-</a>
<input type="text" class="num" :value='v.num' @blur='$emit("cart-change", i, "change", $event.target.value)'>
<a href="javascritp:;" @click.prevent='$emit("cart-change", i, "add")'>+</a>
</div>
<div class="del" @click='$emit("cart-del", i)'>×</div>
</div>
</div>
`
},
'CartTotal': {
props: ['list'],
template: `
<div class="total">
<span v-text='"总价:" + totalAmount'></span>
<button>结算</button>
</div>
`,
computed: {
totalAmount: function() {
let amount = 0;
for (let index = 0; index < this.list.length; index++) {
const element = this.list[index];
amount += element.price * element.num;
}
return amount;
}
}
},
}
});
let vm = new Vue({
el: '#app',
data: {
}
});
</script>
</body>