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.
555 lines
11 KiB
555 lines
11 KiB
# 深入组件
|
|
|
|
## 组件注册
|
|
|
|
### 全局注册
|
|
|
|
```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
|
|
})
|
|
```
|
|
|
|
|