Vue文档阅读——深入了解组件

204 阅读9分钟

组件注册

组件名

全局注册的时候起名,

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

当直接在DOM中使用一个组件的时候,请遵循W3C规范,字母全小写且必须包含一个连字符,避免和HTML元素冲突。

组件名大小写

  • kebab-case短横线分隔
Vue.component('my-component-name', { /* ... */ })
  • PascalCase首字母大写:在引用这个自定义元素的时候两种都可以使用。 反正最好用kebab-case。

全局注册

Vue.component('my-component-name', {
  // ... 选项 ...
})

任何Vue实例内都能用。

局部注册

new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})
import ComponentA from './ComponentA.vue'

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

这里ComponentA其实是ComponentA:ComponentA的缩写

模块系统

在模块中局部注册

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

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

基础组件的自动化全局注册

比如按钮,输入框之类可以在main.js中全局导入基础组件,使用require.context:

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

const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

全局注册的行为必须在根Vue实例创建之前发生。

Prop

prop的大小写

HTML的attribute名是大小写不敏感的,会把所有大写字符解释为小写字符。所以:

Vue.component('blog-post', {
  // 在 JavaScript 中是 camelCase 的
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>

prop类型

如果希望每个prop都有指定的值类型。可以以对象形式列出prop:

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

传递静态或者动态prop

本来默认传的是字符串,但是任何类型的值都可以传给一个prop

传入数字

<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>

传入一个布尔值

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>

传入一个数组

<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>

传入一个对象

<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
  v-bind:author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>

传入一个对象所有属性

可以使用不带参数的v-bind,例如:

post: {
  id: 1,
  title: 'My Journey with Vue'
}

这样写

<blog-post v-bind="post"></blog-post>

等价于

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

单向数据流

单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。

每次父级组件更新,子组件会跟着更新。所以不应该在子组件内部更改prop。

js中对象,数组都是通过引入传入的,所以如果在子组件中改变了对象或数组prop就会影响父组件的状态。

prop验证

可以为组件中的prop指定验证要求,将props中的值变为有验证需求的对象:

Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

当验证失败,Vue就会报警告。

prop会在组件实例创建之前进行验证,所以实例的属性data,computed是不能在验证default,validator中用的

类型检查

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

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

可以用

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

非prop的attribute

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

显式定义的prop用于向子组件传值。

如果你没有在子组件里面写props,但是父组件还是给你传过来了,那这个attribute就会添加到子组件的根元素上。

替换/合并已有的attribute

子组件里:

<input type="date" class="form-control">

而父组件里:

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

这种情况下,有两个不同的class值:

  • form-control
  • date-picker-theme-dark

一般来说子组件会被替换,但是这里可以合并,得到最终的:form-control date-picker-theme-dark

禁用Attribute继承

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

Vue.component('my-component', {
  inheritAttrs: false,
  // ...
})

配合实例的$attrs属性使用,这个属性包含了传递给组件的attribute名和值:

{
  required: true,
  placeholder: 'Enter your username'
}

有了以上两个东西就可以手动决定这些attribute被赋予在哪个元素:

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

这样就传递的attribute就不会作用到根元素而会作用在input元素上。

这个模式允许在使用基础组件的时候更像在使用原始的html元素,而不用担心哪个元素是真正的根元素:

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

自定义事件

事件名

事件名没有自动的大小写转换,触发的事件名要完全匹配。

自定义组件的v-model

一个组件上的v-model默认会利用名为value的prop和名为input的事件,但是单选框,复选框等类型的输入控件可能会将value attribute用于不同的目的。所以可以用model选项解决:

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

现在在这个组件上使用v-model:

<base-checkbox v-model="lovingVue"></base-checkbox>

这里的lovingVue的值就会传入名为checked的prop。当子组件触发了change事件并带了一个新的值,这个lovingVue的值会更新。

将原生事件绑定到组件

要在一个组件的根元素上直接监听一个原生事件,就可以使用.native修饰符:

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

但是在你监听根元素不是input 的时候,.native监听器会静默失败。不报错,但是onFocus处理函数不会调用。

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

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

有这个listeners属性就可以配合v-on="listeners"将所有时间监听器指向这个组件的某个特定的子元素。给这些监听器创建一个类似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>
  `
})

把父级的所有监听器用$listeners和你要添加的监听器用Object.assign合并成一个新的,然后用v-on绑定到你要监听的元素上去。

.sync修饰符

有时候我们需要对一个prop进行双向绑定。但是这样子组件会修改父组件,所有应该用update:myPropName的模式触发事件代替。 在一个含有 title的prop的子组件中,用下面的方法表达对其赋新值的意图:

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不能和表达式一起使用,只能提供想要的属性名,类似v-model

当我们用一个对象同时设置多个prop时,也可以这样

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

不给v-bind加名字,这样doc对象中每个属性都作为一个独立的prop传进去

将v-bind.sync用在字面量的对象上,:.sync="{title:doc.title}"

插槽

插槽内容

子组件:

<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

编译作用域

当想在插槽中使用数据时:

<navigation-link url="/profile">
  Logged in as {{ user.name }}
</navigation-link>

该插槽跟模板其他地方一样,可以访问相同的实例属性,但是不能访问子组件的作用域

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

后备内容

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

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

具名插槽

有时候我们需要多个插槽,看看这个子组件:

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

这种情况,可以有一个特殊的attribute:name。可以用来定义额外的插槽:

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

不带name 的出去会带有隐含的名字default。

向具名插槽提供内容时,可以在template元素上使用v-slot指令,用v-slot的参数形式提供其名称:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

这样template元素中的所有内容会被传入相应的插槽。

v-slot只能添加在template上(只有一种特殊情况)

作用域插槽

要让插槽内容能够访问子组件的数据。

子组件:

<span>
  <slot>{{ user.lastName }}</slot>
</span>

如果要换掉后备内容,为了让user在父级插槽内容中可用,我们可以将user作为元素的一个attribute绑定上去:

<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

绑定在slot元素上的attribute被称为插槽prop。这样在父级作用域中,就可以使用v-slot来定义我们提供的插槽prop数据了:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

这里将包含所有插槽prop的对象命名为slotProps,你也可以用任意你喜欢的名字。

独占默认插槽的缩写语法

当被提供的内容只有默认插槽时,组件的标签才可以被当做插槽的模板来使用,可以把v-slot直接用在组件上:

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

或者干脆不加default

解构插槽Prop

作用域插槽的内部工作原理就是将你的插槽内容包括在一个传入单个参数的函数中去:

function (slotProps) {
  // 插槽内容
}

这意味着v-slot的值时间上可以是任何能够作为函数定义中的参数的js表达式。所有可以用解构来传入具体的插槽prop:

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

同样可以prop重命名:

<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>

甚至可以定义默认内容,当插槽prop是undefined的时候:

<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>

动态插槽名

动态指令参数也可以用在v-slot上来定义动态的插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

具名插槽的缩写

v-slot也有缩写,就是把v-slot: 替换为#:

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

缩写只能在其有参数的时候才能用。

其他示例

插槽prop允许我们将插槽转换为可复用的模板,这些模板可以基于输入的prop渲染出不同的内容。用来设计封装时非常有用。

例如要实现一个todolist的组件:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>

我们可以把每个todo作为父级组件的插槽,通过父级组件对其进行控制,然后将todo作为一个插槽prop进行绑定:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    <!--
    我们为每个 todo 准备了一个插槽,
    将 `todo` 对象作为一个插槽的 prop 传入。
    -->
    <slot name="todo" v-bind:todo="todo">
      <!-- 后备内容 -->
      {{ todo.text }}
    </slot>
  </li>
</ul>

然后我们使用todolist组件的时候就可以给todo定义不一样的template,并且可以从子组件中获取数据:

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

动态组件&异步组件

在动态组件上使用keep-alive

之前使用is attribute来切换不同的组件

<component v-bind:is="currentTabComponent"></component>

当这些组件切换的时候,要想保持这些组件的状态,避免重复渲染,就可以使用元素将动态组件包裹起来:

<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

这个要求被切换到的组件都有自己的名字,不论是通过组件的name选项还是全局/局部注册

最多常见的用法应该是和v-if一起使用:

<!-- 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

当组件在中切换时,它的activated和deactivatied这两个生命周期钩子函数将会被对应执行。

异步组件(组件懒加载)

在大型应用中,我们需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载。为了简化,Vue允许以工厂函数的方式定义组件,这个工厂函数会异步解析你的组件定义。

Vue只有在这个组件需要被渲染的时候才会触发该工厂函数,并且会把结果缓存起来供未来重渲染。

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

这工厂函数收到一个resolve回调,这个回调函数会从服务器得到组件定义的时候被调用。

可以将异步组件和webpack的code-splitting功能一起配合使用;

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

也可以在工厂函数中返回一个Promise,可以这样写:

Vue.component(
  'async-webpack-example',
  // 这个 `import` 函数会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

当局部注册的时候,可以直接提供一个返回Promise的函数:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})

处理加载状态

这里的异步组件工厂函数也可以返回一个如下格式的对象:

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

处理边界情况

访问元素&组件

最好不要触达另一个组件实例内部或手动操作DOM元素,不过特殊情况也可以。

访问根实例

在每个new Vue实例的子组件中,其根实例可以通过$root属性进行访问。 在这个根实例中:

// Vue 根实例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})

所有的子组件都可以将这个实例作为一个全局store来访问使用:

// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()

对于小型应用比较方便,但是大型应用推荐使用vuex。

访问父级组件实例

$root类似,使用$parent可用来从子组件访问父组件实例。

有时候需要特别地分享一些组件库:

<google-map>
  <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>

这个google-map组件可以定义一个map属性,所有的子组件都需要访问他。这时候就只能通过类似this.$parent.getMap来拿。

访问子组件实例或子元素

一般都用prop或事件

可以通过ref这个attribute给子组件赋予一个ID引用

<base-input ref="usernameInput"></base-input>

现在再使用

this.$refs.usernameInput

来访问这个实例。这个子组件可以使用一个类似的ref提供对内部这个指定元素的访问。

<input ref="input">

甚至可以通过父级组件定义方法:

methods: {
  // 用来从父级组件聚焦输入框
  focus: function () {
    this.$refs.input.focus()
  }
}

这样就允许父级组件通过下面的代码聚焦子组件的输入框了

this.$refs.usernameInput.focus()

当ref和v-for一起使用的时候,你得到的引用将会是包含了对应数据源的这些子组件的数组。

$refs只会在组件渲染完成之后生效,并且他们不是响应式的。仅作为一个用于操作子组件的坏方法。应避免在模板或者计算属性中访问$refs

依赖注入

在访问父级组件实例的时候,用$parent无法很好的扩展到更深层级的嵌套组件上。要解决这个问题,可使用两个新的实例选项:provide和inject

provide选项允许我们制定我们想要提供给后代组件的数据/方法。

provide: function () {
  return {
    getMap: this.getMap
  }
}

然后在任何后代组件里,都可以使用inject选项来接受指定的我们想添加在这个实例上的属性:

inject: ['getMap']

相比$parent来说,这个用法可以更好的拿到getMap,而不需要暴露整个实例。

负面影响还是有的。将组件耦合起来了。同时提供的属性是非响应的。推荐使用vuex。

程序化的事件侦听器

$emit用法,可以被v-on侦听,但是Vue实例同时在其事件接口中提供了其他的方法:

  • 通过$on(eventName,eventHandler)侦听一个事件
  • 通过$once(eventName,eventHandler)一次性侦听一个事件
  • 通过$off(eventName,eventHandler)停止侦听一个事件

当需要在一个组件实例上,手动侦听事件时就有用了。

集成一个第三方库的模式:

// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
  // Pikaday 是一个第三方日期选择器的库
  this.picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
  this.picker.destroy()
}

两个问题:

  • 它需要在这个组件实例中保存这个picker
  • 难以程序化地清理我们建立的东西

用一个程序化的侦听器解决:

mounted: function () {
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })

  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}

Vue的事件系统不同于浏览器的EventTargetAPI。

循环引用

递归组件

组件是可以在他们自身的模板中调用自身的,但是只能通过name选项来做这件事

组件之间的循环引用

在A组件中使用了B组件,B组件中又使用了A组件。

当全局注册的时候没问题,但是当是懒加载就会报错。

解决方案:在生命周期钩子beforeCreated时,去注册那个需要的组件。

beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者在本地注册组件的时候,使用异步import

components: {
  TreeFolderContents: () => import('./tree-folder-contents.vue')
}

模板定义的替代品

内联模板

inline-template这样的attribute出现在子组件上时,组件将会使用里面的内容作为模板。

<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>

X-Template

另一个定义模板的方式是在script元素中,为其戴上text/x-template类型,然后通过一个id将模板引用过去:

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})

控制更新

想要强制更新或者阻止不必要的更新。

强制更新

$forceUpdate来强制更新

通过v-once创建低开销的静态组件

在根元素上添加v-once attribute确保这些内容只计算一次就缓存起来

Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})