Vue3 学习笔记(三)组件、插槽、依赖注入

组件基础

组件注册:一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。
组件注册有两种方式:全局注册和局部注册。
全局注册

<script>
import { createApp } from 'vue'
import ComponentA from './App.vue'
import ComponentB from './App.vue'
const app = createApp({})
app.component('ComponentA', ComponentA)
      .component('ComponentB', ComponentB) //可以链式调用
</script>

局部注册

<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

Props声明

defineProps({
  greetingMessage: String //命名建议camelCase  
})
<MyComponent greeting-message="hello" /> //命名建议 kebab-case形式

任何类型的值都可以作为 props 的值被传递

单向数据流
所有的 props 都遵循着单向绑定原则
这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

Prop 校验

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  propF: {
    validator(value) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

注意:
Type类型可以是:String、Number、Boolean、Array、Object、Date、Function、Symbol
所有 prop 默认都是可选的,除非声明了 required: true。
除 Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
Boolean 类型的未传递 prop 将被转换为 false。你应该为它设置一个 default 值来确保行为符合预期。
defineProps() 宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的
Boolean 类型转换

defineProps({
  disabled: Boolean
})
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />

<!-- 等同于传入 :disabled="false" -->
<MyComponent />

组件中的事件传递

<!-- 子组件 BlogPost.vue -->
<script setup>
const props = defineProps(['title']) //定义props参数 
const emit = defineEmits(['enlarge-text']) //定义emit事件
console.log(props.title) //打印参数
</script>

<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

<!--父组件调用-->
const postFontSize = ref(1)
<BlogPost 
  title="My journey with Vue" 
  @enlarge-text="postFontSize += 0.1" //组件中自定义事件
  />

//选项形式写法
export default {
  emits: ['enlarge-text'],
  setup(props, ctx) {
    ctx.emit('enlarge-text')
  }
}

emit传递参数

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>
<MyButton @increase-by="(n) => count += n" />

emit 参数校验

<script setup>
const emit = defineEmits({
  // 没有校验
  click: null,
  // 校验 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})

function submitForm(email, password) {
  emit('submit', { email, password })
}
</script>

注意事项:
和原生 DOM 事件不一样,组件触发的事件没有冒泡机制

透传 Attributes
一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute。常见的示例包括 class、style 和 id attribute。可以通过 $attrs property 访问那些 attribute。
当组件返回单个根节点时,非 prop 的 attribute 将自动添加到根节点的 attribute 中。

<!-- 具有非 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>

同样的规则也适用于事件监听器

<div id="date-picker" class="demo">
  <date-picker @change="showChange"></date-picker>
</div>
app.component('date-picker', {
  template: `
    <select>
      <option value="1">Yesterday</option>
      <option value="2">Today</option>
      <option value="3">Tomorrow</option>
    </select>
  `,
  methods: {
    showChange(event) {
      console.log(event.target.value) // 将打印所选选项的值
    }
  }
})

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

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

在组件上使用v-model
默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。比如:
<custom-input v-model="searchText"></custom-input>
相当于:

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>
app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `<input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >`
})

我们可以通过向 v-model 传递参数来修改这些名称,如:

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

多个model绑定(写自定义组件或者插件时可能会用到)

<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>
app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName'],
  template: `
    <input 
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)">

    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)">
  `
})

插槽

渲染作用域:该插槽可以访问与模板其余部分相同的实例 property (即相同的“作用域”)
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
备用内容(默认内容):它只会在没有提供内容的时候被渲染,如果我们提供内容,则这个提供的内容将会被渲染从而取代备用内容

slots.dbdaf1e8.png

简单用法:

<!-- 组件内容 -->
<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>
<!-- 组件调用 -->
<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>
<!-- 此时会渲染成 -->
<button class="fancy-btn">Click me!</button>

具名插槽
组件在slot上添加name属性,父组件模板调用v-slot:,其简写为 #
如定义<base-layout> 组件:

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

父组件调用

<base-layout>
  <template v-slot:header> //可简写为<template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:default> //可简写为<template #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>

注意,v-slot 只能添加在 <template>

作用域插槽(插槽props参数传递)
目的:是让父组件中插槽的内容能够访问子组件的数据(子组件solt向父组件中传参)
子组件定义

app.component('todo-list', {
  data() {
    return {
      items: ['Feed a cat', 'Buy milk']
    }
  },
  template: `
    <ul>
      <li v-for="(item, index) in items">
        <slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>      //参数传递
      </li>
    </ul>
  `
})

绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在,在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
父组件中使用v-slot接收

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>
</todo-list>
<!-- 如果只要一个默认插槽,可简写为-->
<todo-list v-slot="slotProps"> // 另一种写法<todo-list #default="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>

<!-- 多个的话,需要分开写-->
<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</todo-list>
<!-- 结合结构解析-->
<todo-list v-slot="{ item }">  //v-slot="{ item: todo }" 结构解析重命名 v-slot="{ item = 'Placeholder' }" 结构解析的默认值
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

依赖注入 Provide / Inject

父组件 provide 选项来提供数据,子组件inject 选项使用这些数据。

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>

<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

注意:当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

可以使用 readonly() 来包装提供的值。表示只读如:

const count = ref(0)
provide('read-only-count', readonly(count))

动态组件

动态组件可以使用is attribute 来切换不同组件

<div id="dynamic-component-demo" class="demo">
  <button
     v-for="tab in tabs"
     :key="tab"
     :class="['tab-button', { active: currentTab === tab }]"
     @click="currentTab = tab"
   >
    {{ tab }}
  </button>
  <keep-alive>
    <component :is="currentTabComponent"></component>
  </keep-alive>
</div>

const app = Vue.createApp({
  data() {
    return {
      currentTab: 'Home',
      tabs: ['Home', 'Posts', 'Archive']
    }
  },
  computed: {
    currentTabComponent() {
      return 'tab-' + this.currentTab.toLowerCase()
    }
  }
})

app.component('tab-home', {
  template: `<div class="demo-tab">Home component</div>`
})
app.component('tab-posts', {
  template: `<div class="demo-tab">posts component</div>`,
})
app.component('tab-archive', {
  template: `<div class="demo-tab">Archive component</div>`
})

app.mount('#dynamic-component-demo')

异步组件

defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。


<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() =>
 import('./components/AdminPageComponent.vue')
)
</script>

<template>
 <AdminPage />
</template>

搭配 Suspense 使用
可参考:https://cn.vuejs.org/guide/built-ins/suspense.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容