一、组件注册

1. 组件名

在注册一个组件的时候,我们始终需要给它一个名字

  • 给予组件的名字可能依赖于你打算拿它来做什么
  • 当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突
    1. Vue.component('my-component-name', { /* ... */ })

定义组件名的方式有两种

  • 使用 kebab-case
  • 使用 PascalCase

直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的

  1. Vue.component('my-component-name', { /* ... */ })
  2. Vue.component('MyComponentName', { /* ... */ })

2. 全局注册

Vue.component 来创建的组件是全局注册的,可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中

  1. Vue.component('component-a', { /* ... */ })
  2. Vue.component('component-b', { /* ... */ })
  3. Vue.component('component-c', { /* ... */ })
  4. new Vue({ el: '#app' })
  1. <div id="app">
  2. <component-a></component-a>
  3. <component-b></component-b>
  4. <component-c></component-c>
  5. </div>

3. 局部注册

通过一个普通的 JavaScript 对象来定义组件,然后在 components 选项中定义你想要使用的组件

  1. var ComponentA = { /* ... */ }
  2. var ComponentB = { /* ... */ }
  3. var ComponentC = { /* ... */ }
  1. new Vue({
  2. el: '#app',
  3. components: {
  4. 'component-a': ComponentA,
  5. 'component-b': ComponentB
  6. }
  7. })

通过 Babel 和 Webpack 使用 ES2015 模块,代码更像

  1. import ComponentA from './ComponentA.vue'
  2. export default {
  3. components: {
  4. ComponentA
  5. },
  6. // ...
  7. }

4. 模块系统

在模块系统中局部注册

  • 推荐创建一个 components 目录,并将每个组件放置在其各自的文件中
  • 然后你需要在局部注册之前导入每个你想使用的组件 ```javascript import ComponentA from ‘./ComponentA’ import ComponentC from ‘./ComponentC’

export default { components: { ComponentA, ComponentC }, // … }

  1. 基础组件的自动化全局注册
  2. - 可以使用 require.context 只全局注册这些非常通用的基础组件
  3. - 下面是一份可以让你在应用入口文件 (比如 `src/main.js`) 中全局导入基础组件的示例代码
  4. - 记住全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生
  5. ```javascript
  6. import Vue from 'vue'
  7. import upperFirst from 'lodash/upperFirst'
  8. import camelCase from 'lodash/camelCase'
  9. const requireComponent = require.context(
  10. // 其组件目录的相对路径
  11. './components',
  12. // 是否查询其子目录
  13. false,
  14. // 匹配基础组件文件名的正则表达式
  15. /Base[A-Z]\w+\.(vue|js)$/
  16. )
  17. requireComponent.keys().forEach(fileName => {
  18. // 获取组件配置
  19. const componentConfig = requireComponent(fileName)
  20. // 获取组件的 PascalCase 命名
  21. const componentName = upperFirst(
  22. camelCase(
  23. // 获取和目录深度无关的文件名
  24. fileName
  25. .split('/')
  26. .pop()
  27. .replace(/\.\w+$/, '')
  28. )
  29. )
  30. // 全局注册组件
  31. Vue.component(
  32. componentName,
  33. // 如果这个组件选项是通过 `export default` 导出的,
  34. // 那么就会优先使用 `.default`,
  35. // 否则回退到使用模块的根。
  36. componentConfig.default || componentConfig
  37. )
  38. })

二、Prop

1. Prop 的大小写

HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名

  • 如果你使用字符串模板,那么这个限制就不存在了
    1. Vue.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>

2. Prop 类型

可以字符串数组形式列出的 prop
还可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型

  1. props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
  2. props: {
  3. title: String,
  4. likes: Number,
  5. isPublished: Boolean,
  6. commentIds: Array,
  7. author: Object,
  8. callback: Function,
  9. contactsPromise: Promise // or any other constructor
  10. }

3. 传递静态或动态 Prop

可以给 prop 传递静态值
也可以通过 v-bind 动态赋值

  1. <blog-post title="My journey with Vue"></blog-post>
  2. <!-- 动态赋予一个变量的值 -->
  3. <blog-post v-bind:title="post.title"></blog-post>
  4. <!-- 动态赋予一个复杂表达式的值 -->
  5. <blog-post
  6. v-bind:title="post.title + ' by ' + post.author.name"
  7. ></blog-post>

传入一个数字

  1. <!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
  2. <!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
  3. <blog-post v-bind:likes="42"></blog-post>
  4. <!-- 用一个变量进行动态赋值。-->
  5. <blog-post v-bind:likes="post.likes"></blog-post>

传入一个布尔值

  1. <!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
  2. <blog-post is-published></blog-post>
  3. <!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
  4. <!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
  5. <blog-post v-bind:is-published="false"></blog-post>
  6. <!-- 用一个变量进行动态赋值。-->
  7. <blog-post v-bind:is-published="post.isPublished"></blog-post>

传入一个数组

  1. <!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
  2. <!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
  3. <blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>
  4. <!-- 用一个变量进行动态赋值。-->
  5. <blog-post v-bind:comment-ids="post.commentIds"></blog-post>

传入一个对象

  1. <!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
  2. <!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
  3. <blog-post
  4. v-bind:author="{
  5. name: 'Veronica',
  6. company: 'Veridian Dynamics'
  7. }"
  8. ></blog-post>
  9. <!-- 用一个变量进行动态赋值。-->
  10. <blog-post v-bind:author="post.author"></blog-post>

4. 传入一个对象的所有 property

如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name)

  • 例如对于一个给定的对象 post
    1. post: {
    2. id: 1,
    3. title: 'My Journey with Vue'
    4. }
    1. <blog-post v-bind="post"></blog-post>
    2. <!-- 等价于 -->
    3. <blog-post
    4. v-bind:id="post.id"
    5. v-bind:title="post.title"
    6. ></blog-post>

5. 单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:

  • 父级 prop 的更新会向下流动到子组件中
  • 但是反过来则不行

额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。

  • 这意味着你不应该在一个子组件内部改变 prop
  • 如果你这样做了,Vue 会在浏览器的控制台中发出警告

注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。

这里有两种常见的视图变更一个 prop 的情形

  1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。
    • 在这种情况下,最好定义一个本地的 data property 并将这个 prop 用作其初始值
  2. 这个 prop 以一种原始的值传入且需要进行转换
    • 在这种情况下,最好使用这个 prop 的值来定义一个计算属性 ```javascript props: [‘initialCounter’], data: function () { return { counter: this.initialCounter } }

props: [‘size’], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }

  1. <a name="te3gn"></a>
  2. ### 6. Prop 验证
  3. 我们可以为组件的 prop 指定验证要求,在 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组
  4. - 当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告
  5. - 注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的
  6. ```javascript
  7. Vue.component('my-component', {
  8. props: {
  9. // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
  10. propA: Number,
  11. // 多个可能的类型
  12. propB: [String, Number],
  13. // 必填的字符串
  14. propC: {
  15. type: String,
  16. required: true
  17. },
  18. // 带有默认值的数字
  19. propD: {
  20. type: Number,
  21. default: 100
  22. },
  23. // 带有默认值的对象
  24. propE: {
  25. type: Object,
  26. // 对象或数组默认值必须从一个工厂函数获取
  27. default: function () {
  28. return { message: 'hello' }
  29. }
  30. },
  31. // 自定义验证函数
  32. propF: {
  33. validator: function (value) {
  34. // 这个值必须匹配下列字符串中的一个
  35. return ['success', 'warning', 'danger'].indexOf(value) !== -1
  36. }
  37. }
  38. }
  39. })

类型检查,type 可以是下列原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

额外的,type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。

  1. // 这是一个构造函数
  2. function Person (firstName, lastName) {
  3. this.firstName = firstName
  4. this.lastName = lastName
  5. }
  6. // 来验证 author prop 的值是否是通过 new Person 创建的
  7. Vue.component('blog-post', {
  8. props: {
  9. author: Person
  10. }
  11. })

7. 非 Prop 的 Attribute

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute

  • 因为显式定义的 prop 适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景
  • 这也是为什么组件可以接受任意的 attribute,而这些 attribute 会被添加到这个组件的根元素上

示例:这个 data-date-picker=”activated” attribute 就会自动添加到 的根元素上

  1. <bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>

替换/合并已有的 Attribute

  • 对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值,class 和 style 会合并
  • 对于下面的示例中
    • 如果传入 type=”text” 就会替换掉 type=”date” 并把它破坏!
    • class 和 style attribute 会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark
      1. <!-- 想象一下 <bootstrap-date-input> 的模板是这样的 -->
      2. <input type="date" class="form-control">
      1. <!-- 像这样添加一个特别的类名 -->
      2. <bootstrap-date-input
      3. data-date-picker="activated"
      4. class="date-picker-theme-dark"
      5. ></bootstrap-date-input>

8. 禁用 Attribute 继承

如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false

  • 这尤其适合配合实例的 $attrs property 使用,该 property 包含了传递给一个组件的 attribute 名和 attribute 值
  • 有了 inheritAttrs: false 和 $attrs,你就可以手动决定这些 attribute 会被赋予哪个元素
    1. Vue.component('my-component', {
    2. inheritAttrs: false,
    3. // ...
    4. })
    1. Vue.component('base-input', {
    2. inheritAttrs: false,
    3. props: ['label', 'value'],
    4. template: `
    5. <label>
    6. {{ label }}
    7. <input
    8. v-bind="$attrs"
    9. v-bind:value="value"
    10. v-on:input="$emit('input', $event.target.value)"
    11. >
    12. </label>
    13. `
    14. })

三、自定义事件

1. 事件名

不同于组件和 prop,事件名不存在任何自动化的大小写转换
而是触发的事件名需要完全匹配监听这个事件所用的名称

v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的)
所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到

因此,我们推荐你始终使用 kebab-case 的事件名

2. v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突

  • 下面示例中,lovingVue 的值将会传入这个名为 checked 的 prop
  • 同时当 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新
  1. Vue.component('base-checkbox', {
  2. model: {
  3. prop: 'checked',
  4. event: 'change'
  5. },
  6. props: {
  7. checked: Boolean
  8. },
  9. template: `
  10. <input
  11. type="checkbox"
  12. v-bind:checked="checked"
  13. v-on:change="$emit('change', $event.target.checked)"
  14. >
  15. `
  16. })
  1. <base-checkbox v-model="lovingVue"></base-checkbox>

3. 将原生事件绑定到组件

你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on 的 .native 修饰符

  1. <base-input v-on:focus.native="onFocus"></base-input>

当尝试监听一个类似 的非常特定的元素时,上面方法不行了,因为根元素实际上是一个

  • 为了解决这个问题,Vue 提供了一个 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器
  • 有了这个 $listeners property,你就可以配合 v-on=”$listeners” 将所有的事件监听器指向这个组件的某个特定的子元素
  • 对于类似 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的
  • 这时, 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 元素一样使用了:所有跟它相同的 attribute 和监听器都可以工作,不必再使用 .native 监听器
    1. <label>
    2. {{ label }}
    3. <input
    4. v-bind="$attrs"
    5. v-bind:value="value"
    6. v-on:input="$emit('input', $event.target.value)"
    7. >
    8. </label>
    1. {
    2. focus: function (event) { /* ... */ }
    3. input: function (value) { /* ... */ },
    4. }
    1. Vue.component('base-input', {
    2. inheritAttrs: false,
    3. props: ['label', 'value'],
    4. computed: {
    5. inputListeners: function () {
    6. var vm = this
    7. // `Object.assign` 将所有的对象合并为一个新对象
    8. return Object.assign({},
    9. // 我们从父级添加所有的监听器
    10. this.$listeners,
    11. // 然后我们添加自定义监听器,
    12. // 或覆写一些监听器的行为
    13. {
    14. // 这里确保组件配合 `v-model` 的工作
    15. input: function (event) {
    16. vm.$emit('input', event.target.value)
    17. }
    18. }
    19. )
    20. }
    21. },
    22. template: `
    23. <label>
    24. {{ label }}
    25. <input
    26. v-bind="$attrs"
    27. v-bind:value="value"
    28. v-on="inputListeners"
    29. >
    30. </label>
    31. `
    32. })

4. .sync 修饰符

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。

  • 推荐使用以 update:myPropName 的模式触发事件。

    1. this.$emit('update:title', newTitle)
    1. <text-document
    2. v-bind:title="doc.title"
    3. v-on:update:title="doc.title = $event"
    4. ></text-document>

    为这种模式提供一个缩写,即 .sync 修饰符

    1. <text-document
    2. v-bind:title.sync="doc.title"
    3. ></text-document>

    当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用

  • 这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去

  • 然后各自添加用于更新的 v-on 监听器

将 v-bind.sync 用在一个字面量的对象上,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

  1. <text-document v-bind.sync="doc"></text-document>

四、插槽

1. 插槽内容

Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 元素作为承载分发内容的出口

  • 插槽内可以包含任何模板代码,包括 HTML
  • 甚至其它的组件 ```html Your Profile

Your Profile

Your Profile

  1. ```html
  2. <a
  3. v-bind:href="url"
  4. class="nav-link"
  5. >
  6. <slot></slot>
  7. </a>

2. 编译作用域

记住这条规则即可:

  • 父级模板里的所有内容都是在父级作用域中编译的
  • 子模板里的所有内容都是在子作用域中编译的

3. 后备内容

有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染

  • 将“Submit”作为后备内容,我们可以将它放在 标签内
    1. <button type="submit">
    2. <slot>Submit</slot>
    3. </button>

4. 具名插槽

它的使用场景是需要多个插槽,对于这种情况, 元素有一个特殊的 attribute:name。

  • 这个 attribute 可以用来定义额外的插槽
  • 一个不带 name 的 出口会带有隐含的名字“default”
  • 在向具名插槽提供内容的时候,我们可以在一个