alioth/before/cha/14=vue3/docs/学习vue3/vue3深入组件.md
2025-05-30 09:18:01 +08:00

556 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 深入组件
## 组件注册
### 全局注册
```js
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>`
```vue
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
```
- 没有使用 `<script setup>`
```js
export default {
props: ['foo'],
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
}
}
```
### 使用一个对象绑定多个 prop
```vue
<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 校验
```js
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'
}
}
})
```
## 组件事件
```vue
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>
<!--推荐使用 kebab-case 形式来编写监听器-->
<MyComponent @some-event="callback" />
<MyComponent @some-event.once="callback" />
```
![](./vue3深入组件.assets/3d9f574cc12244ecb8dcab9ef49d7697.png)
### 事件参数
```vue
<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>` 的顶级作用域下。
```vue
<script setup>
// 这个必须写在最外曾
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
ctx.emit('submit')
}
}
</script>
```
### 事件校验
- 要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 emit 的内容,返回一个布尔值来表明事件是否合法。
```vue
<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` 可以在组件上使用以实现双向绑定。
```vue
<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 的参数
```vue
<MyComponent v-model:title="bookTitle" />
```
```vue
<!-- 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 绑定
```vue
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
```
```vue
<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 继承
```vue
<!--父组件-->
<MyButton class="large" />
<!-- <MyButton> 子组件的模板 -->
<button>click me</button>
<!--最后渲染出的 DOM 结果-->
<button class="large">click me</button>
```
### `class` 和 `style`合并
- `template`下只有一个元素的话会继承,多个的话就不会,见**多根节点的 Attributes 继承**
### `v-on` 监听器继承
### 深层组件继承——重写组件的话,会被子组件调用的子组件继承
### 禁用 Attributes 继承
```vue
<script setup>
defineOptions({
inheritAttrs: false
})
// ...setup 逻辑
</script>
```
### 多根节点的 Attributes 继承
- 和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。
- 如果 <CustomLayout> 有下面这样的多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告。
- 如果 $attrs 被显式绑定,则不会有警告:
```vue
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
```
### 在 JavaScript 中访问透传 Attributes
```vue
<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
### 默认内容
```vue
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
```
### 具名插槽
- `v-slot` 有对应的简写 `#`,因此 `<template v-slot:header>` 可以简写为 `<template #header>`。其意思就是“将这部分模板片段传入子组件的 `header` 插槽中”。
```vue
<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>
```
### 动态插槽名
```vue
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
```
### 作用域插槽——使用自组建的传值
```vue
<!-- <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>
```
### 具名作用域插槽
```vue
<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 (提供)
```vue
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
```
### 应用层 Provide
```js
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
```
### Inject (注入)
```vue
<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 作注入名
- 包含非常多的依赖提供,很麻烦
```js
// keys.js
export const myInjectionKey = Symbol()
```
## 异步组件 ? 懒加载?
### 基本用法
```js
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
```
### ES 模块动态导入也会返回一个 Promise
```js
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
app.component('MyComponent', defineAsyncComponent(() =>
import('./components/MyComponent.vue')
))
```
```vue
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
```
### 加载与错误状态
```js
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件默认值是Infinity
timeout: 3000
})
```