Vue 跨请求状态污染
SSR(服务端渲染)环境下,应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样。如果我们用单个用户特定的数据对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个用户的请求。我们把这种情况称为跨请求状态污染。
Vue 跨请求状态污染
SSR(服务端渲染)环境下,应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样。如果我们用单个用户特定的数据对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个用户的请求。我们把这种情况称为跨请求状态污染。
Vue3内置指令:v-bind
v-bind 动态的绑定一个或多个 attribute,也可以是组件的 prop。
缩写:
:
或者 .
(当使用 .prop
修饰符)期望:any (带参数) | Object (不带参数)
参数:attrOrProp (可选的)
修饰符
.camel
- 将短横线命名的 attribute 转变为驼峰式命名。.prop
- 强制绑定为 DOM property。3.2+.attr
- 强制绑定为 DOM attribute。3.2+用途
当用于绑定 class
或 style
attribute,v-bind
支持额外的值类型如数组或对象。详见下方的指南链接。
在处理绑定时,Vue 默认会利用 in
操作符来检查该元素上是否定义了和绑定的 key 同名的 DOM property。如果存在同名的 property,则 Vue 会将它作为 DOM property 赋值,而不是作为 attribute 设置。这个行为在大多数情况都符合期望的绑定值类型,但是你也可以显式用 .prop
和 .attr
修饰符来强制绑定方式。有时这是必要的,特别是在和自定义元素打交道时。
当用于组件 props 绑定时,所绑定的 props 必须在子组件中已被正确声明。
当不带参数使用时,可以用于绑定一个包含了多个 attribute 名称-绑定值对的对象。
示例
<!-- 绑定 attribute -->
<img v-bind:src="imageSrc" />
<!-- 动态 attribute 名 -->
<button v-bind:[key]="value"></button>
<!-- 缩写 -->
<img :src="imageSrc" />
<!-- 缩写形式的动态 attribute 名 (3.4+),扩展为 :src="src" -->
<img :src />
<!-- 动态 attribute 名的缩写 -->
<button :[key]="value"></button>
<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName" />
<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]"></div>
<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>
<!-- 绑定对象形式的 attribute -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>
<!-- prop 绑定。“prop” 必须在子组件中已声明。 -->
<MyComponent :prop="someThing" />
<!-- 传递子父组件共有的 prop -->
<MyComponent v-bind="$props" />
<!-- XLink -->
<svg><a :xlink:special="foo"></a></svg>
.prop
修饰符也有专门的缩写,.
:
<div :someProperty.prop="someObject"></div>
<!-- 等同于 -->
<div .someProperty="someObject"></div>
当在 DOM 内模板使用 .camel
修饰符,可以驼峰化 v-bind
attribute 的名称,例如 SVG viewBox
attribute:
<svg :view-box.camel="viewBox"></svg>
如果使用字符串模板或使用构建步骤预编译模板,则不需要 .camel
。
Vue 组件注册
一个 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 组件注册代码来配合不同来源的模板。
Vue Props 简要教程
此章节假设你已经看过了组件基础。若你还不了解组件是什么,请先阅读该章节。
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute (关于透传 attribute,我们会在专门的章节中讨论)。
在使用 <script setup>
的单文件组件中,props 可以使用 defineProps()
宏来声明:
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
在没有使用 <script setup>
的组件中,props 可以使用 props
选项来声明:
export default {
props: ['foo'],
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
}
}
注意传递给 defineProps()
的参数和提供给 props
选项的值是相同的,两种声明方式背后其实使用的都是 props 选项。
除了使用字符串数组来声明 props 外,还可以使用对象的形式:
// 使用 <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>
更多关于基于类型的声明的细节请参考组件 props 类型标注。
如果一个 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 的书写风格。
至此,你已经见过了很多像这样的静态值形式的 props:
<BlogPost title="My journey with Vue" />
相应地,还有使用 v-bind
或缩写 :
来进行动态绑定的 props:
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />
<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
在上述的两个例子中,我们只传入了字符串值,但实际上任何类型的值都可以作为 props 的值被传递。
<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :likes="42" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes" />
<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<BlogPost is-published />
<!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :is-published="false" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :is-published="post.isPublished" />
<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :comment-ids="[234, 266, 273]" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :comment-ids="post.commentIds" />
<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost
:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
/>
<!-- 根据一个变量的值动态传入 -->
<BlogPost :author="post.author" />
如果你想要将一个对象的所有属性都当作 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 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,对 Vue 来说,阻止这种更改需要付出的代价异常昂贵。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。
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'
}
}
})
TIP
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 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]
})
Vue选项式API:生命周期API
在组件实例初始化完成之后立即调用。
类型
interface ComponentOptions {
beforeCreate?(this: ComponentPublicInstance): void
}
详细信息
在实例初始化完成并且 props 被解析后立即调用。
接着 props 会被定义成响应式属性,data()
和 computed
等选项也开始进行处理。
注意,组合式 API 中的 setup()
钩子会在所有选项式 API 钩子之前调用,beforeCreate()
也不例外。
在组件实例处理完所有与状态相关的选项后调用。
类型
interface ComponentOptions {
created?(this: ComponentPublicInstance): void
}
详细信息
当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此 $el
属性仍不可用。
在组件被挂载之前调用。
类型
interface ComponentOptions {
beforeMount?(this: ComponentPublicInstance): void
}
详细信息
当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
这个钩子在服务端渲染时不会被调用。
在组件被挂载之后调用。
类型
interface ComponentOptions {
mounted?(this: ComponentPublicInstance): void
}
详细信息
组件在以下情况下被视为已挂载:
所有同步子组件都已经被挂载。(不包含异步组件或 <Suspense>
树内的组件)
其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。
这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端被调用。
这个钩子在服务端渲染时不会被调用。
在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。
类型
interface ComponentOptions {
beforeUpdate?(this: ComponentPublicInstance): void
}
详细信息
这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。
这个钩子在服务端渲染时不会被调用。
在组件因为一个响应式状态变更而更新其 DOM 树之后调用。
类型
interface ComponentOptions {
updated?(this: ComponentPublicInstance): void
}
详细信息
父组件的更新钩子将在其子组件的更新钩子之后调用。
这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。
这个钩子在服务端渲染时不会被调用。
WARNING
不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!
在一个组件实例被卸载之前调用。
类型
interface ComponentOptions {
beforeUnmount?(this: ComponentPublicInstance): void
}
详细信息
当这个钩子被调用时,组件实例依然还保有全部的功能。
这个钩子在服务端渲染时不会被调用。
在一个组件实例被卸载之后调用。
类型
interface ComponentOptions {
unmounted?(this: ComponentPublicInstance): void
}
详细信息
一个组件在以下情况下被视为已卸载:
其所有子组件都已经被卸载。
所有相关的响应式作用 (渲染作用以及 setup()
时创建的计算属性和侦听器) 都已经停止。
可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
这个钩子在服务端渲染时不会被调用。
在捕获了后代组件传递的错误时调用。
类型
interface ComponentOptions {
errorCaptured?(
this: ComponentPublicInstance,
err: unknown,
instance: ComponentPublicInstance | null,
info: string
): boolean | void
}
详细信息
错误可以从以下几个来源中捕获:
setup()
函数这个钩子带有三个实参:错误对象、触发该错误的组件实例,以及一个说明错误来源类型的信息字符串。
TIP
在生产环境中,第三个参数 (info
) 是一个缩短的代码,而不是含有完整信息的字符串。错误代码和字符串的映射可以参阅生产环境错误代码参考。
你可以在 errorCaptured()
中更改组件状态来为用户显示一个错误状态。然而重要的是,不要让错误状态渲染为导致本次错误的内容,否则组件就会进入无限的渲染循环中。
这个钩子可以通过返回 false
来阻止错误继续向上传递。请看下方的传递细节介绍。
错误传递规则
默认情况下,所有的错误都会被发送到应用级的 app.config.errorHandler
(前提是这个函数已经定义),这样这些错误都能在一个统一的地方报告给分析服务。
如果组件的继承链或组件链上存在多个 errorCaptured
钩子,对于同一个错误,这些钩子会被按从底至上的顺序一一调用。这个过程被称为“向上传递”,类似于原生 DOM 事件的冒泡机制。
如果 errorCaptured
钩子本身抛出了一个错误,那么这个错误和原来捕获到的错误都将被发送到 app.config.errorHandler
。
errorCaptured
钩子可以通过返回 false
来阻止错误继续向上传递。即表示“这个错误已经被处理了,应当被忽略”,它将阻止其他的 errorCaptured
钩子或 app.config.errorHandler
因这个错误而被调用。
在一个响应式依赖被组件的渲染作用追踪后调用。
这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用。
类型
interface ComponentOptions {
renderTracked?(this: ComponentPublicInstance, e: DebuggerEvent): void
}
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TrackOpTypes /* 'get' | 'has' | 'iterate' */
key: any
}
参考深入响应式系统
在一个响应式依赖被组件触发了重新渲染之后调用。
这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用。
类型
interface ComponentOptions {
renderTriggered?(this: ComponentPublicInstance, e: DebuggerEvent): void
}
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
参考深入响应式系统
若组件实例是 <KeepAlive>
缓存树的一部分,当组件被插入到 DOM 中时调用。
这个钩子在服务端渲染时不会被调用。
类型
interface ComponentOptions {
activated?(this: ComponentPublicInstance): void
}
若组件实例是 <KeepAlive>
缓存树的一部分,当组件从 DOM 中被移除时调用。
这个钩子在服务端渲染时不会被调用。
类型
interface ComponentOptions {
deactivated?(this: ComponentPublicInstance): void
}
当组件实例在服务器上被渲染之前要完成的异步函数。
类型
interface ComponentOptions {
serverPrefetch?(this: ComponentPublicInstance): Promise<any>
}
详细信息
如果这个钩子返回了一个 Promise,服务端渲染会在渲染该组件前等待该 Promise 完成。
这个钩子仅会在服务端渲染中执行,可以用于执行一些仅在服务端才有的数据抓取过程。
示例
export default {
data() {
return {
data: null
}
},
async serverPrefetch() {
// 组件会作为初次请求的一部分被渲染
// 会在服务端预抓取数据,因为这比客户端更快
this.data = await fetchOnServer(/* ... */)
},
async mounted() {
if (!this.data) {
// 如果数据在挂载时是 null,这意味着这个组件
// 是在客户端动态渲染的,请另外执行一个
// 客户端请求作为替代
this.data = await fetchOnClient(/* ... */)
}
}
}
参考服务端渲染
Vue 组合式 API
组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它涵盖了以下方面的 API:
响应式 API:例如 ref()
和 reactive()
,使我们可以直接创建响应式状态、计算属性和侦听器。
生命周期钩子:例如 onMounted()
和 onUnmounted()
,使我们可以在组件各个生命周期阶段添加逻辑。
依赖注入:例如 provide()
和 inject()
,使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。
组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api
。在 Vue 3 中,组合式 API 基本上都会配合 <script setup>
语法在单文件组件中使用。下面是一个使用组合式 API 的组件示例:
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 更改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
Vue异步组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
如你所见,defineAsyncComponent
方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve
回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason)
表明加载失败。
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
最后得到的 AsyncComp
是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
与普通组件一样,异步组件可以使用 app.component()
全局注册:
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>
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent()
也支持在高级选项中处理这些状态:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。