Vue 组件
组件是一个可复用的 Vue 实例,并且带有一个名字。
示例:定义一个名为 button-counter
的组件
1Vue.component('button-counter', {
2 data: function () {
3 return {
4 count: 0
5 }
6 },
7 template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
8})
组件需放在一个 Vue 根实例对应的模板中,把这个组件作为自定义元素来使用。如下面的例子:
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Components demo</title>
6 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
7</head>
8
9<body>
10 // 组件可以进行任意次复用
11 <div id="components-demo">
12 <button-counter></button-counter>
13 <button-counter></button-counter>
14 <button-counter></button-counter>
15 </div>
16
17</body>
18<script>
19 // 定义一个名为 button-counter 的新组件
20 Vue.component('button-counter', {
21 data: function () {
22 return {
23 count: 0
24 }
25 },
26 template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
27 })
28 var app = new Vue({ el: '#components-demo' });
29</script>
30
31</html>
Vue 组件与 Vue 根实例的区别:
- Vue 组件中没有
el
属性。 - Vue 组件中的
data
必须是一个函数。这样每个组件实例中的data
都是独立的,不会相互影响。
另外,每个组件中必须只有一个根元素。
组件注册
组件有两种注册方式:
- 全局注册
- 局部注册
组件名称
定义组件名称的方式有两种:
- 使用 kebab-case
Vue.component('my-component-name', { /* ... */ })
- 使用 PascalCase
Vue.component('MyComponentName', { /* ... */ })
当使用 kebab-case 方式定义一个组件,在引用这个组件时也应该使用同样的形式。
当使用 PascalCase 方式定义一个组件,在引用这个组件时两种命名法都可以使用。
另外,直接在 DOM 中使用时,只有 kebab-case 形式是有效的。
全局注册
全局注册之后,组件可以用在任何新建的 Vue 根实例的模板中。
1// 组件名就是 Vue.component 的第一个参数。
2Vue.component('component-a', { /* ... */ })
3Vue.component('component-b', { /* ... */ })
4Vue.component('component-c', { /* ... */ })
5
6new Vue({ el: '#app' })
1<div id="app">
2 <component-a></component-a>
3 <component-b></component-b>
4 <component-c></component-c>
5</div>
局部注册
通过一个普通的 JavaScript 对象来定义组件:
1var ComponentA = { /* ... */ }
2var ComponentB = { /* ... */ }
3var ComponentC = { /* ... */ }
然后在 components
属性中指定要使用的组件:
1new Vue({
2 el: '#app',
3 components: {
4 'component-a': ComponentA,
5 'component-b': ComponentB
6 }
7})
模块系统
在使用 Babel 和 webpack 等模块系统时,Vue 建议将每个组件放在各自的文件中。当需要使用这些组件时,通过 import/require
导入。
示例:局部注册
1import ComponentA from './ComponentA'
2import ComponentC from './ComponentC'
3
4export default {
5 components: {
6 ComponentA,
7 ComponentC
8 },
9 // ...
10}
11// 现在 ComponentA 和 ComponentC 都可以在当前文件使用了
示例:全局注册通用的基础组件
1import Vue from 'vue'
2import upperFirst from 'lodash/upperFirst'
3import camelCase from 'lodash/camelCase'
4
5const requireComponent = require.context(
6 // 其组件目录的相对路径
7 './components',
8 // 是否查询其子目录
9 false,
10 // 匹配基础组件文件名的正则表达式
11 /Base[A-Z]\w+\.(vue|js)$/
12)
13
14requireComponent.keys().forEach(fileName => {
15 // 获取组件配置
16 const componentConfig = requireComponent(fileName)
17
18 // 获取组件的 PascalCase 命名
19 const componentName = upperFirst(
20 camelCase(
21 // 获取和目录深度无关的文件名
22 fileName
23 .split('/')
24 .pop()
25 .replace(/\.\w+$/, '')
26 )
27 )
28
29 // 全局注册组件
30 Vue.component(
31 componentName,
32 // 如果这个组件选项是通过 `export default` 导出的,
33 // 那么就会优先使用 `.default`,
34 // 否则回退到使用模块的根。
35 componentConfig.default || componentConfig
36 )
37})
Prop
Prop 是在组件上注册的一些自定义 attribute。通常用 prop 来向子组件传递数据,传递的流程大概如下:
简单的说,就是父组件实例通过父组件的模板(包含子组件元素)将值传给子组件实例,子组件实例再将值传给他的模板(template)。
其中,组件中的 props
属性就是用来接收父组件数据的。
上图中的代码:
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Blog psot demo</title>
6 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
7</head>
8
9<body>
10 <div id="blog-post-demo" class="demo">
11 <blog-post v-for="post in posts" v-bind:key="post.id" v-bind:title="post.title" />
12 </div>
13
14</body>
15<script>
16 Vue.component('blog-post', {
17 props: ['title'],
18 template: '<h3>{{ title }}</h3>'
19 })
20 new Vue({
21 el: '#blog-post-demo',
22 data: {
23 posts: [
24 { id: 1, title: 'My journey with Vue' },
25 { id: 2, title: 'Blogging with Vue' },
26 { id: 3, title: 'Why Vue is so fun' }
27 ]
28 }
29 })
30</script>
31
32</html>
Prop 名称
由于 HTML 中的属性名是大小写不敏感的,所以浏览器会把所有大写字母解释为小写字母。
所以,在 DOM 中使用 camelCase (驼峰命名法) 定义的 prop时,需要将名称改为等价的 kebab-base(短横线分隔命名)形式。
示例:
1Vue.component('blog-post', {
2 // 在 JavaScript 中是 camelCase 的
3 props: ['postTitle'],
4 template: '<h3>{{ postTitle }}</h3>'
5})
1<!-- 在 HTML 中是 kebab-case 的 -->
2<blog-post post-title="hello!"></blog-post>
Prop 类型
在定义 prop 时,通常是以字符串数组形式把所有 prop 列举出来,如下面的例子:
1props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
也可以给每个 prop 指定数据类型,如下面的例子:
1props: {
2 title: String,
3 likes: Number,
4 isPublished: Boolean,
5 commentIds: Array,
6 author: Object,
7 callback: Function,
8 contactsPromise: Promise // or any other constructor
9}
Prop 传递
prop 传递是一种单向下行绑定:父级 prop 的更新会传递到子组件中,反过来则不允许。这是为了防止子组件意外变更父级组件的状态。
既可以给 prop 传递一个静态的值,也可以给 prop 传递一个动态的值。
示例:
1<!-- 传入一个字符串 -->
2<!-- 静态值 -->
3<blog-post title="My journey with Vue"></blog-post>
4<!-- 动态值(变量) -->
5<blog-post v-bind:title="post.title"></blog-post>
6
7<!-- 传入一个数字 -->
8<!-- 静态值 -->
9<blog-post v-bind:likes="42"></blog-post>
10<!-- 动态值(变量) -->
11<blog-post v-bind:likes="post.likes"></blog-post>
12
13<!-- 传入一个布尔值 -->
14<!-- 若不指定值,则默认为 `true`。-->
15<blog-post is-published></blog-post>
16<!-- 静态值-->
17<blog-post v-bind:is-published="false"></blog-post>
18<!-- 动态值(变量)-->
19<blog-post v-bind:is-published="post.isPublished"></blog-post>
20
21<!-- 传入一个数组 -->
22<!-- 静态值 -->
23<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>
24<!-- 动态值(变量) -->
25<blog-post v-bind:comment-ids="post.commentIds"></blog-post>
26
27<!-- 传入对象 -->
28<!-- 静态值 -->
29<blog-post v-bind:author="{name: 'Veronica',
30 company: 'Veridian Dynamics'}">
31</blog-post>
32<!-- 动态值 -->
33<blog-post v-bind:author="post.author"></blog-post>
34
35<!-- 传入一个对象的所有 property -->
36<!-- 可以使用不带参数的 v-bind (取代 v-bind:prop-name) -->
37<blog-post v-bind="post"></blog-post>
Prop 验证
可以为组件中的 prop 指定一些验证规则,当传递的值不符合要求时,Vue 会在浏览器控制台中发出警告信息。
示例:
1Vue.component('my-component', {
2 props: {
3 // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
4 propA: Number,
5 // 多个可能的类型
6 propB: [String, Number],
7 // 必填的字符串
8 propC: {
9 type: String,
10 required: true
11 },
12 // 带有默认值的数字
13 propD: {
14 type: Number,
15 default: 100
16 },
17 // 带有默认值的对象
18 propE: {
19 type: Object,
20 // 对象或数组默认值必须从一个工厂函数获取
21 default: function () {
22 return { message: 'hello' }
23 }
24 },
25 // 自定义验证函数
26 propF: {
27 validator: function (value) {
28 // 这个值必须匹配下列字符串中的一个
29 return ['success', 'warning', 'danger'].indexOf(value) !== -1
30 }
31 }
32 }
33})
这些验证发生在组件创建之前。所以在上面的示例中,实例的 property (如 data、computed 等) 在 default
或 validator
函数中是不可用的。
Prop 支持下面这些数据类型(构造函数):
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
另外,也支持自定义的构造函数,并且通过 instanceof
来进行验证。
示例:定义一个构造函数
1function Person (firstName, lastName) {
2 this.firstName = firstName
3 this.lastName = lastName
4}
然后可以在定义 Prop 时使用:
1Vue.component('blog-post', {
2 props: {
3 author: Person
4 }
5})
自定义事件
Vue 提供了一个内建的 $emit
方法,通过这个方法可以触发一个事件。
事件名称
不同于组件名称和 prop 名称,事件名不会自动转换大小写。Vue 推荐始终使用 kebab-case 的事件名。
监听子组件事件
与监听原生事件一样,使用 v-on
指令同样可以监听组件自定义的事件。
示例:
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Event demo</title>
6 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
7</head>
8
9<body>
10 <div id="blog-posts-events-demo">
11 <div :style="{ fontSize: postFontSize + 'em' }">
12 <!-- 通过 `v-on` 监听子组件实例的任意事件 -->
13 <blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post"
14 v-on:enlarge-text="postFontSize += 0.1"></blog-post>
15 </div>
16 </div>
17
18</body>
19<script>
20 Vue.component('blog-post', {
21 props: ['post'],
22 // 通过调用内建的 `$emit` 方法并传入事件名称来触发一个事件
23 template: `
24 <div class="blog-post">
25 <h3>{{ post.title }}</h3>
26 <button v-on:click="$emit('enlarge-text')">
27 Enlarge text
28 </button>
29 <div v-html="post.content"></div>
30 </div>
31 `
32 })
33 new Vue({
34 el: '#blog-posts-events-demo',
35 data: {
36 posts: [
37 { id: 1, title: 'My journey with Vue', content: '...content...' },
38 { id: 2, title: 'Blogging with Vue', content: '...content...' },
39 { id: 3, title: 'Why Vue is so fun', content: '...content...' }
40 ],
41 postFontSize: 1
42 }
43 })
44</script>
45
46</html>
另外,还可以使用事件抛出一个值
1<button v-on:click="$emit('enlarge-text', 0.1)">
2 Enlarge text
3</button>
然后当在父级组件监听这个事件的时候,我们可以通过 $event
访问到被抛出的这个值
1<blog-post
2 ...
3 v-on:enlarge-text="postFontSize += $event"
4></blog-post>
如果这个事件处理函数是一个方法:
1<blog-post
2 ...
3 v-on:enlarge-text="onEnlargeText"
4></blog-post>
那么这个值将会作为第一个参数传入这个方法:
1methods: {
2 onEnlargeText: function (enlargeAmount) {
3 this.postFontSize += enlargeAmount
4 }
5}
在组件上使用 v-model
对于表单中的原生事件,我们可以使用 v-model
指令实现数据的双向绑定。并且 v-model
指令是 Vue 给我们提供的一个语法糖,比如:
1<input v-model="searchText">
其实等价于:
1<input v-bind:value="searchText" v-on:input="searchText = $event.target.value">
同样,我们也可以在自定义的 input 组件上使用 v-model
指令。
前提是,这个组件内的 <input>
必须:
- 将其
value
属性绑定到一个名叫value
的 prop 上 - 当 input 事件被触发时,将新的值通过自定义 input 事件抛出
示例:自定义 input 组件
1Vue.component('custom-input', {
2 props: ['value'],
3 template: `
4 <input
5 v-bind:value="value"
6 v-on:input="$emit('input', $event.target.value)"
7 >
8 `
9})
这样就可以在组件中使用 v-model
指令了:
1<custom-input v-model="searchText"></custom-input>
它实际上等价于:
1<custom-input v-bind:value="searchText" v-on:input="searchText = $event"></custom-input>
默认情况下,一个组件上的 v-model
会把 value
用作 prop
,把 input
用作 event
,上面的示例其实可以简写为:
1Vue.component('custom-input', {
2 template: `
3 <input
4 v-on:input="$emit('input', $event.target.value)"
5 >
6 `
7})
model 选项
对于单选框和复选框,其事件和需要绑定的值与普通的输入框不一样。在定义组件时需要使用 model
选项指定 prop
和 event
。
示例:
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Event demo</title>
6 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
7</head>
8
9<body>
10 <div id="events-model-demo">
11 <base-checkbox v-model="isChecked"></base-checkbox>
12 <p>Is checked: {{ isChecked }}</p>
13 </div>
14
15</body>
16<script>
17 Vue.component('base-checkbox', {
18 model: {
19 prop: 'checked',
20 event: 'change'
21 },
22 props: {
23 checked: Boolean
24 },
25 template: `
26 <input
27 type="checkbox"
28 v-bind:checked="checked"
29 v-on:change="$emit('change', $event.target.checked)"
30 >
31 `
32 })
33 new Vue({
34 el: '#events-model-demo',
35 data: {
36 isChecked: false
37 }
38 })
39</script>
40
41</html>
插槽
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。
插槽用来向一个组件传递内容。
示例:
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Slots demo</title>
6 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
7</head>
8
9<body>
10 <div id="slots-demo" class="demo">
11 <alert-box>
12 Something bad happened.
13 </alert-box>
14 </div>
15
16</body>
17<script>
18 Vue.component('alert-box', {
19 template: '\
20 <div class="demo-alert-box">\
21 <strong>Error!</strong>\
22 <slot></slot>\
23 </div>\
24 '
25 })
26 new Vue({ el: '#slots-demo' })
27</script>
28
29</html>
在上面的示例中,渲染时 Something bad happened.
将会替换组件模板中的 <slot></slot>
。
后备内容
在定义模板时,可以在插槽内设置一个后备内容(也就是默认内容)。
示例:
在 <submit-button>
组件中定义模板
1<button type="submit">
2 <slot>Submit</slot>
3</button>
如果在父级组件中使用 <submit-button>
组件,但不提供插槽内容:
1<submit-button></submit-button>
该组件将会渲染为:
1<button type="submit">
2 Submit
3</button>
如果在父级组件中使用 <submit-button>
组件,提供了插槽内容:
1<submit-button>
2 Save
3</submit-button>
默认内容将被覆盖,该组件将会渲染为:
1<button type="submit">
2 Save
3</button>
具名插槽
在组件中的 template
中定义插槽时,可以给插槽指定名称,以定义多个插槽。若不指定名称,则该插槽会有一个默认的名称:"default"。
然后在父组件中使用时,通过在 <template>
元素上使用 v-slot
指令来指定插槽名称。Vue 会根据指定的插槽名称将传入的内容将对应的插槽替换掉。
任何没有包含在带有 v-slot
的 <template>
中的内容都会被视为默认插槽的内容。
示例:
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Slots demo</title>
6 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
7</head>
8
9<body>
10 <div id="slots-demo" class="demo">
11 <base-layout>
12 <template v-slot:header>
13 <h1>Here might be a page title</h1>
14 </template>
15
16 <!-- 该 template 标签可以省略 -->
17 <template v-slot:default>
18 <p>A paragraph for the main content.</p>
19 <p>And another one.</p>
20 </template>
21
22 <template v-slot:footer>
23 <p>Here's some contact info</p>
24 </template>
25 </base-layout>
26 </div>
27
28</body>
29<script>
30 Vue.component('base-layout', {
31 template: '\
32 <div class="container"> \
33 <header> \
34 <slot name="header"></slot> \
35 </header> \
36 <main> \
37 <slot></slot> \
38 </main> \
39 <footer> \
40 <slot name="footer"></slot> \
41 </footer> \
42 </div> \
43 '
44 })
45 new Vue({ el: '#slots-demo' })
46</script>
47
48</html>
具名插槽的缩写(2.6.0 新增)
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。例如 v-slot:header
可以被重写为 #header
:
1<base-layout>
2 <template #header>
3 <h1>Here might be a page title</h1>
4 </template>
5
6 <p>A paragraph for the main content.</p>
7 <p>And another one.</p>
8
9 <template #footer>
10 <p>Here's some contact info</p>
11 </template>
12</base-layout>
并且,#
后的插槽名称不能省略,下面的写法是无效的:
1<!-- 这样会触发一个警告 -->
2<current-user #="{ user }">
3 {{ user.firstName }}
4</current-user>
作用域插槽
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
在使用普通的具名插槽时,我们在父级组件中提供的内容是不能访问子组件作用域中的内容的。举个例子:
下面是 <current-user>
组件中定义的模板,该模板中设置了一个插槽,并且指定了默认值。
1<!-- user 是 `current-user` 组件中的数据 -->
2<span>
3 <slot>{{ user.lastName }}</slot>
4</span>
当我们在其他组件中使用 <current-user>
组件,并且想把插槽内容改为:user.firstName
,使用下面的方式是行不通的:
1<current-user>
2 {{ user.firstName }}
3</current-user>
因为,{{ user.firstName }}
是在父级组件提供给子组件的内容,无法访问子组件中的数据。
为了让父级组件提供的插槽内容可以访问子组件的数据,Vue 提供了作用域插槽。
具体的做法是:
- 在设置插槽时,使用
v-bind
指令将数据绑定到<slot>
元素的属性上。该属性被称为 插槽prop。 - 然后在父级组件中使用
v-slot:slot_name
来获取子组件绑定的内容。
示例:
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Slots demo</title>
6 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
7</head>
8
9<body>
10 <div id="slots-demo">
11 <!-- 此处提供插槽内容,并通过`v-slot`访问了子组件的数据,渲染后的结果是:zhang -->
12 <current-user>
13 <template v-slot:default="slotProps">
14 {{ slotProps.user.firstName }}
15 </template>
16 </current-user>
17
18 <br>
19 <!-- 此处未提供插槽内容,渲染后的结果是:san -->
20 <current-user></current-user>
21 </div>
22
23</body>
24<script>
25 Vue.component('current-user', {
26 data: function () {
27 return {
28 user: { firstName: 'san', lastName: 'zhang' }
29 };
30 },
31 template: '\
32 <span> \
33 <slot v-bind:user="user"> \
34 {{ user.lastName }} \
35 </slot> \
36 </span> \
37 '
38 })
39 new Vue({ el: '#slots-demo' })
40</script>
41
42</html>
动态插槽名(2.6.0 新增)
动态指令参数也可以用在 v-slot
上,来定义动态的插槽名:
1<base-layout>
2
3 <template v-slot:[dynamicSlotName]>
4 ...
5 </template>
6
7</base-layout>
动态组件
通过 Vue 的 <component>
元素加一个特殊的 is
可以实现组件动态切换的效果。
示例:
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Dynamic-component-demo</title>
6 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
7</head>
8
9<body>
10 <div id="dynamic-component-demo" class="demo">
11 <button v-for="tab in tabs" v-bind:key="tab" class="dynamic-component-demo-tab-button"
12 v-bind:class="{ 'dynamic-component-demo-tab-button-active': tab === currentTab }"
13 v-on:click="currentTab = tab">
14 {{ tab }}
15 </button>
16 <component v-bind:is="currentTabComponent" class="dynamic-component-demo-tab"></component>
17 </div>
18
19</body>
20<script>
21 Vue.component('tab-home', { template: '<div>Home component</div>' })
22 Vue.component('tab-posts', { template: '<div>Posts component</div>' })
23 Vue.component('tab-archive', { template: '<div>Archive component</div>' })
24 new Vue({
25 el: '#dynamic-component-demo',
26 data: {
27 currentTab: 'Home',
28 tabs: ['Home', 'Posts', 'Archive']
29 },
30 computed: {
31 currentTabComponent: function () {
32 return 'tab-' + this.currentTab.toLowerCase()
33 }
34 }
35 })
36</script>
37<style>
38 .dynamic-component-demo-tab-button {
39 padding: 6px 10px;
40 border-top-left-radius: 3px;
41 border-top-right-radius: 3px;
42 border: 1px solid #ccc;
43 cursor: pointer;
44 background: #f0f0f0;
45 margin-bottom: -1px;
46 margin-right: -1px;
47 }
48
49 .dynamic-component-demo-tab-button:hover {
50 background: #e0e0e0;
51 }
52
53 .dynamic-component-demo-tab-button-active {
54 background: #e0e0e0;
55 }
56
57 .dynamic-component-demo-tab {
58 border: 1px solid #ccc;
59 padding: 10px;
60 }
61</style>
62
63</html>
当在这些组件之间切换的时候,如果会想保持这些组件的状态,以避免反复重渲染导致的性能问题。我们可以用一个 <keep-alive>
标签将其动态组件包裹起来。
1<keep-alive>
2 <component v-bind:is="currentTabComponent"></component>
3</keep-alive>