VUE2中文文档:深入组件(上)

Posted FunkyEric的小九九

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了VUE2中文文档:深入组件(上)相关的知识,希望对你有一定的参考价值。

组件注册

组件名称

命名方案

可以通过两种可选方式,定义组件名称:

串联式命名(kebab-case)


Pascal 式命名(PascalCase)
使用串联式命名(kebab-case)定义一个组件,在引用其自定义元素时,你必须也使用串联式命名(kebab-case),例如 <my-component-name>


使用 Pascal 式命名(PascalCase)定义一个组件,在引用其自定义元素时,两种方式都可以使用。也就是说 <my-component-name> 和 <MyComponentName> 都是可以接受的。然而要注意,直接在 DOM 中(即,非字符串模板)引用自定义元素,串联式命名(kebab-case)是唯一有效的命名方式。

 

全局注册方式(global registration)

Vue.component(‘my-component-name‘, {
  // ... options ...
})

这些组件都是全局注册。也就是说,全局注册的组件,可以在之后(通过 new Vue)创建的 Vue 根实例的模板中引用。

这甚至可以应用于所有子组件,这意味着,这三个组件还可以在每个其他组件内部使用

 

局部注册方式(local registration)

如果你使用一个类似 webpack 的模块构建系统,全局注册所有组件,意味着就算你不引用某个组件,它仍然会打包到最终的构建 bundle 中。这会增加 javascript 的体积,让用户下载多余的代码。

在下面这些示例中,可以将你的组件定义为纯 JavaScript 对象:

var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }

然后,在 components 选项中,定义你需要用到的组件:

new Vue({
  el: ‘#app‘
  components: {
    ‘component-a‘: ComponentA,
    ‘component-b‘: ComponentB
  }
})

对于 components 对象的每个属性,对象的 key 是自定义元素的名称,而 value 包含着组件的选项对象。

注意,局部注册的组件在子组件中无法访问

如果使用 ES2015 模块(例如,通过 Babel 和 webpack 进行转译),则看起来可能类似这样:

import ComponentA from ‘./ComponentA.vue‘

export default {
  components: {
    ComponentA
  },
  // ...
}

在 ES2015+ 中,在一个对象放置类似 ComponentA 这样的变量名称,其实是 ComponentA: ComponentA 的简写形式,

模块系统

在模块系统中的局部注册方式

你可能正在使用一个模块系统(例如,通过 Babel 和 webpack 进行转译)。在这种场景中,我们推荐你创建一个 components 目录,每个组件中都定义在文件中。

然后,在局部注册这些组件之前,你需要预先导入每个需要用到的组件。例如,在假想的 ComponentB.js 或 ComponentB.vue 文件中:

import ComponentA from ‘./ComponentA‘
import ComponentC from ‘./ComponentC‘

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

自动化全局注册基本组件

多相对通用的组件。我们有时将这些组件归为 基础组件,并且往往在其他组件中频繁使用这类组件。

结果就是,许多组件可能会列出一个很长的基础组件清单,然后在 components 选项中进行逐个引用。

幸运的是,如果你正在使用 webpack(或者内置 webpack 的 Vue CLI 3+),你就可以只通过 require.context 来全局注册这些常用基础组件。在你的应用程序入口文件(例如 src/main.js)中,你可能会通过全局方式导入基础组件,下面是一些示例代码:

import Vue from ‘vue‘
import upperFirst from ‘lodash/upperFirst‘
import camelCase from ‘lodash/camelCase‘

const requireComponent = require.context(         //匹配的你想导入的目标组件们
  // components 文件夹的相对路径
  ‘./components‘,
  // 是否查找子文件夹
  false,
  // 用于匹配组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {      //取得它们的pascal命名
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 取得组件的 Pascal 式命名
  const componentName = upperFirst(
    camelCase(
      // 将文件名前面的 `‘./` 和扩展名剥离
      fileName.replace(/^\.\/(.*)\.\w+$/, ‘$1‘)
    )
  )

  // 以全局方式注册组件
  Vue.component(                      //全局注册
    componentName,
    // 如果组件是通过 `export default` 导出,
    // 则在 `.default` 中,查找组件选项,
    // 否则回退至模块根对象中,查找组件选项
    componentConfig.default || componentConfig
  )
})

 

props

prop 命名方案(驼峰式和串联式)

Vue.component(‘blog-post‘, {
  // 在 JavaScript 中使用驼峰式(camelCase)
  props: [‘postTitle‘],
  template: ‘<h3>{{ postTitle }}</h3>‘
})


<!-- 在 html 中使用串联式(kebab-case) -->
<blog-post post-title="hello!"></blog-post>

再次申明,如果是在使用字符串模板的场景,则没有这些限制。

静态 props 和动态 props

可以通过 v-bind 给 props 分配动态值,就像这样:

<blog-post v-bind:title="post.title"></blog-post>

我们传递字符串值,然而,实际上可以给一个 prop 传递任意类型的值。

传递一个 Number 类型值

传递一个 Boolean 类型值

传递一个 Array 类型值

传递一个 Object 类型值

<!-- object 是静态的,这就需要我们使用 v-bind, -->
<!-- 来告诉 Vue 它是以 JavaScript 表达式表现,而不是一个字符串 -->
<blog-post v-bind:comments="{ id: 1, title: ‘我的 Vue 旅程‘ }"></blog-post>

<!-- 将一个变量,动态地分配到属性值上 -->
<blog-post v-bind:post="post"></blog-post>

传递一个对象的所有属性

如果你想要向 props 传递一个对象所有属性,你可以使用不带参数的 v-bind(即 v-bind 来替换 v-bind:prop-name)。

单向数据流

所有 props 都在子组件和父组件之间形成一个单向往下流动的数据绑定:当父组件中的属性更新时,数据就会向下流动到子组件,但是反过来,子组件属性更新时,父组件并不会感知到子组件的数据变化。这种机制可以防止子组件意外地修改了父组件的状态,造成应用程序的数据流动变得难于理解。

此外,每次父组件更新时,子组件中所有的 props 都会更新为最新值。也就是说,你不应该试图在子组件内部修改 prop。如果你这么做,Vue 就会在控制台给出警告。

诱使我们修改 prop 的原因,通常有两种:

1.prop 用于传递初始值(initial value);之后子组件需要将 prop 转为一个局部数据属性。在这种情况中,最好定义一个局部的 data 属性,然后将 prop 的值,作为局部属性初始值。

props: [‘initialCounter‘],
data: function () {
  return {
    counter: this.initialCounter
  }
}

2.prop 用于传递一个需要转换的未加工值(raw value)。在这种情况中,最好预先定义一个 computed 属性,然后在其函数内部引用 prop 的值:

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

注意,JavaScript 中的对象和数组都是通过引用(reference)传递的,因此,如果 prop 是一个数组或对象,则在子组件内部改变对象或数组本身,仍然会影响到父组件状态。

prop 验证

你需要将 props 的值定义为一个带有验证接收条件的对象,而不是一个由字符串构成的数组。

Vue.component(‘my-component‘, {
  props: {
    // 基本类型(base type)的检查(`null` 表示接受所有类型)
    propA: Number,
    // 多种可能的类型
    propB: [String, Number],
    // 必须传递,且 String 类型
    propC: {
      type: String,
      required: true
    },
    // Number 类型,有一个默认值
    propD: {
      type: Number,
      default: 100
    },
    // Object 类型,有一个默认值
    propE: {
      type: Object,
      // Object/Array 类型,
      // 默认必须返回一个工厂函数
      default: function () {
        return { message: ‘hello‘ }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 值必须是这些字符串中的一个
        return [‘success‘, ‘warning‘, ‘danger‘].indexOf(value) !== -1
      }
    }
  }
})

当 prop 验证失败,(如果使用的是开发构建版本,)Vue 就会在控制台抛出警告。

注意,props 会在组件实例创建之前进行验证,因此在 default 或 validator这些验证函数中,还无法访问到实例上的属性(像 datacomputed 这些)。

 

类型检查

type 可以是以下原生构造函数之一:

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

除了以上这些,type 还可以是一个自定义构造函数,通过 instanceof 对 props 值进行类型推断。

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}
//你可以这样做类型推断:以验证 author prop 的值,是由 new Person 创建出来的。

Vue.component(‘blog-post‘, {
  props: {
    author: Person
  }
})

非 prop 特性(non-prop attributes)

例如,假想我们使用一个第三方 bootstrap-date-input 组件,其内部引用一个 BootStrap 插件,现在需要我们向组件内的 input 元素传入一个 data-date-picker 特性。我们可以在组件实例上添加这个特性:

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

然后,data-date-picker="activated" 特性就会被自动添加到 bootstrap-date-input 组件的根元素上。

替换/合并现有的特性(replacing/merging with existing attributes)

对于大多数特性,传给组件的值将会替换掉组件自身设置的值。例如,向组件传入 type="text",将会替换掉组件自身设置的 type=”date”,这就很可能破坏组件的一些预设功能!幸运的是,class 和 style 特性会略微智能,这两个值会被合并而非替换,而最终的值是:form-control date-picker-theme-dark

禁用特性继承(disabling attribute inheritance)  

如果你不希望组件根元素从父实例中继承特性(attribute),你可以在组件选项中设置 inheritAttrs: false 来禁用特性继承。

这对于通过子组件 $attrs 实例属性,合并父实例的特性来说尤其有用,其中 $attrs 对象包含父实例传入到一个组件的特性名称和特性值,类似这样:

{
  class: ‘username-input‘,
  placeholder: ‘Enter your username‘
}

通过 inheritAttrs: false 和 $attrs,你可以手动决定将特性,传送到具体的某个元素,对于 基本组件 来说,这是符合需求的功能:

Vue.component(‘base-input‘, {
  inheritAttrs: false,
  props: [‘label‘, ‘value‘],
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit(‘input‘, $event.target.value)"
      >
    </label>
  `
})


//这种方式允许你像使用原始 HTML 元素那样去使用基本组件,而不必关心组件根元素是哪个元素:

<base-input
  v-model="username"
  class="username-input"
  placeholder="Enter your username"
></base-input>

自定义事件

 

事件名称(event names)

与 components 和 props 不同,事件名称并不提供命名自动转换,事件名称永远不会用作 JavaScript 变量或属性名称,所以没有理由去使用驼峰式命名(camelCase)或帕斯卡命名(PascalCase)。DOM 模板中的 v-on 事件监听器会自动转换为小写(这是因为 HTML 属性名称不区分大小写)。

由于这些原因,我们建议你总是使用串联式命名(kebab-cased)来命名事件名称。

定制组件 v-model

在一个组件中,v-model 默认使用 value 作为 prop,以及默认使用 input 作为监听事件,然而,对于某些类型的 input 元素(例如 checkbox 和 radio),由于这些类型的 input 元素本身具有 不同用法,可能会占用 value 特性。在这种情况下,使用组件的 model 选项可以避免冲突:

Vue.component(‘base-checkbox‘, {
  model: {
    prop: ‘checked‘,                   //将prop更改为checked
    event: ‘change‘                    //将监听事件改为change
  },
  props: {
    checked: Boolean     //??
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"         //绑定checked作为prop
      v-on:change="$emit(‘change‘, $event.target.checked)"    //将checked传给监听事件change
    >
  `
})
<base-checkbox v-model="lovingVue"></base-checkbox>

lovingVue 的值就会传递给 checked prop。当 <base-checkbox> 内部触发一个 change事件,并且传递一个新值,lovingVue 属性就会进行更新。

为组件绑定本地事件(binding native events to components)

有时候,你可能希望某个组件的根元素能够直接监听到组件所处位置的本地事件。在这种场景中,你可以在 v-on 上使用 .native 修饰符:

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

有时绑定本地事件会很有用,但是注意,如果你试图监听一个非常特殊的元素(例如 <input> 元素),则不是正确用法。。举例说明,上面的 <base-input> 组件或许会进行重构,因此其根元素可能实际上是一个 <label> 元素:

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit(‘input‘, $event.target.value)"
  >
</label>

为了解决这个问题,Vue 提供了一个 $listeners 属性,它是包含组件中所有监听器的对象

你可以将所有父组件中由 v-on="$listeners" 绑定的监听器,通过 $listeners 属性转发到子组件的特定元素。

对于像 <input> 这样的元素,你还需要实现 v-model 机制,通常会创建返回一个新的 listeners 对象的 computed 属性,类似如下 inputListeners

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>
  `
})

现在,<base-input> 组件是一个毫无疑惑的容器组件(fully transparent wrapper)了,也就是说,可以像使用一个普通的 <input> 元素一样去使用它:所有的特性和监听器,都能够如同普通 input 元素一样正常运行。

.sync 修饰符

通过子组件触发 update:my-prop-name 事件的方式,来更新父组件的状态。例如,在一个接收 titleprop 的假想组件中,我们想要分配一个子组件的新值给父组件的意图,可以通过通信机制来实现:

this.$emit(‘update:title‘, newTitle)


//父组件可以监听到这个事件,并且(如果需要的话)更新一个本地数据属性。例如:

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

为了简便,我们使用 .sync 修饰符,提供了这个模式的简写:

<text-document v-bind:title.sync="doc.title"></text-document>
//.sync 修饰符还可以用于 v-bind,使用一个对象一次性设置多个 prop:

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


//这会传递 doc 对象中的每个属性(例如 title),作为单独的 prop,然后为每个属性添加相应的 v-on:update 监听器。

对一个字面量对象使用 v-bind.sync(例如 v-bind.sync=”{ title: doc.title }”),则不会正常运行,因为在解析像这种复杂表达式时,需要考虑太多的边界情况。

 

slot

插槽内容(slot content)

//内容分发机制,可以帮助你以如下方式构成组件:

<navigation-link url="/profile">
  Your Profile
</navigation-link>
//然后,在 <navigation-link> 模板中,可能是: <a v-bind:href="url" class="nav-link" > <slot></slot> </a>
//在组件渲染时,<slot> 元素就会被替换为 “Your Profile”。在 slot 位置,可以包含任何模板代码,也包括 HTML<navigation-link url="/profile"> <!-- 添加一个 Font Awesome 图标 --> <span class="fa fa-user"></span> Your Profile </navigation-link>

//甚至,slot 位置也能包含其他组件: <navigation-link url="/profile"> <!-- 使用一个组件添加一个图标 --> <font-awesome-icon name="user"></font-awesome-icon> Your Profile </navigation-link>

//如果 <navigation-link> 完全没有 <slot> 元素,则 slot 位置传递的所有内容都会被直接丢弃。

 

命名插槽(named slot)

在某些场景中,需要用到多个插槽。对于这种场景,<slot> 元素有一个特殊的 name 特性,可以用于定义除默认插槽以外的多余插槽:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

为了给命名插槽提供内容,我们可以在父组件模板的 <template> 元素上使用 slot 特性

<base-layout>
  <template slot="header">
    <h1>这里是一个页面标题</h1>
  </template>

  <p>main 内容的一个段落。</p>
  <p>main 内容的另一个段落。</p>

  <template slot="footer">
    <p>这里是一些联系信息</p>
  </template>
</base-layout>

//或者,也可以对某个普通元素,直接使用 slot 特性: <base-layout> <h1 slot="header">这里是一个页面标题</h1> <p>main 内容的一个段落。</p> <p>main 内容的另一个段落。</p> <p slot="footer">这里是一些联系信息</p> </base-layout>

还有一个未命名插槽(unnamed slot),这是默认插槽,它是用于放置所有不匹配内容的插槽位置。在以上这两个示例中,最终渲染的 HTML 是:

<div class="container">
  <header>
    <h1>这里是一个页面标题</h1>
  </header>
  <main>
    <p>main 内容的一个段落。</p>
    <p>main 内容的另一个段落。</p>
  </main>
  <footer>
    <p>这里是一些联系信息</p>
  </footer>
</div>

默认插槽内容(default slot content)

<button type="submit">
  <slot>Submit</slot>
</button>

 

如果父组件模板中,向 slot 位置提供了内容,子组件 slot 元素的默认内容就会被替换。

编译时的作用域(compilation scope)

父组件模板的内容,全部在父组件作用域内编译;子组件模板的内容,全部在子组件作用域内编译。

 

作用域插槽(scoped slots)

在某些场景中,需要提供一个具有「可以访问组件内部数据的可复用插槽(reusable slot)」的组件。

//一个简单的 <todo-list> 组件
<
ul> <li v-for="todo in todos" v-bind:key="todo.id" > {{ todo.text }} </li> </ul>

但是在我们应用程序的某些部分中,我们想要将 todo items 中的每一项,都渲染为不同于 todo.text 的内容。

为了实现此潜在功能,我们必须将 todo item 的内容,包裹到一个 <slot> 元素中,然后,将此 slot 内部所需的所有相关数据,都传递给它的上下文环境(context)

<ul>
  <li
    v-for="todo in todos"
    v-bind:key="todo.id"
  >
    <!-- 我们为每个 todo 提供一个 slot 元素, -->
    <!-- 然后,将 `todo` 对象作为 slot 元素的一个 prop 传入。 -->
    <slot v-bind:todo="todo">
      <!-- 这里是回退内容(fallback content) -->
      {{ todo.text }}
    </slot>
  </li>
</ul>

现在,在我们引用 <todo-list> 组件的位置,我们可以将 todo items 插槽内容稍作修改,定义为一个 <template>,并且通过 slot-scope 特性访问子组件数据:

<todo-list v-bind:todos="todos">
  <!-- 将 `slotProps` 作为插槽内容所在作用域(slot scope)的引用名称 -->
  <template slot-scope="slotProps">
    <!-- 为 todo items 定义一个模板, -->
    <!-- 通过 `slotProps` 访问每个 todo 对象。 -->
    <span v-if="slotProps.todo.isComplete">?</span>
    {{ slotProps.todo.text }}
  </template>
</todo-list>

在 2.5.0+,slot-scope 不再局限于 <template> 元素,而是可以在任何元素或任何组件中的插槽内容上使用。

解构 slot-scope (不理解)

slot-scope 的值,实际上可以接收任何有效的 JavaScript 表达式,可以出现在函数定义中的参数所在位置。也就是说,在支持的环境中(在 单个文件组件 或在 现代浏览器),可以使用 ES2015 解构 来对表达式进行解构,就像这样:

<todo-list v-bind:todos="todos">
  <template slot-scope="{ todo }">
    <span v-if="todo.isComplete">?</span>
    {{ todo.text }}
  </template>
</todo-list>

 

以上是关于VUE2中文文档:深入组件(上)的主要内容,如果未能解决你的问题,请参考以下文章

Vue2从入门到精通深入浅出,带你彻底搞懂Vue2组件通信的9种方式

vue2.0的变化

VUE2中文文档:组件基础篇

vue2.0 代码功能片段

Vue2将任意命名变量作为道具传递

深入理解DOM节点类型第四篇——文档片段节点DocumentFragment