组件注册
一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。
全局注册
我们可以使用 [Vue 应用实例]的 .component()
方法,让组件在当前 Vue 应用中全局可用。
import {
createApp } from 'vue' const app = createApp({
}) app.component( // 注册的名字 'MyComponent', // 组件的实现 {
/* ... */ } )
如果使用单文件组件,你可以注册被导入的 .vue
文件:
import MyComponent from './App.vue' app.component('MyComponent', MyComponent)
.component()
方法可以被链式调用:
app .component('ComponentA', ComponentA) .component('ComponentB', ComponentB) .component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用:
<!-- 这在当前应用的任意组件中都可用 --> <ComponentA/> <ComponentB/> <ComponentC/>
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用。
局部注册
全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
- 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
在使用 <script setup>
的单文件组件中,导入的组件可以直接在模板中使用,无需注册:
<script setup> import ComponentA from './ComponentA.vue' </script> <template> <ComponentA /> </template>
如果没有使用 <script setup>
,则需要使用 components
选项来显式注册:
import ComponentA from './ComponentA.js' export default {
components: {
ComponentA }, setup() {
// ... } }
对于每个 components
对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。上面的例子中使用的是 ES2015 的缩写语法,等价于:
export default {
components: {
ComponentA: ComponentA } // ... }
请注意:局部注册的组件在后代组件中*不*可用。在这个例子中,ComponentA
注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用。
组件名格式
在整个指引中,我们都使用 PascalCase 作为组件名的注册格式,这是因为:
- PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
<PascalCase />
在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来。
在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase 的标签名在 DOM 内模板中是不可用的,详情参见 [DOM 内模板解析注意事项]。
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent
为名注册的组件,在模板中可以通过 <MyComponent>
或 <my-component>
引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。
Props 声明
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute。
在使用 <script setup>
的单文件组件中,props 可以使用 defineProps()
宏来声明:
<script setup> const props = defineProps(['foo']) console.log(props.foo) </script>
在没有使用 <script setup>
的组件中,prop 可以使用 [props
]选项来声明:
export default {
props: ['foo'], setup(props) {
// setup() 接收 props 作为第一个参数 console.log(props.foo) } }
注意传递给 defineProps()
的参数和提供给 props
选项的值是相同的,两种声明方式背后其实使用的都是 prop 选项。
除了使用字符串数组来声明 prop 外,还可以使用对象的形式:
// 使用 <script setup> defineProps({
title: String, likes: Number })
// 非 <script setup> export default {
props: {
title: String, likes: Number } }
对于以对象形式声明的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number
类型,则可使用 Number
构造函数作为其声明的值。
对象形式的 props 声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告。我们将在本章节稍后进一步讨论有关 [prop 校验]的更多细节。
如果你正在搭配 TypeScript 使用 <script setup>
,也可以使用类型标注来声明 props:
<script setup lang="ts"> defineProps<{ title?: string likes?: number }>() </script>
传递 prop 的细节
Prop 名字格式
如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。
defineProps({
greetingMessage: String })
<span>{
{ greetingMessage }}</span>
虽然理论上你也可以在向子组件传递 props 时使用 camelCase 形式 (使用 [DOM 内模板]时例外),但实际上为了和 HTML attribute 对齐,我们通常会将其写为 kebab-case 形式:
<MyComponent greeting-message="hello" />
对于组件名我们推荐使用 [PascalCase],因为这提高了模板的可读性,能帮助我们区分 Vue 组件和原生 HTML 元素。然而对于传递 props 来说,使用 camelCase 并没有太多优势,因此我们推荐更贴近 HTML 的书写风格。
静态 vs. 动态 Prop
至此,你已经见过了很多像这样的静态值形式的 props:
<BlogPost title="My journey with Vue" />
相应地,还有使用 v-bind
或缩写 :
来进行动态绑定的 props:
<!-- 根据一个变量的值动态传入 --> <BlogPost :title="post.title" /> <!-- 根据一个更复杂表达式的值动态传入 --> <BlogPost :title="post.title + ' by ' + post.author.name" />
传递不同的值类型
在上述的两个例子中,我们只传入了字符串值,但实际上任何类型的值都可以作为 props 的值被传递。
Number
<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind --> <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 --> <BlogPost :likes="42" /> <!-- 根据一个变量的值动态传入 --> <BlogPost :likes="post.likes" />
Boolean
<!-- 仅写上 prop 但不传值,会隐式转换为 `true` --> <BlogPost is-published /> <!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind --> <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 --> <BlogPost :is-published="false" /> <!-- 根据一个变量的值动态传入 --> <BlogPost :is-published="post.isPublished" />
Array
<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind --> <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 --> <BlogPost :comment-ids="[234, 266, 273]" /> <!-- 根据一个变量的值动态传入 --> <BlogPost :comment-ids="post.commentIds" />
Object
<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind --> <!-- 因为这是一个 JavaScript 表达式而不是一个字符串 --> <BlogPost :author="{ name: 'Veronica', company: 'Veridian Dynamics' }" /> <!-- 根据一个变量的值动态传入 --> <BlogPost :author="post.author" />
使用一个对象绑定多个 prop
如果你想要将一个对象的所有属性都当作 props 传入,你可以使用[没有参数的 v-bind
],即只使用 v-bind
而非 :prop-name
。例如,这里有一个 post
对象:
const post = {
id: 1, title: 'My Journey with Vue' }
以及下面的模板:
<BlogPost v-bind="post" />
而这实际上等价于:
<BlogPost :id="post.id" :title="post.title" />
单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:
const props = defineProps(['foo']) // ❌ 警告!prop 是只读的! props.foo = 'bar'
导致你想要更改一个 prop 的需求通常来源于以下两种场景:
- prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
const props = defineProps(['initialCounter']) // 计数器只是将 props.initialCounter 作为初始值 // 像下面这样做就使 prop 和后续更新无关了 const counter = ref(props.initialCounter)
- 需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
const props = defineProps(['size']) // 该 prop 变更时计算属性也会自动更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())
更改对象 / 数组类型的 props
当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该[抛出一个事件]来通知父组件做出改变。
Prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。
要声明对 props 的校验,你可以向 defineProps()
宏提供一个带有 props 校验选项的对象,例如:
defineProps({
// 基础类型检查 // (给出 `null` 和 `undefined` 值则会跳过任何类型检查) propA: Number, // 多种可能的类型 propB: [String, Number], // 必传,且为 String 类型 propC: {
type: String, required: true }, // 必传但可为空的字符串 propD: {
type: [String, null], required: true }, // Number 类型的默认值 propE: {
type: Number, default: 100 }, // 对象类型的默认值 propF: {
type: Object, // 对象或数组的默认值 // 必须从一个工厂函数返回。 // 该函数接收组件所接收到的原始 prop 作为参数。 default(rawProps) {
return {
message: 'hello' } } }, // 自定义类型校验函数 // 在 3.4+ 中完整的 props 作为第二个参数传入 propG: {
validator(value, props) {
// The value must match one of these strings return ['success', 'warning', 'danger'].includes(value) } }, // 函数类型的默认值 propH: {
type: Function, // 不像对象或数组的默认,这不是一个 // 工厂函数。这会是一个用来作为默认值的函数 default() {
return 'Default function' } } })
defineProps()
宏中的参数不可以访问 <script setup>
中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
一些补充细节:
- 所有 prop 默认都是可选的,除非声明了
required: true
。 - 除
Boolean
外的未传递的可选 prop 将会有一个默认值undefined
。 Boolean
类型的未传递 prop 将被转换为false
。这可以通过为它设置default
来更改——例如:设置为default: undefined
将与非布尔类型的 prop 的行为保持一致。- 如果声明了
default
值,那么在 prop 的值被解析为undefined
时,无论 prop 是未被传递还是显式指明的undefined
,都会改为default
值。
当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。
如果使用了[基于类型的 prop 声明],Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举例来说,defineProps<{ msg: string }>
会被编译为 { msg: { type: String, required: true }}
。
运行时类型检查
校验选项中的 type
可以是下列这些原生构造函数:
String
Number
Boolean
Array
Object
Date
Function
Symbol
Error
另外,type
也可以是自定义的类或构造函数,Vue 将会通过 instanceof
来检查类型是否匹配。例如下面这个类:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName this.lastName = lastName } }
你可以将其作为一个 prop 的类型:
defineProps({
author: Person })
Vue 会通过 instanceof Person
来校验 author
prop 的值是否是 Person
类的一个实例。
可为空的类型
如果该类型是必传的但可为空,你可以用一个包含 null
的数组语法:
defineProps({
id: {
type: [String, null], required: true } })
注意如果 type
仅为 null
而非使用数组语法,它将允许任何类型。
Boolean 类型转换
为了更贴近原生 boolean attributes 的行为,声明为 Boolean
类型的 props 有特别的类型转换规则。以带有如下声明的 <MyComponent>
组件为例:
defineProps({
disabled: Boolean })
该组件可以被这样使用:
<!-- 等同于传入 :disabled="true" --> <MyComponent disabled /> <!-- 等同于传入 :disabled="false" --> <MyComponent />
当一个 prop 被声明为允许多种类型时,Boolean
的转换规则也将被应用。然而,当同时允许 String
和 Boolean
时,有一种边缘情况——只有当 Boolean
出现在 String
之前时,Boolean
转换规则才适用:
// disabled 将被转换为 true defineProps({
disabled: [Boolean, Number] }) // disabled 将被转换为 true defineProps({
disabled: [Boolean, String] }) // disabled 将被转换为 true defineProps({
disabled: [Number, Boolean] }) // disabled 将被解析为空字符串 (disabled="") defineProps({
disabled: [String, Boolean] })
关注我,不迷路,共学习,同进步
关注我,不迷路,共学习,同进步
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/467.html