vue 官方文档——深入组件

286 阅读5分钟

组件注册

组件名

  1. 组件名尽量遵循以下两个命名规则:
  • 全部小写
  • 包含连字符(即多个单词用连字符连接)
  1. 组件名字引用写法:
  • 当组件注册的时候采用连字符注册时,引用的时候必须使用连字符写法,如:

    // 引用组件
    <my-component-name></my-component-name>
    // 注册组件
    app.component('my-component-name', {
      /* ... */
    })
    
  • 当组件注册的时候采用首字母大写的方式注册时,引用的时候既可以用首字母大写的方式引用,也可以用连字符的方式引用,如:

    // 引用组件方式1:
    <MyComponentName></MyComponentName>
    // 引用组件方式2:
    <my-component-name></my-component-name>
    // 注册组件
    app.component('MyComponentName', {
      /* ... */
    })
    

全局注册和局部注册

全局注册:

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

局部注册:

const app = Vue.createApp({
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

props

传入一个对象所有的 property

如果想要把一个对象的所有属性都作为 prop 传入,可以使用不带参数的 v-bind 写法。如:

post: {
    id: 1,
    name: 'hhh'
}

<child-component v-bind="post"></child-component>
// 等价于
<child-component v-bind:id="post.id" v-bind:name="post.name"></child-component>

prop 验证

在组件中可以为 prop 指定验证要求,应该注意的是,这些验证是在组件创建之前进行的,即组件的 property(data、computed)在 default 函数和 validator 函数中都是不可用的。

app.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() {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator(value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 具有默认值的函数
    propG: {
      type: Function,
      // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
})

非 prop 的 Attribute

单个根结点的 Attribute 继承

当组件为单个根结点时,非 prop 的 attribute(包括事件监听器) 将自动添加到根节点的 attribute 中。

<!-- data-picker 组件的定义 -->
app.component('date-picker', {
  template: `
    <div class="date-picker">
      <input type="datetime-local" />
    </div>
  `
})

<!-- data-picker 组件的引用 -->
<!-- 具有非prop attribute的Date-picker组件 -->
<date-picker data-status="activated"></date-picker>

<!-- 渲染 date-picker 组件 -->
<div class="date-picker" data-status="activated">
  <input type="datetime-local" />
</div>

事件监听:

事件监听同样适用这个规则。如:当有一个具有 change 事件的 HTML 元素将作为 date-picker 的根元素时。在这种情况下,change 事件监听器从父组件传递到子组件,它将在根元素的 change 事件上触发,从而不需要在 date-picker 组件中定义事件:

<date-picker @change="showChange"></date-picker>
app.component('date-picker', {
  template: `
    <select>
      <option value="1">Yesterday</option>
      <option value="2">Today</option>
      <option value="3">Tomorrow</option>
    </select>
  `
})

const app = Vue.createApp({
  methods: {
    showChange(event) {
      console.log(event.target.value) // 将记录所选选项的值
    }
  }
})

多个根结点的 Attribute 继承

有多个根节点的组件不具有自动 attribute 隐式贯穿行为。如果未显式绑定 $attrs,将发出运行时警告。

<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// 这将发出警告
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  `
})

// 没有警告,$attrs被传递到<main>元素
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main v-bind="$attrs">...</main>
    <footer>...</footer>
  `
})

禁用 Attribute 继承

若不想组件的根元素继承 attribute ,可以在组件的设置 inheritAttrs: false,并且可以将 attribute 应用于根结点之外的其他元素。通过将 inheritAttrs 选项设置为 false,就可以访问组件的 $attrs property,该 property 包括组件 propsemits property 中未包含的所有属性 (例如,classstylev-on 监听器等)。

app.component('date-picker', {
  inheritAttrs: false,
  template: `
    <div class="date-picker">
      <input type="datetime-local" v-bind="$attrs" />
    </div>
  `
})

这样,data-status attribute 将应用于 input 元素

<!-- Date-picker 组件 使用非 prop attribute -->
<date-picker data-status="activated"></date-picker>

<!-- 渲染 date-picker 组件 -->
<div class="date-picker">
  <input type="datetime-local" data-status="activated" />
</div>

自定义事件

emits配置项

可以通过 emits 配置项在组件上定义发出的事件。

app.component('custom-form', {
  emits: ['inFocus', 'submit']
})

也可以将 emits 配置项定义为对象,从而验证发出的事件。要添加验证,将事件定义为一个函数,该函数接收传递给 $emit 调用的参数,并返回一个布尔值以指示事件是否有效。

app.component('custom-form', {
  emits: {
    // 没有验证
    click: null,

    // 验证submit 事件
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm(email, password) {
      this.$emit('submit', { email, password })
    }
  }
})

v-model 参数

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。我们可以通过向 v-model 传递参数来修改这些名称:

<my-component1 v-model="bookTitle"></my-component1>
<my-component2 v-model:title="bookTitle"></my-component2>
app.component('my-component1', {
  props: {
    modelValue: String, // v-model未带名称时使用 modelValue 接收值,即 bookTitle
  },
  emits: ['update:modelValue'],
  template: `
    <input
      type="text"
      :value="title"
      @input="$emit('update:modelValue', $event.target.value)">
  `
})

app.component('my-component2', {
  props: {
    title: String
  },
  emits: ['update:title'],
  template: `
    <input
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)">
  `
})

v-model 自定义修饰符

例子:创建一个示例自定义修饰符 capitalize,它将 v-model 绑定提供的字符串的第一个字母大写。

添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers prop。

请注意,当组件的 created 生命周期钩子触发时,modelModifiers prop 会包含 capitalize,且其值为 true——因为 capitalize 被设置在了写为 v-model.capitalize="myText"v-model 绑定上。

<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  template: `
    <input type="text"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)">
  `,
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
})

现在我们已经设置了 prop,我们可以检查 modelModifiers 对象键并编写一个处理器来更改发出的值。在下面的代码中,每当元素触发 input 事件时,我们都将字符串大写。

const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
})

app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  },
  template: `<input
    type="text"
    :value="modelValue"
    @input="emitValue">`
})

app.mount('#app')

对于带参数的 v-model 绑定,生成的 prop 名称将为 参数名称 + "Modifiers"

<my-component v-model:description.capitalize="myText"></my-component>
app.component('my-component', {
  props: ['description', 'descriptionModifiers'],
  emits: ['update:description'],
  template: `
    <input type="text"
      :value="description"
      @input="$emit('update:description', $event.target.value)">
  `,
  created() {
    console.log(this.descriptionModifiers) // { capitalize: true }
  }
})

插槽

插槽内容

插槽可以在引用组件内插入内容,并在组件内部的slot元素替换内容,如下:

<todo-button>
  Add todo
</todo-button>

<!-- todo-button 组件,此时slot元素将会被替换为Add todo -->
<button class="btn-primary">
  <slot></slot>
</button>

如果组件的template内没有包含slot元素,则组件的起始标签和结束标签之间的内容将不会渲染

插槽作用域

插槽作用域与父级作用域一致,即父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

备用内容

在插槽替换位置的slot内书写内容,这样在外部引用组件首尾标签中间不提供内容,插槽内容将会被slot标签中间的内容替代。

<!-- 父级引用组件 -->
<submit-button></submit-button>

<!-- submit-button组件 -->
<button type="submit">
  <slot>Submit</slot>
</button>

<!-- 实际渲染结果 -->
<button type="submit">
  Submit
</button>

具名插槽

slot元素有一个name属性,这个属性可以定义插槽名称从而定义额外的插槽。不带 nameslot 出口会带有隐含的名字“default”。

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

<!-- 父元素中引用子元素并使用插槽插入内容 -->
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>
  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>
  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

<!-- 子元素 base-layout -->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<!-- 实际渲染的HTML -->
<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

v-slot 只能添加在 template,只有一种特殊,见作用域插槽中第一个例子的独占默认插槽的缩写语法

作用域插槽

正常情况下,父组件是无法访问子组件中的内容的。但是,可以通过在子组件的 <slot> 绑定插槽 prop (如例子中的 item , index, another-attribute),然后在父组件中使用 v-slot 定义提供的插槽 prop 的名字。

<!-- todo-list组件  -->
<ul>
  <li v-for="( item, index ) in items">
    <slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
  </li>
</ul>

<!-- 父组件 -->
<!-- 只有一个默认插槽时,可以省略 default,并将 v-slot 绑定在 todo-list 组件上 -->
<!-- 独占默认插槽的缩写语法 -->
<todo-list v-slot="slotProps">
  <span>{{ slotProps.index + "、" }}</span>
  <span>{{ slotProps.item }}</span>
</todo-list>

<!-- 有多个插槽时,不可以省略 default 
因为省略会导致作用域不明确(注意与具名插槽的区别),并且 v-slot 必须绑定在 template 上 -->
<todo-list>
  <template v-slot:default="slotProps">
    <i></i>
    <span>{{ slotProps.item }}</span>
  </template>

  <template v-slot:other="otherSlotProps">
  </template>
</todo-list>

解构插槽 prop

作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里。这就意味着,可以解构传入具体插槽的 prop,如:

<todo-list v-slot="{ item }">
  <i></i>
  <span>{{ item }}</span>
</todo-list>

<!-- 将 item 重命名为 todo -->
<todo-list v-slot="{ item: todo }">
  <i></i>
  <span>{{ todo }}</span>
</todo-list>

<!-- 为 item 提供默认值 'Placeholder' -->
<todo-list v-slot="{ item = 'Placeholder' }">
  <i></i>
  <span>{{ item }}</span>
</todo-list>

动态插槽名

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

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

具名插槽的缩写

v-onv-bind 一样,v-slot 也有缩写,为字符 #。例如 v-slot:header 可以被重写为 #header。该缩写必须带有参数:

<!-- 错误写法 -->
<todo-list #="{ item }">
  <i></i>
  <span>{{ item }}</span>
</todo-list>

<!-- 正确写法 -->
<todo-list #default="{ item }">
  <i></i>
  <span>{{ item }}</span>
</todo-list>

<!-- 正确写法 -->
<todo-list #default>
  <i></i>
  <span>I'm hungry!</span>
</todo-list>

<!-- 正确写法 -->
<todo-list>
  <template #default="slotProps">
    <i></i>
    <span>{{ slotProps.item }}</span>
  </template>
  <template #other="otherSlotProps">
    <span>111</span>
  </template>
  <template #another>
    <span>222</span>
  </template>
</todo-list>

Provide / Inject

provide 和 inject 用于祖先组件想向子孙组件传递数据

provide 的两种形式:

provide: {
    user: "No.1",
    userLength: this.userList.length // 报错,对象形式的 provide 不能使用 this 指向
}

provide() {
    return {
        user: "No.1",
	userLength: this,userList.length // 函数形式的 provide 可以使用 this 指向
    }
}

inject 使用数组形式:

inject: ["user", "userLength"]

处理响应性

默认情况下,provide/inject 绑定并不是响应式的,可以通过传递一个 ref property 或 reactive 对象给 provide 来改变这种行为

app.component('todo-list', {
  provide() {
    return {
      // vue版本要在3.x以上才有这个computed方法
      todoLength: Vue.computed(() => this.todos.length)
    }
  }
})

app.component('todo-list-statistics', {
  inject: ['todoLength'],
  created() {
    console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
  }
})