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

556 lines
11 KiB

5 months ago
# 深入组件
## 组件注册
### 全局注册
```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
})
```