Vue 组件

  |   0 评论   |   0 浏览

组件是一个可复用的 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 来向子组件传递数据,传递的流程大概如下:
image20210511151209635.png

简单的说,就是父组件实例通过父组件的模板(包含子组件元素)将值传给子组件实例,子组件实例再将值传给他的模板(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 等) 在 defaultvalidator 函数中是不可用的。

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 选项指定 propevent

示例

 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-onv-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>