Vue 2 组件

Vue 中的组件是页面中的一部分,通过层层拼装和复用,最终形成了一个完整的页面。

使用组件的一般场景:

  • 代码复用
  • 功能分类管理(每个组件实现一个功能)

组件必须先注册以便 Vue 应用能够识别,有两种组件的注册类型:

  • 全局注册
  • 局部注册

然后就可以使用组件,它类似标签一样,通过尖括号包裹组件名称 <component-name> 来使用组件。

Tip

由于 HTML 不区分大小写,如果在组件名定义时 name 使用驼峰式命名法,在 HTML 中使用组件时需要改为采用连字号 -,因此建议组件名称采用全小写,对于多个单词组成的名字,使用连字符号 - 分隔

Tip

组件的 data 选项必须是函数,从中 return 对象

js
// 定义全局组件
Vue.component('myComponent', {
    data() {
        return {
            name: 'Ben'
        }
    }
})
html
<!-- 使用组件 -->
<body>
  <div id="app">
    <my-component>
  </div>
</body>

全局组件

使用 Vue 原型 prototype 上的方法 Vue.component() 注册一个全局组件,第一个参数 name 是组件名称,第二个参数是一个对象包含组件相关的选项

vue
Vue.component('name', {
  options,
  template: `...`
})
Warning

==需要在 new Vue({}) 之前进行全局组件的注册==,之后它可以在任何地方(根组件或其他子组件的模板中)使用。

Tip

如果有大量基础组件,同时使用 webpack 工具,可以在入口文件进行配置,使用 require.context 实现自动引入并进行全局注册这些组件

局部组件

通过一个普通的 JavaScript 对象来定义组件(提供组件的 options),然后在 Vue 实例的 components 选项中进行注册,之后它只能在该父级组件内使用

js
const ComponentA = { /* ... */ }
const ComponentB = { /* ... */ }
const ComponentC = { /* ... */ }
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

全局组件虽然可以在整个项目的所有其他组件(包括自己)中都可用,但是这可能造成构建项目时体积增大,用户下载 JavaScript 的无谓增加,因此不能滥用全局组件,视情况而定,如果组件只在特定父级中使用,应该将其注册为局部组件。

Play Video

动态组件

基于标签 <component>is 属性值(组件名),动态在不同组件之间进行切换

Tip

有时候为了在切换时,保存动态组件的状态,例如组件中的输入框的值,可以用标签 <keep-alive></keep-alive> 包裹动态组件

Tip

属性 is 还可以用于解决 HTML 元素嵌套的规则限制,将它应用到原生的 HTML 标签上,它的值就是组件名,这样原生标签实际渲染出来的内容就是组件。因为对于 <ul><ol><table><select> 这些元素,其内部允许放置的直接子元素是有严格限制的,如果嵌入其他元素会被视为无效的内容,而提升到外部造成最终渲染问题。

html
<!-- 这样做是有必要的,因为组件 `<my-row>` 放在一个 `<table>` 内可能无效而被放置到外面 -->
<table>
  <tr is="my-row"></tr>
</table>

但以上限制只是在 HTML 中直接使用 Vue 模板时才会遇到,如果是在以下情况的模板中就没有这种限制:

  • 字符串,例如 template: '...'
  • 单文件组件 .vue
  • <script type="text/x-template">

组件间数据传递

  • 父组件将需要传递的数据绑定到子组件的属性上,子组件内部通过 props 接收。
  • 子组件可以通过抛出事件 $emit(eventName, value) 同时传递数据,父组件监听事件并接收数据

props

props 用于接收父组件传递的数据,需要在子组件的选项 props 中预先设置会有哪些 prop,可以以字符串数组形式列出,也可以以对象形式列出(可指定 prop 接收的数据类型、默认值、是否必须、验证条件等)。

需要注意以下几点:

  • ==如果在父组件中传入的数据是非字符串的,需要通过绑定 v-bind:propName 的方式来传递(即使传递纯数字这一类的静态值)==;否则直接传递数据都会转换为字符串
  • 如果 prop 是 Boolean 类型,但在使用组件中没有设置该 prop 时,则实例化后该组件的这个 prop 预设为 false;而使用组件时,设置了该 prop 但没有提供 true/false 值(no value 的情况),就会设置为 true。由于布尔类型不是字符串,正确传递方式是使用 v-bind 来传递非字符串的数据
    Play Video
  • prop 应该遵循==单向数据流==,即不应该在子组件中修改 prop 值(因为这些数据是在父层的),如果希望从子组件触发 prop 值的修改,可以通过子组件 $emit('eventName', value) 抛出事件,同时传递需要更新的数据,然后在父级组件中监听事件,在回调函数中做修改。:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 或者以 prop 值作为初始值或响应依赖:
    • 以 prop 传递来的数据作为一个初始值,在子组件接下来,拷贝到自己 data 的一个变量中
    • 基于 prop 定义一个计算属性,实现对原始的值的转换

    Play Video

如果希望将一个对象的所有属性「拆分」为多个 prop 分别传入,可以使用不带参数的 v-bind(即 v-bind="obj",而非 v-bind:propName="obj"),:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 所以可以将大量的 prop 包装进一个对象,一次绑定到组件上

Play Video

可以对 prop 进行限制,确保传递进来的值符合要求。有多个属性可以进行设置

js
// ...
props: {
  propA: {
    type: String,
    required: true,
    default: 'abc',
    validator: func()   // 自定义验证函数,返回 true/false
  }
}
Warning

prop 会在一个组件实例创建之前进行验证,所以该组件的 datacomputed 数据在 validator 验证函数中无法进行访问

emit

当组件需要修改父层控管的数据(这些数据通过 props 传递进来)时,基于单向数据流原则不能在组件内进行修改,而是需要通过 $emit('eventName', value) 向外抛出自定义的事件,通知父层进行数据修改,其中 value 一般是向外传递的需要变动的数据。

Tip

自定义事件名推荐使用 kebab-case 方式(:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 小写单词之间用连字符 - 连接,因为与组件和 prop 名称不同,Vue 不会对事件名进行任何大小写转换),以便于 HTML 正确识别

Play Video

==Vue 对于具有表单元素 <input> 的输入型组件,支持使用 v-model 实现类似「双向绑定」的效果==。但在该组件内部需要进行 input 事件监听,并手动 $emit('change', $event.target.value) 抛出该事件和用户输入的数据,让变量数据的修改仍在父层完成

  • 在外部(引用该组件时,在组件的标签上)通过指令 v-model 双向绑定需要同步的变量,它的数据作为 prop 传递到组件内(因此记得在组件内部需要在 props 选项里声明 value 作为 prop),作为子组件内部表单元素的 value 属性的值
  • 在组件内使用指令 v-bind 将表单元素表单元素的 value 属性绑定到以上声明的 prop 变量上,即 v-bind:value="value"(这样就可以将外部传进来的值作为表单的值,实现与外部数据的「同步」)
  • 在组件内使用指令 v-on 监听表单元素 change 输入事件,并通过抛出事件 $emit('input', $event.target.value) 向外传递相应的数据。这样在父组件中通过指令 v-model 绑定的变量就会基于抛出的数据进行改变 (即数据的修改的操作还是在父层完成)
js
// component
Vue.component('base-checkbox', {
  props: ['value'],
  template: `
    <input
      type="text"
      v-bind:value="value"
      v-on:change="$emit('input', $event.target.value)"
    >
  `
})
html
<base-input v-model="inputText"></base-input>

通过遵守这种设置,在使用输入型的组件时仅需简单地以 v-model 的方式,方便地实现数据从父组件传递给子组件,并将用户输入改动 $emit 给父组件进行修改,实现类似「双向绑定」的效果。

Tip

如果输入型组件需要绑定的 prop 名称不是 value,且抛出的事件不是 input,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 可以在组件中选项 model 进行配置,例如表单类型为 checkbox 时,需要绑定的是属性是 checked,抛出的事件是 change

js
Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
html
<base-checkbox v-model="lovingVue"></base-checkbox>

这里的 lovingVue 的值将会传入子组件,作为 prop checked 的值;而当 <base-checkbox> 抛出 change 事件并附带一个新的值时,这个所绑定的变量 lovingVue 将会被更新。

Play Video

sync

Vue 还为一般的组件提供 .sync 修饰符,和一个以特定前缀 update 命名的事件,让 prop 变量实现类似于「双向绑定」的效果,更方便地从子组件中「修改」父层传入的数据。

在使用组件设置 prop 时,==如果在绑定变量时 v-bind:propName.sync="variable" 添加 .sync 修饰符,就可以省略在父层设置监听事件和回调函数这一步==。

Vue 会自动监听子组件抛出的以特殊形式命名的事件 this.$emit('update:propName', newValue)事件以特定模式 update:propName 命名),然后在父层对 prop 绑定的变量进行更新。

Play Video

Tip

而且支持以 v-bind.sync="obj" 的形式同时设置多个具有「双向绑定」效果的 prop,其中对象 obj 的每一个 property 都作为一个独立的 prop 传进去,然后可以在组件内部使用 this.$emit('update:propName', newValue) 抛出事件来更新数据。

非 prop 属性

非 prop 的 attribute 是指未在组件的 props 选项中显式声明的,但在引用组件时传递给子组件的 attribute,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 这些 attribute 会默认被添加到这个组件的根元素上(即在组件的模板中作为容器的第一层的 <tag> 上。

Play Video

如果组件的根节点 <tag> 上已有预设了相应的 attribute,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 该属性就会被新传入的值覆盖;如果这个 attribute 是 classstyle,会将它们与组件根元素上的 classstyle 合并

Play Video

Tip

如果希望 attribute 添加到组件的特定元素上,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 可以设定组件的选项 inheritAttrs: false,然后将特殊的变量 $attrs(这是一个包含所有非 prop 的 attribute 的对象)绑定 v-bind="$attrs" 到指定的节点上,classstyle 不能指定到组件的非根元素上。也可指定某个 attribute 绑定 :attributeName="$attrs.propertyName"

Play Video

监听事件

有时候希望在使用组件时才添加事件监听,这样就会将事件监听添加到组件的根元素上,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 但此时监听的事件都被认为是自定义事件(即监听从子组件手动 $emit 出来的事件),如果希望监听 JS 预设的原生事件,如 clickfocus 等,需要在其后添加 .native 修饰符。

Play Video

如果希望在引用组件时添加事件监听,但又不是添加到组件的根元素上,而是添加到组件内的特定元素上,可以在组件中配置选项 inheritAttrs: false,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 并在模板非根元素的标签上使用 v-on="$listener",这样在使用组件时才设置的事件监听器,都指向该特定节点上。

Vue 提供了一个 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

js
{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

Play Video

官方的例子实现了将外部添加的事件监听器(通过 $listeners 获取),和内部为了配合 v-model 设置的 input 事件监听器合并,再一起绑定到组件的 <input> 元素上。

js
Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
      // 从父级添加所有的监听器
        this.$listeners,
        // 自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

插槽

插槽 slot 是 Vue 实现的一套内容分发的 API,一般用于组件作为布局 layout 使用。

在子组件的模板中使用标签 <slot> 作为「占位符」预留位置,在使用组件时,内嵌在组件 <component-name>content</component-name> 中的内容 content(插入的内容可以是 HTML,也包含模板代码或其他组件) 会替代 <slot> 标签,渲染出来。

Tip

可以在组件模板的插槽中设置默认内容,但是只要在使用组件时,有提供插入内容就会替换默认内容。如果组件定义模板时没有包含一个 <slot> 元素,则使用该组件时,即使组件起始标签和结束标签之间有内容,都会被抛弃。

具名插槽

可以在组件定义的模板中设置多个插槽,并为它们设置属性 name,它们称为具名插槽 <slot name="slotName"></slot>

在使用组件时,使用 <template> 元素作为容器(该标签本身不会被渲染到页面上),并以==带参数的指令 v-slot:slotName 的形式 <template v-slot:slotName>content</template> 将提供的内容分发到相应名称的插槽==。

Play Video

Tip

和其他指令一样,v-slot:slotName 指令有缩写形式 #slotName

当模板中存在多个具名插槽 <slot> 时,可以有一个不具名插槽,它作为默认插槽(实际上带有隐含的名字 v-slot:default。在使用组件时,任何没有被包裹在带有指令 v-slot:slotName<template> 中的内容都会被视为默认插槽的内容。

作用域插槽

由于使用组件时,是在父层指定插入的内容,所以这些内容现在父级作用域进行编译,它们只能访问父层的数据

Play Video

如果希望可以访问组件中才有的数据,需要使用作用域插槽 <slot v-bind:variable="value">,即在定义组件时,在插槽中预先将允许访问的数据绑定到变量上,这些绑定到 <slot> 的属性称为插槽 props

然后在使用组件时,可以==为指令 v-slot 设置 <template v-slot:slotName="objName">,该值是一个对象,它包含所有在组件内绑定的插槽 props==,然后就可以通过 objName.variable 的方式来读取子作用域才有的数据。除了使用 objName「接收」内层抛出的所有 props,还可以使用 ES6 解构的方式直接 <template v-slot:slotName="{ variable }"> 获取单个 variable ,便于后面调用。

html
<!-- 子组件 current-user 的模板 -->
<span>
  <slot v-bind:user="user">
    {{ user.firstName}} {{ user.lastName }}
  </slot>
</span>
html
<!-- 使用组件 current-user -->
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>
<!-- 解构的方式接收特定的 prop -->
<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

Play Video

Tip

如果组件只有默认插槽时,组件的标签可以被当作插槽的模板来使用,即可以省略 <template> 标签,直接在组件标签上设置 v-slot="objName" 来接收内层抛出的 props

html
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

混入

混入 mixin 是指将组件选项中可复用的部分「抽取」出来成为一个对象(可以包含 computedmethodwatch 等),以供其他组件复用。:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 然后在组件(包括根组件)的选项 mixins 中使用该对象即可复用。

Play Video

在组件或 Vue 实例中添加选项 mixins: [mixinName] 引入预设的选项,它们将被「混合」进入该组件本身的选项。

js
// 定义一个混入对象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
  mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
Tip

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行 「合并」:

Play Video

  • 同名生命周期钩子函数,如 createdmounted 等,将合并为一个数组都将被调用,而且混入对象的钩子函数优先级更高,将在组件自身钩子函数之前调用。
  • 当选项的值为对象,如 methodscomponents,将被合并为同一个对象。如果合并时两个对象键名冲突,取组件对象的键值对
  • 如果 希望(针对相同选项)自定义混入时合并策略,可以向 Vue.config.optionMergeStrategies 添加一个函数
    js
    Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
      // 返回合并后的值
    }

    Play Video

可以通过 Vue 原型 prototype 上的方法 Vue.mixin({options}) 设置 global mixin 全局混入,这样在所有组件中都会引入该 mixin(不需要手动以 mixins: [mixinName] 的方式添加)⚠️ 由于全局混入会影响每个单独创建的 Vue 实例(包括第三方组件),可能会导致一些冲突。


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes