推荐阅读:
官方网站:
<脚本设置> |维.js (vuejs.org)
https://vuejs.org/api/sfc-script-setup.html
介绍
Vue 3 引入了 Composition API 作为一种在 Vue 应用程序中处理反应状态的新方法。您可以按功能(用户、API、表单)对代码进行分组,而不是按功能(数据、计算、方法、监视等)组织代码。这在构建 Vue 应用程序时允许更大的灵活性。我们已经在其他文章中讨论过 Composition(如果您还没有阅读,请查看它们!),但是随着 Vue 3.2 的发布,另一个与 Composition 相关的功能已经稳定发布 –
<script setup>
。
简而言之,
<script setup>
允许开发人员定义组件而无需从 JavaScript 块中导出任何内容 – 只需定义变量并在模板中使用它们!这种编写组件的风格在很多方面都类似于 Svelte,对于第一次接触 Vue 的人来说,这是一个巨大的改进。
<script lang=”ts” setup>
<script setup>
是一种编译时语法糖,用于在单文件组件 (SFC) 中使用组合 API。如果同时使用 SFC 和组合 API,则这是建议的语法。与正常语法相比,它提供了许多优点:
<script>
-
更简洁的代码,更少的样板
-
能够使用纯 TypeScript 声明 props 和发出的事件
-
更好的运行时性能(模板编译为同一作用域中的渲染函数,无需中间代理)
-
更好的 IDE 类型推断性能(语言服务器从代码中提取类型的工作量更少)
<script setup>
基本
<script setup>
让我们看一个例子。如果您使用的是 Options API(Vue 2 的标准),那么您的所有单文件组件将如下所示:
<template>
<div>Hello, {{ name }}!</div>
<input v-model="name" />
<button :disabled="!isNamePresent" @click="submitName">Submit</button>
</template>
<script>
export default {
data() {
return {
name: ''
}
},
computed: {
isNamePresent() {
return this.name.length > 0
}
},
methods: {
submitName() {
console.log(this.name)
}
}
}
</script>
我们有我们的模板(一个简单的表单)和我们的脚本块。在脚本块中,我们导出一个具有三个键的对象:名称、计算和方法。如果你熟悉 Vue,这对你来说应该很熟悉。现在,让我们切换此代码以使用 Composition API。
<template>
<div>Hello, {{ name }}!</div>
<input v-model="name" />
<button :disabled="!isNamePresent" @click="submitName">Submit</button>
</template>
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const name = ref('')
const isNamePresent = computed(() => name.value.length > 0)
function submitName() {
console.log(name.value)
}
return {
name,
isNamePresent,
submitName
}
}
}
</script>
我们的组件做的事情和以前完全一样。我们定义了状态(名称)、计算属性(isNamePresent)和提交函数。如果其中任何一个不熟悉,请查看我之前关于 Vue Composition API 的文章。我们不必在要导出的对象中搭建我们的应用程序,而是可以根据需要自由定义变量。这种灵活性还允许我们根据需要从组件中提取重复的逻辑,但在这种情况下,我们的组件非常简单。
但是,我们仍然有那个尴尬的
export default
说法。我们的代码都存在于 setup 函数中,而其余的实际上只是样板文件。我们不能直接删除它吗?事实上,我们现在可以!这就是
<script setup>
进来的地方。让我们切换到使用脚本设置而不是标准脚本块。
<template>
<div>Hello, {{ name }}!</div>
<input v-model="name" />
<button :disabled="!isNamePresent" @click="submitName">Submit</button>
</template>
<script setup>
import { ref, computed } from 'vue'
const name = ref('')
const isNamePresent = computed(() => name.value.length > 0)
function submitName() {
console.log(name.value)
}
</script>
让我们来看看这里发生了什么变化。首先,我们在脚本标签中添加了“setup”这个词,这为编写 Vue 组件启用了这种新模式。其次,我们从
setup
函数中获取我们的代码,并仅用我们的代码替换我们现有的导出对象。一切都按预期工作!
请注意,在脚本标签中声明的所有内容都在组件的模板中可用。这包括非反应性变量或常量,以及实用函数或其他库。这样做的主要好处是您不需要手动将外部文件(例如,Constants.js)绑定为组件的值 – Vue 现在会为您处理这个问题。
基本语法
#
要选择加入语法,请将该属性添加到块中:
setup
<script>
<script setup>
console.log('hello script setup')
</script>
内部代码被编译为组件函数的内容。这意味着与正常不同,普通仅在首次导入组件时执行一次,
每次创建组件的实例时
,内部代码都会执行。
setup()
<script>
<script setup>
顶级绑定向模板公开
#
使用 时,其中声明的任何顶级绑定(包括变量、函数声明和导入)都可以直接在模板中使用:
<script setup>
<script setup>
<script setup>
// variable
const msg = 'Hello!'
// functions
function log() {
console.log(msg)
}
</script>
<template>
<div @click="log">{{ msg }}</div>
</template>
进口以同样的方式公开。这意味着您可以在模板表达式中直接使用导入的帮助程序函数,而不必通过以下选项公开它:
methods
<script setup>
import { capitalize } from './helpers'
</script>
<template>
<div>{{ capitalize('hello') }}</div>
</template>
Reactivity
需要使用
Reactivity API
显式创建响应式状态。与从函数返回的值类似,在模板中引用 ref 时会自动解开包装:
setup()
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
定义组件
使用时
<script setup>
,我们不必再手动定义我们导入的组件了。通过将组件导入文件,编译器能够自动将其添加到我们的应用程序中。让我们通过将表单抽象为它自己的组件来更新我们的组件。我们将它命名为 Form.vue。现在,它只是模板,我们稍后会讨论逻辑。
<!-- Form.vue -->
<template>
<form @submit.prevent="submitHandler">
<label>Name
<input type="text" />
</label>
<button>Submit</button>
</form>
</template>
<script setup>
function submitHandler() {
// Do something
}
</script>
<!-- App.vue -->
<template>
<div>Hello, {{ name }}!</div>
<Form />
</template>
<script setup>
import { ref } from 'vue'
import Form from './components/Form.vue'
const name = ref('')
function submitForm() {
console.log(name.value)
}
</script>
而已!我们的组件现在必须导入到我们的 Vue 文件中,并且它会自动在我们的模板中可用。不再有
components
块占用我们文件中的空间!
现在,我们需要将
name
子组件作为道具传递给我们。但是等等,我们不能定义道具!我们没有要添加 props 选项的对象!此外,我们需要发出表单已提交,以便我们可以触发我们的提交。我们如何定义子组件发出的内容?
defineProps
和
defineEmits
defineProps
defineEmits
我们仍然可以使用新的辅助方法
defineProps
和
defineEmits
.
来自 Vue 文档
,“
defineProps
并且
defineEmits
是编译器宏,只能在内部使用
<script setup>
。它们不需要导入,在
<script setup>
处理时会被编译掉。” 这些编译时函数采用与标准键与完整导出对象一起使用的相同参数。让我们更新我们的应用程序以使用
defineProps
和
defineEmits
。
<!-- Form.vue -->
<template>
<form @submit.prevent="submitHandler">
<label>Name
<input v-model="name" type="text" />
</label>
<button>Submit</button>
</form>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'submit']);
const name = computed({
get () {
return props.modelValue
},
set(val) {
emit('update:modelValue', val);
}
})
function submitHandler() {
emit('submit')
}
</script>
<!-- App.vue -->
<template>
<div>Hello, {{ name }}!</div>
<Form v-model="name" @submit="submitForm" />
</template>
<script setup>
import { ref } from 'vue'
import Form from './components/Form.vue'
const name = ref('')
function submitForm() {
console.log(name.value)
}
</script>
让我们来看看这里发生了什么变化。
-
首先,我们曾经
defineProps
期望一个 modelValue(在 Vue 3 中与 v-model 一起使用的预期道具)。 -
然后我们用 定义了我们的发射
defineEmits
,这样我们既可以报告这个组件发射的内容,也可以访问该
emit
函数(以前在 `this.$emit 上可用)。 - 接下来,我们创建一个使用自定义 getter 和设置的计算属性。我们这样做是为了可以轻松地在表单输入中使用 v-model,但这不是必需的。getter 返回我们的 prop,setter 将更新事件发送到我们的父组件。
- 最后,我们连接了 submitHandler 函数来发出一个提交事件。
我们的 App.vue 组件或多或少与我们离开它时一样,添加
v-model="name"
了
@submit="submitForm"
Form 子组件。这样,我们的应用程序再次按预期工作!
其它功能
这里有更多功能可供我们使用,但它们在典型应用程序中的用例较少。
-
动态组件
– 由于我们的组件在模板中立即可用,我们可以在编写动态组件时使用它们(
<component :is="Form" />
例如)。 -
命名空间组件
– 如果您有许多从同一文件导入的组件,则可以使用
import * as Form
语法对这些组件进行命名空间。然后,您无需任何额外工作即可访问
<Form.Input>
或。
<Form.Submit>
-
Top-Level Await
– 如果您需要将 API 请求作为组件设置的一部分,您可以在组件的顶层自由使用 async/await 语法 – 无需包装在 async 函数中!请记住,使用它的组件必须由
<Suspense>
组件在外部包裹
要记住的另一点是,您并没有被锁定在使用
<script setup>
. 如果您将这种新语法用于组件并遇到无法完成某事的情况,或者只是想对特定情况使用 Options 语法,您可以通过添加一个额外的
<script>
块来自由地这样做到您的组件。Vue 将为您将两者混合在一起,因此您的合成代码和选项代码可以保持分离。这在使用像 Nuxt 这样的框架时非常有用,这些框架为标准选项语法提供了额外的方法,而
<script setup>
. 有关这方面的一个很好的例子,
请参阅 Vue 文档。
结论
这是 Vue 和 Composition API 向前迈出的一大步。事实上,Evan You 已经公开表示,这是为了成为 Vue 单文件组件未来的标准语法。来自
Github 上的讨论
:
这有一些历史,因为关于 Composition API 的最初提案表明打算用它完全取代 Options API,但遭到了强烈反对。尽管我们确实相信Composition API 从长远来看有潜力成为“前进的道路”,但我们意识到(1)仍有人机工程学/工具/设计问题需要解决,(2)范式转变不能一天之内完成。在我们可以自信地向所有 Vue 用户推荐新的东西之前,我们需要时间和早期采用者来验证、尝试、采用和试验新范式。
这实质上导致了一个“过渡期”,在此期间我们有意避免将 Composition API 声明为“新方式”,以便我们可以执行验证过程并与主动采用它的用户子集一起构建周围的工具/生态系统。
现在
<script setup>
,随着 IDE 工具支持的改进,我们相信 Composition API 已达到为大多数用户提供卓越 DX 和可扩展性的状态。但我们需要时间来达到这一点。
在同一线程的早些时候,Evan 表达了他对 Vue 未来发展的看法:
目前推荐的方法是:
- 使用 SFC
<script setup>
++ 组合 API- 使用 VSCode + Volar(或 WebStorm,一旦它支持
<script setup>
即将发布)- TS 没有严格要求,但如果适用,请使用 Vite 构建工具。
使用组件
#
作用域中的值也可以直接用作自定义组件标记名称:
<script setup>
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
可以将其视为变量引用。如果你使用过JSX,这里的心智模型是相似的。烤肉串大小的等效项也适用于模板 – 但是强烈建议使用PascalCase组件标签以保持一致性。它还有助于与本机自定义元素区分开来。
MyComponent
<my-component>
动态组件
#
由于组件被引用为变量而不是在字符串键下注册,因此在内部使用动态组件时,我们应该使用动态绑定:
:is
<script setup>
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
请注意如何将组件用作三元表达式中的变量。
递归组件
#
车间作业控制可以通过其文件名隐式引用自身。例如,名为 的文件可以像在其模板中一样引用自身。
FooBar.vue
<FooBar/>
请注意,此组件的优先级低于导入的组件。如果命名导入与组件的推断名称冲突,则可以为导入添加别名:
import { FooBar as FooBarChild } from './components'
命名空间组件
#
您可以使用带有点的组件标记来引用嵌套在对象属性下的组件。当您从单个文件导入多个组件时,这很有用:
<Foo.Bar>
<script setup>
import * as Form from './form-components'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
使用自定义指令
#
全局注册的自定义指令正常工作。本地自定义指令不需要显式注册到 ,但它们必须遵循命名方案:
<script setup>
vNameOfDirective
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// do something with the element
}
}
</script>
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>
如果要从其他位置导入指令,可以对其进行重命名以适合所需的命名方案:
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
defineProps() & defineEmits()
#
要声明具有完整类型推理支持和之类的选项,我们可以使用 和 API,它们在 以下部分中自动可用:
props
emits
defineProps
defineEmits
<script setup>
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup code
</script>
-
defineProps
并且是编译器
宏
,只能在 内部使用。它们不需要导入,并且在处理时会被编译掉。
defineEmits
<script setup>
<script setup>
-
defineProps
接受与选项相同的值,同时接受与选项相同的值。
props
defineEmits
emits
-
defineProps
并根据传递的选项提供正确的类型推断。
defineEmits
-
传递给和将从设置中吊出的选项进入模块范围。因此,这些选项不能引用在安装范围中声明的局部变量。这样做将导致编译错误。但是,
它可以
引用导入的绑定,因为它们也在模块范围内。
defineProps
defineEmits
如果您使用的是 TypeScript,则还可以
使用纯类型注释声明 props 和 emits
。
defineExpose()
#
默认情况下,使用的组件是
关闭
的 – 即,通过模板引用或链检索的组件的公共实例
不会
公开其中声明的任何绑定。
<script setup>
$parent
<script setup>
若要显式公开组件中的属性,请使用编译器宏:
<script setup>
defineExpose
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父级通过模板 refs 获取此组件的实例时,检索到的实例将具有该形状(refs 会自动展开,就像在普通实例上一样)。
{ a: number, b: number }
useSlots()
&
useAttrs()
#
useSlots()
useAttrs()
和 内部的使用应该相对较少,因为您可以直接在模板中访问它们。在极少数情况下,您确实需要它们,请分别使用 和 帮助程序:
slots
attrs
<script setup>
$slots
$attrs
useSlots
useAttrs
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
useSlots
并且是实际的运行时函数,返回 等效的 和 。它们也可以用于正常的组合 API 函数。
useAttrs
setupContext.slots
setupContext.attrs
与正常一起使用
<script>
#
<script>
<script setup>
可与普通 一起使用。在我们需要的情况下,可能需要一个正常:
<script>
<script>
-
声明不能用 表示的选项,例如或通过插件启用的自定义选项。
<script setup>
inheritAttrs
-
声明命名导出。
-
运行副作用或创建应仅执行一次的对象。
<script>
// normal <script>, executed in module scope (only once)
runSideEffectOnce()
// declare additional options
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// executed in setup() scope (for each instance)
</script>
顶级
await
#
await
顶级可以在 内部使用。生成的代码将编译为:
await
<script setup>
async setup()
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
此外,等待的表达式将自动编译为的格式,该格式将保留 后面的当前组件实例上下文。
await
注意
async setup()
必须与 结合使用,这目前仍然是一个实验性功能。我们计划在将来的版本中完成并记录它 – 但如果你现在很好奇,你可以参考它的
测试
来看看它是如何工作的。
Suspense
仅键入脚本功能
#
仅键入属性/发出声明
#
道具和发出也可以使用纯类型语法声明,方法是将文本类型参数传递给 或 :
defineProps
defineEmits
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
-
defineProps
或者只能使用运行时声明或类型声明。同时使用两者将导致编译错误。
defineEmits
-
使用类型声明时,等效的运行时声明是从静态分析中自动生成的,以消除对双重声明的需要,同时仍可确保正确的运行时行为。
-
在开发模式下,编译器将尝试从类型推断出相应的运行时验证。例如,这里是从类型推断出来的。如果类型是对导入类型的引用,则推断的结果将是(等于类型),因为编译器没有外部文件的信息。
foo: String
foo: string
foo: null
any
-
在 prod 模式下,编译器将生成数组格式声明以减小捆绑包大小(此处的 props 将编译为
['foo', 'bar']
)
-
发出的代码仍然是具有有效类型的TypeScript,可以通过其他工具进一步处理。
-
-
截至目前,类型声明参数必须是以下参数之一,以确保正确的静态分析:
-
文字类型
-
对同一文件中的接口或类型文本的引用
目前不支持复杂类型和从其他文件导入类型。将来可以支持类型导入。
-
使用类型声明时的默认属性值
#
仅类型声明的一个缺点是它没有办法为 props 提供默认值。若要解决此问题,还提供了一个编译器宏:
defineProps
withDefaults
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
这将编译为等效的运行时属性选项。此外,该帮助程序还为默认值提供类型检查,并确保返回的类型已为声明了默认值的属性删除了可选标志。
default
withDefaults
props
限制
#
由于模块执行语义的差异,内部代码依赖于 SFC 的上下文。当移动到外部或文件中时,可能会导致开发人员和工具的混淆。因此,
<脚本设置>
不能与属性一起使用。
<script setup>
.js
.ts
src