组件介绍
组件是可复用的 Vue 实例,且带有一个名字。
组件可以扩展 HTML 元素,封装可重用的代码。
组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树。
组件是可复用的vue实例,所以它们也能接收data、computed、watch、methods以及生命周期钩子等选项。
自定义组件
vue组件分为全局组件和局部组件。
全局组件
使用 Vue.component 来定义全局组件,紧接着用 new Vue({ el: '#container '}) 在每个页面内指定一个容器元素。
使用步骤
注册组件
- 调用vue.extend()创建一个组件构造器。
该构造器中有一个选项对象的template属性可以用来定义组件要渲染的HTML。也可以省略构造器,直接在组册组件时直接使用template属性。
var MyComponent = Vue.extend({
// 选项...
})
- 使用vue.component()注册组件,把构造器关联到组件。
需要提供2个参数:组件的标签和组件构造器。vue.component()内部会调用组件构造器,创建一个组件实例。
// 全局注册组件,tag 为 my-component
Vue.component('my-component', MyComponent)
为了简化组件使用,可以在注册组件时直接传入选项对象而不是构造器给 Vue.component() 和 component 选项。Vue.js 在背后自动调用 Vue.extend()。
Vue.component('component-name', {
data: '组件数据',
template: '组件模板内容'
})
用法示例:
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<button-counter></button-counter>
</div>
<script>
Vue.component('button-counter', {
data: function() {
return {
count: 0
}
},
template: '<button v-on:click="count++">点击了{{count}}次</button>'
})
vm = new Vue(
{
el: "#app"
}
)
</script>
</body>
</html>
注意:
data必须是一个函数,组件模板内容必须是单个跟元素
挂载组件
将组建挂载到某个vue实例下。
在注册之后,组件便可以用在父实例的模块中,以自定义元素 <my-component> 的形式使用。
<div id="example">
<my-component></my-component>
</div>
示例代码:
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<btn-component></btn-component>
</div>
<script>
let btn_template = Vue.extend(
{
template: "<input type='button' value='vue项目'> </input>"
}
)
Vue.component('btn-component', btn_template)
vm = new Vue(
{
el: "#app"
}
)
</script>
</body>
</html>
全局组件使用场景
如果该组件的特定功能需要在任何被Vue实例挂载的标签中使用,推荐使用全局组件。
- 全局组件可以在任何被挂着的标签中使用。
- 全局组件的配置对象中必须包含template属。
全局组件的缺点
- 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
- 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 \
- 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
- 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器,如 Pug (formerly Jade) 和 Babel
局部组件
全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /*...*/ }
var ComponentB = { /*...*/ }
var ComponentC = { /*...*/ }
new vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB,
'component-c': ComponentC,
}
})
组件模块化
模块中局部注册组件
如果使用了webpack 的模块系统。可以创建一个 components 目录,并将每个组件放置在其各自的文件中。然后,在局部注册之前导入组件。
假设在components文件夹下面创建了ComponentA.vue文件,我们可以在新文件ComponentB.vue中采用如下方式导入组件
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
},
// ...
}
组件全局化
可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。
所以会导致很多组件里都会有一个包含基础组件的长列表:
import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'
export default {
components: {
BaseButton,
BaseIcon,
BaseInput
}
}
如果使用了 webpack,可以在应用入口文件 (比如 src/main.js) 中使用 require.context 只全局注册这些非常通用的基础组件。
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
组件间数据交互
父组件通过props向子组件传值
基本用法
Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个属性。
子组件通过props接收传递过来的值:
Vue.component('blog-post', {
// 用于接收父组件传递过来的数据
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
父组件通过属性的方式将值传递给子组件:
//父组件title属性向子组件传递值
<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>
浏览器效果:
动态传递 prop
获取动态属性数组
在实际项目中,属性值通常是从API获取,事先并不知道传递的属性列表长度。这种情况下可以使用 v-bind 来动态传递 prop。
对于父组件中的属性值,可以在data属性中获取相应的属性值。对于上面的例子,可以定义一个data数组存储title对应的属性值。
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<!-- 父组件title属性向子组件传递值 -->
<blog-post v-for="post in posts" v-bind:key='post.id' v-bind:title="post.title"></blog-post>
</div>
<script>
Vue.component('blog-post', {
props: ['title'], // 用于接收父组件接收过来的数据
template: '<h3>{{ title }}</h3>'
})
vm = 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"}
]
}
}
)
</script>
</body>
</html>
获取动态属性
当组件的属性变得越来越多越复杂的时候,为每一个属性定义prop会变得很复杂。例如:
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:title="post.title"
v-bind:content="post.content"
v-bind:publishedAt="post.publishedAt"
v-bind:comments="post.comments"
></blog-post>
此时,可以在prop中直接定义一个对象,当对象中加入新属性时,template中可以自动使用属性
<div id="app">
<!-- 父组件title属性向子组件传递值 -->
<blog-post v-for="post in posts" v-bind:key='post.id' v-bind:post="post"></blog-post>
</div>
<script>
Vue.component('blog-post', {
props: ['post'], // 用于接收父组件接收过来的数据
template: `
<div class="blog-post">
<h3>{{ post.title }}</h3>
<div v-html="post.content"></div>
</div>
`
})
vm = new Vue(
{
el: "#app",
data: {
posts: [
{id: 1, title: "vue", content: "Hello vue", "comment": "front-end"},
{id: 2, title: "python", content: "Hello python", "comment": "back-end"},
{id: 3, title: "java", content: "Hello java", "comment": "back-end"}
]
}
}
)
</script>
浏览器效果:
改变 prop的值
每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。
通常有两种改变 prop 的情况:
- prop 作为初始值传入,子组件之后只是将它的初始值作为本地数据的初始值使用;
- prop 作为需要被转变的原始值传入。
情况一:定义一个局部 data 属性,并将 prop 的初始值作为局部数据的初始值。
props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}
情况二:定义一个 computed 属性,此属性从 prop 的值计算得出。
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
prop 验证
组件可以为 props 指定验证要求。如果未指定验证要求,Vue 会发出警告。当组件给其他人使用时这很有用。
prop 是一个对象而不是字符串数组时,它包含验证要求:
Vue.component('example', {
props: {
// 基础类型检测 (`null` 意思是任何类型都可以)
propA: Number,
// 多种类型
propB: [String, Number],
// 必传且是字符串
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 将拒绝在子组件上设置此值,如果使用的是开发版本会抛出一条警告。
子组件通过events给父组件发消息
子组件要把数据传递回去,就需要使用自定义事件。
监听子组件事件
可以使用 v-on 绑定自定义事件, 每个 Vue 实例都实现了事件接口(Events interface),即:
- 使用 $on(eventName) 监听事件。
vm.$on( event, fn )//监听event事件后运行 fn; - 使用 $emit(eventName) 触发父组件的自定义事件。
vm.$emit( event, arg ) //触发当前实例上的事件;
父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。
示例代码如下:
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<!-- 父组件监听子组件的事件increment -->
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
<script>
Vue.component('button-counter', {
template: '<button v-on:click="incrementhandler">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
incrementhandler: function () {
this.counter += 1
this.$emit('increment') //子组件触发increment事件
}
}
})
vm = new Vue(
{
el: "#app",
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
}
)
</script>
</body>
</html>
浏览器效果:
事件抛出值
$emit 的第二个参数可以传递数据给父组件
父组件中可以通过$event捕获到子组件传递的数据
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
<!-- 父组件中可以通过$event捕获到子组件传递的数据 -->
v-bind:post="post" v-on:enlarge-text="postFontSize += $event"
></blog-post>
</div>
</div>
<script>
Vue.component('blog-post', {
props: ['post'],
template: `
<div class="blog-post">
<h3>{{ post.title }}</h3>
// $emit 的第二个参数可以传递数据给父组件
<button v-on:click="$emit('enlarge-text', 0.1)">
Enlarge text
</button>
<div v-html="post.content"></div>
</div>
`
})
vm = new Vue(
{
el: '#app',
data: {
posts: [
{id: 1, content: "hello", title: "btn_1"},
{id: 2, content: "hello vue", title: "btn_2"}
],
postFontSize: 1
}
}
)
</script>
</body>
</html>
在实际项目中,父组件可以定义函数接收到子组件事件值。
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post" v-on:enlarge-text="enlargeText"
></blog-post>
</div>
</div>
<script>
Vue.component('blog-post', {
props: ['post'],
template: `
<div class="blog-post">
<h3>{{ post.title }}</h3>
<button v-on:click="$emit('enlarge-text', 0.1)">
Enlarge text
</button>
<div v-html="post.content"></div>
</div>
`
})
vm = new Vue(
{
el: '#app',
data: {
posts: [
{id: 1, content: "hello", title: "btn_1"},
{id: 2, content: "hello vue", title: "btn_2"}
],
postFontSize: 1
},
methods:{
enlargeText: function (cnt) {
this.postFontSize += cnt
}
}
}
)
</script>
</body>
</html>
v-model 数据双向绑定
自定义事件用来创建自定义的表单输入组件,可以使用 v-model 来进行数据双向绑定。
v-model指令的本质是: 它负责监听用户的输入事件,从而更新数据,并对一些极端场景进行一些特殊处理。同时,v-model会忽略所有表单元素的value、checked、selected特性的初始值,它总是将vue实例中的数据作为数据来源。 然后当输入事件发生时,实时更新vue实例中的数据。
<input v-model="something">
//value会自动把输入值赋值给vue实例的attr字段。
仅仅是一个语法糖,等价于:
<input v-bind:value="something" v-on:input="something = $event.target.value">
以在组件中使用时,它相当于下面的简写:
<custom-input v-bind:value="something" v-on:input="something = arguments[0]"></custom-input>
组件的 v-model 生效,它必须:
- 接受一个 value 属性
- 在有新的 value 时触发 input 事件
示例:
输入框接收price,不使用v-model时,代码如下
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<price-input :value="price" @input="onInput"></price-input>
<p>输入价格: {{price}}</p>
</div>
<script>
Vue.component('price-input', {
props: ['value'],
template: `
<div>
<input
type="text"
placeholder="请输入"
v-bind:value="value"
v-on:input="updateVal($event.target.value)"
>
</div>
`,
methods: {
updateVal: function(val) {
this.$emit('input', val);
}
}
})
vm = new Vue(
{
el: '#app',
data:{
price:'123'
},
methods: {
onInput: function (val) {
console.log("控制台打印output详情")
this.price = val
console.log(this.price)
}
}
}
)
</script>
</body>
</html>
使用v-model代替复杂的双向绑定过程,代码如下
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<price-input v-model="price"></price-input>
<p>输入价格: {{price}} </p>
</div>
<script>
Vue.component('price-input', {
props: ['value'],
template: `
<div>
<input
type="text"
placeholder="请输入"
v-bind:value="value"
v-on:input="updateVal($event.target.value)"
>
</div>
`,
methods: {
updateVal: function(val) {
this.$emit('input', val);
}
}
})
vm = new Vue(
{
el: '#app',
data:{
price:'123'
},
methods: {
onInput: function(val) {
this.price = val
console.log("控制台打印output详情: " + this.price)
}
}
}
)
</script>
</body>
</html>
Slot 内容分发
插槽(Slot),用于决定将所携带的内容,插入到指定的某个位置。
插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示就由子组件来进行控制。
为什么要用slot:
- 为了保证组件内容的灵活性,组件的内容由其所在的上下文环境决定;
- 将组件的内部层级透明的展现在外部环境中,即外部环境不需要关注字组件的层级。
slot基本用法
默认情况下,父组件在子组件内套的内容,不会在页面上显示。
例如,需要在子组件<price-input>中嵌入父组件的文档提示。
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<price-input v-model="price">
请输入价格:<!--默认情况下这行不会显示-->
</price-input>
</div>
<script>
Vue.component('price-input', {
props: ['value'],
template: `
<div>
<input
type="text"
v-bind:value="value"
v-on:input="updateVal($event.target.value)"
>
</div>
`,
methods: {
updateVal: function(val) {
this.$emit('input', val);
}
}
})
vm = new Vue(
{
el: '#app',
data:{
price:'123'
},
methods: {
onInput: function(val) {
this.price = val
console.log("控制台打印output详情: " + this.price)
}
}
}
)
</script>
</body>
</html>
浏览器效果:
假如父组件需要在子组件内放一些DOM,那么这些DOM是显示或者隐藏,在哪个地方显示,怎么显示,需要slot分发负责。
父组件放在子组件里的内容,插到了子组件位置;
注意:即使有多个标签,会一起被插入,相当于在父组件放在子组件里的标签,替换了<slot></slot>这个标签。
在子组件中,需要显示父组件内容的地方,加上slot即可显示父组件内容。
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<price-input v-model="price">
请输入价格:
</price-input>
</div>
<script>
Vue.component('price-input', {
props: ['value'],
template: `
<div>
<slot>
<p>父组件没传入slot,使用默认价格:</p>
</slot>
<input
type="text"
v-bind:value="value"
v-on:input="updateVal($event.target.value)"
>
</div>
`,
methods: {
updateVal: function(val) {
this.$emit('input', val);
}
}
})
vm = new Vue(
{
el: '#app',
data:{
price:'123'
},
methods: {
onInput: function(val) {
this.price = val
console.log("控制台打印output详情: " + this.price)
}
}
}
)
</script>
</body>
</html>
浏览器效果:
具名slot
给\ 元素指定一个 name 后可以分发多个内容。
<meta charset="utf-8">
<title>vue小白</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<body>
<div id="app">
<demo_component>
<h2 slot="slot_1">父组件的标题</slot></h2>
<p>父组件的内容</p>
<div slot="slot_2">父组件的结尾</div>
<p>父组件默认的内容2</p>
</demo_component>
</div>
<script>
Vue.component('demo_component', {
template: `
<div class="demo">
<div class="header">
<slot name="slot_1"> 子组件slot_1默认值</slot>
</div>
<div class="main">
<slot>子组件main默认值</slot>
</div>
<div class="footer">
<slot name="slot_2">子组件slot_2默认值</slot>
</div>
</div>
`
})
vm = new Vue(
{
el: '#app'
}
)
</script>
</body>
</html>
以上代码,<div class=“main”>中的slot没有使用name属性,它将作为默认 slot 出现,组件没有使用 slot 特性的元素与内容都将出现在这里。如果没有指定默认的匿名 slot, 父组件内多余的内容片段都将被抛弃。
浏览器效果如下:
作用域slot-scope
作用域插槽允许我们将插槽转换为可复用的模板,这些模板可以基于子组件传来的数据渲染出不同的内容。这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的。
参考文献:
vue官方文档
vue slot用法