You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
vue3/docs/学习vue3/vue3深入组件.md

11 KiB

深入组件

组件注册

全局注册

import { createApp } from 'vue'

const app = createApp({})

app.component(
  // 注册的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)
// 单文件组件
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)

// .component() 方法可以被链式调用
app
    .component('ComponentA', ComponentA)
    .component('ComponentB', ComponentB)
    .component('ComponentC', ComponentC)

命名

  • PascalCase:大驼峰

Props

Props 声明

  • 使用 <script setup>
<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>
  • 没有使用 <script setup>
export default {
  props: ['foo'],
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}

使用一个对象绑定多个 prop

<script setup>
    const post = {
        id: 1,
        title: 'My Journey with Vue'
    }
</script>

<template>
    <BlogPost v-bind="post" />
    <!--等价于-->
    <BlogPost :id="post.id" :title="post.title" />
</template>

单向数据流

  • 不能重新赋值props
  • 能修改引用类型,但是会造成损耗不推荐

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' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propF: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

组件事件

<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

<!--推荐使用 kebab-case 形式来编写监听器-->
<MyComponent @some-event="callback" />
<MyComponent @some-event.once="callback" />

事件参数

<button @click="$emit('increaseBy', 1, 2, 3)">
  Increase by 1
</button>

<MyButton @increase-by="(n, l, m) => count += n" />

声明触发的事件

  • 我们在 <template> 中使用的 $emit 方法不能在组件的 <script setup> 部分中使用,但 defineEmits() 会返回一个相同作用的函数供我们使用:
  • defineEmits() 宏不能在子函数中使用。如上所示,它必须直接放置在 <script setup> 的顶级作用域下。
<script setup>
    // 这个必须写在最外曾
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}



    export default {
        emits: ['inFocus', 'submit'],
        setup(props, ctx) {
            ctx.emit('submit')
        }
    }
</script>

事件校验

  • 要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 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>

组件 v-model

  • v-model 可以在组件上使用以实现双向绑定。
<script setup>
//    子组件
const model = defineModel()

// 使 v-model 必填
const model = defineModel({ required: true })

// 提供一个默认值
const model = defineModel({ default: 0 })
function update() {
    // 以ref的方式调用
    model.value++
}
</script>

<template>
    <!-- 父组件Parent.vue -->
    <Child v-model="count" />
</template>

v-model 的参数

<MyComponent v-model:title="bookTitle" />
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
const title = defineModel('title', { required: true })
</script>

<template>
  <input type="text" v-model="title" />
</template>

多个 v-model 绑定

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

? 传递对象是否还在使用ref响应式的参数

透传 Attributes

Attributes 继承

<!--父组件-->
<MyButton class="large" />
<!-- <MyButton> 子组件的模板 -->
<button>click me</button>
<!--最后渲染出的 DOM 结果-->
<button class="large">click me</button>

classstyle合并

  • template下只有一个元素的话会继承,多个的话就不会,见多根节点的 Attributes 继承

v-on 监听器继承

深层组件继承——重写组件的话,会被子组件调用的子组件继承

禁用 Attributes 继承

<script setup>
defineOptions({
  inheritAttrs: false
})
// ...setup 逻辑
</script>

多根节点的 Attributes 继承

  • 和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。
  • 如果 有下面这样的多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告。
  • 如果 $attrs 被显式绑定,则不会有警告:
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

在 JavaScript 中访问透传 Attributes

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

const attrs = useAttrs()

// 如果没有使用 <script setup>,attrs 会作为 setup() 上下文对象的一个属性暴露:
export default {
    setup(props, ctx) {
        // 透传 attribute 被暴露为 ctx.attrs
        console.log(ctx.attrs)
    }
}
</script>

插槽 Slots

默认内容

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

具名插槽

  • v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<!--父组件-->
<BaseLayout>
    <template v-slot:header>
        <!-- header 插槽的内容放这里 -->
    </template>
</BaseLayout>

<BaseLayout>
    <template #header>
        <h1>Here might be a page title</h1>
    </template>

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

    <template #footer>
        <p>Here's some contact info</p>
    </template>
</BaseLayout>

动态插槽名

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

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

作用域插槽——使用自组建的传值

<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

<MyComponent v-slot="slotProps">
    {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
<MyComponent v-slot="{ text, count }">
    {{ text }} {{ count }}
</MyComponent>

具名作用域插槽

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

<!--向具名插槽中传入 props-->
<slot name="header" message="hello"></slot>

依赖注入

  • 解决多级props传递

Provide (提供)

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

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

应用层 Provide

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

Inject (注入)

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

const message = inject('message')

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')

// 在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
const value = inject('key', () => new ExpensiveClass(), true)
// 第三个参数表示默认值应该被当作一个工厂函数
</script>

使用 Symbol 作注入名

  • 包含非常多的依赖提供,很麻烦
// keys.js
export const myInjectionKey = Symbol()

异步组件 ? 懒加载?

基本用法

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

ES 模块动态导入也会返回一个 Promise

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

app.component('MyComponent', defineAsyncComponent(() =>
    import('./components/MyComponent.vue')
))
<script setup>
import { defineAsyncComponent } from 'vue'

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

<template>
  <AdminPage />
</template>

加载与错误状态

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})