vue script setup 已经官宣定稿。本文主要翻译了来自 0040-script-setup 的内容。

摘要

在单文件组件(SFC)中引入一个新的 <script> 类型 setup。它向模板公开了所有的顶层绑定。

基础示例

  1. <script setup>
  2. //imported components are also directly usable in template
  3. import Foo from './Foo.vue'
  4. import { ref } from 'vue'
  5. //write Composition API code just like in a normal setup ()
  6. //but no need to manually return everything
  7. const count = ref(0)
  8. const inc = () => {
  9. count.value++
  10. }
  11. </script>
  12. <template>
  13. <Foo :count="count" @click="inc" />
  14. </template>
编译输出
  1. import Foo from './Foo.vue'
  2. import { ref } from 'vue'
  3. export default {
  4. setup() {
  5. const count = ref(1)
  6. const inc = () => {
  7. count.value++
  8. }
  9. return function render() {
  10. return h(Foo, {
  11. count,
  12. onClick: inc,
  13. })
  14. }
  15. },
  16. }

声明 Props 和 Emits

  1. <script setup>
  2. //expects props options
  3. const props = defineProps({
  4. foo: String,
  5. })
  6. //expects emits options
  7. const emit = defineEmits(['update', 'delete'])
  8. </script>

动机

这个提案的主要目标是通过直接向模板公开 <script setup> 的上下文,减少在单文件组件(SFC)中使用 Composition API 的繁琐程度。

之前有一个关于 <script setup> 的提案 这里,目前已经实现(但被标记为实验性)。旧的提议选择了导出语法,这样代码就能与未使用的变量配合得很好。

这个提议采取了一个不同的方向,基于我们可以在 eslint-plugin-vue 中提供定制的 linter 规则的前提下。这使我们能够以最简洁的语法为目标。

设计细节

使用 script setup 语法

要使用 script setup 语法,直接在 script 标签中加入 setup 就可以了

  1. <script setup>
  2. //syntax enabled
  3. </script>

暴露顶级绑定

当使用 <script setup> 时,模板被编译成一个渲染函数,被内联在 setup 函数 scope 内。这意味着任何在 <script setup> 中声明的顶级绑定(top level bindings)(包括变量和导入)都可以直接在模板中使用。

  1. <script setup>
  2. const msg = 'Hello!'
  3. </script>
  4. <template>
  5. <div>{{ msg }}</div>
  6. </template>
编译输出
  1. export default {
  2. setup() {
  3. const msg = 'Hello!'
  4. return function render() {
  5. //has access to everything inside setup () scope
  6. return h('div', msg)
  7. }
  8. },
  9. }

注意到模板范围的心智模型与 Options API 的不同是很重要的:当使用 Options API 时,<script> 和模板是通过一个 “渲染上下文对象” 连接的。当我们写代码时,我们总是在考虑 “在上下文中暴露哪些属性”。这自然会导致对 “在上下文中泄露太多的私有逻辑” 的担忧。

然而,当使用 <script setup> 时,心理模型只是一个函数在另一个函数内的模型:内部函数可以访问父范围内的所有东西,而且因为父范围是封闭的,所以没有 "泄漏" 的问题。

使用组件

<script setup> 范围内的值也可以直接用作自定义组件标签名,类似于 JSX 中的工作方式。

  1. <script setup>
  2. import Foo from './Foo.vue'
  3. import MyComponent from './MyComponent.vue'
  4. </script>
  5. <template>
  6. <Foo />
  7. <!-- kebab-case also works -->
  8. <my-component />
  9. </template>
编译输出
  1. import Foo from './Foo.vue'
  2. import MyComponent from './MyComponent.vue'
  3. export default {
  4. setup() {
  5. return function render() {
  6. return [h(Foo), h(MyComponent)]
  7. }
  8. },
  9. }

使用动态组件

  1. <script setup>
  2. import Foo from './Foo.vue'
  3. import Bar from './Bar.vue'
  4. </script>
  5. <template>
  6. <component :is="Foo" />
  7. <component :is="someCondition ? Foo : Bar" />
  8. </template>
编译输出
  1. import Foo from './Foo.vue'
  2. import Bar from './Bar.vue'
  3. export default {
  4. setup() {
  5. return function render() {
  6. return [h(Foo), h(someCondition ? Foo : Bar)]
  7. }
  8. },
  9. }

使用指令

除了一个名为 v-my-dir 的指令会映射到一个名为 vMyDir 的 setup 作用域变量,指令的工作方式与此类似:

  1. <script setup>
  2. import { directive as vClickOutside } from 'v-click-outside'
  3. </script>
  4. <template>
  5. <div v-click-outside />
  6. </template>
编译输出
  1. import { directive as vClickOutside } from 'v-click-outside'
  2. export default {
  3. setup() {
  4. return function render() {
  5. return withDirectives(h('div'), [[vClickOutside]])
  6. }
  7. },
  8. }

之所以需要 v 前缀,是因为全局注册的指令(如 v-focus)很可能与本地声明的同名变量发生冲突。v 前缀使得使用一个变量作为指令的意图更加明确,并减少了意外的 “shadowing”。

声明 props 和 emits

为了在完全的类型推导支持下声明 props 和 emits 等选项,我们可以使用 defineProps 和 defineEmits API,它们在 <script setup> 中自动可用,无需导入。

  1. <script setup>
  2. const props = defineProps({
  3. foo: String,
  4. })
  5. const emit = defineEmits(['change', 'delete'])
  6. //setup code
  7. </script>
编译输出
  1. export default {
  2. props: {
  3. foo: String,
  4. },
  5. emits: ['change', 'delete'],
  6. setup(props, { emit }) {
  7. //setup code
  8. },
  9. }
  • defineProps 和 defineEmits 根据传递的选项提供正确的类型推理。
  • defineProps 和 defineEmits 是编译器宏(compiler macros ),只能在 <script setup> 中使用。它们不需要被导入,并且在处理 <script setup> 时被编译掉。
  • 传递给 defineProps 和 defineEmits 的选项将被从 setup 中提升到模块范围。因此,这些选项不能引用在 setup 作用域内声明的局部变量。这样做会导致一个编译错误。然而,它可以引用导入的绑定,因为它们也在模块范围内。

使用 slots 和 attrs

<script setup> 中使用 slots 和 attrs 应该是比较少的,因为你可以在模板中直接访问它们,如 $slots 和 $attrs。在罕见的情况下,如果你确实需要它们,请分别使用 useSlots 和 useAttrs 帮助函数(helpers)。

  1. <script setup>
  2. import { useSlots, useAttrs } from 'vue'
  3. const slots = useSlots()
  4. const attrs = useAttrs()
  5. </script>

useSlots 和 useAttrs 是实际的运行时函数,其返回值等价于 setupContext.slotssetupContext.attrs。它们也可以在 Composition API 函数中使用。

纯类型的 props 或 emit 声明

props 和 emits 也可以使用 TypeScript 语法来声明,方法是向 defineProps 或 defineEmits 传递一个字面类型参数。

  1. const props = defineProps<{
  2. foo: string
  3. bar?: number
  4. }>()
  5. const emit = defineEmits<{
  6. (e: 'change', id: number): void
  7. (e: 'update', value: string): void
  8. }>()
  • defineProps 或 defineEmits 只能使用运行时声明或类型声明。同时使用两者会导致编译错误。
  • 当使用类型声明时,等效的运行时声明会从静态分析中自动生成,以消除双重声明的需要,并仍然确保正确的运行时行为。
    • 在 dev 模式下,编译器将尝试从类型中推断出相应的运行时验证。例如这里的 foo: String 是由 foo: string 类型推断出来的。如果该类型是对导入类型的引用,推断的结果将是 foo: null (等于 any 类型),因为编译器没有外部文件的信息。
    • 在 prod 模式下,编译器将生成数组格式声明以减少包的大小(这里的 props 将被编译成 ['msg'])。
    • 发出的代码仍然是具有有效类型的 TypeScript,它可以被其他工具进一步处理。
  • 截至目前,类型声明参数必须是以下之一,以确保正确的静态分析。
    • 一个类型的字面意义
    • 对同一文件中的接口或类型字的引用

目前不支持复杂类型和从其他文件导入的类型。理论上,将来有可能支持类型导入。

使用类型声明时的默认 props

纯类型的 defineProps 声明的一个缺点是,它没有办法为 props 提供默认值。为了解决这个问题,提供了一个 withDefaults 编译器宏(compiler macros )。

  1. interface Props {
  2. msg?: string
  3. }
  4. const props = withDefaults(defineProps<Props>(), {
  5. msg: 'hello',
  6. })

这将被编译成等效的运行时 props 默认选项。此外,withDefaults 为默认值提供了类型检查,并确保返回的 props 类型对于那些确实有默认值声明的属性来说已经删除了可选标志(optional flags)。

顶级作用域 await

顶层的 await 可以直接在 <script setup> 里面使用。由此产生的 setup () 函数将自动添加 async:

  1. <script setup>
  2. const post = await fetch(`/api/post/1`).then(r => r.json())
  3. </script>

此外,添加 await 的表达式将被自动编译成一种格式,保留了当前组件实例上下文:

编译输出
  1. import { withAsyncContext as _withAsyncContext } from 'vue'
  2. const __sfc__ = {
  3. async setup(__props) {
  4. let __temp, __restore
  5. const post =
  6. (([__temp, __restore] = _withAsyncContext(() =>
  7. fetch(`/api/post/1`).then(r => r.json())
  8. )),
  9. (__temp = await __temp),
  10. __restore(),
  11. __temp)
  12. return () => {}
  13. },
  14. }
  15. __sfc__.__file = 'App.vue'
  16. export default __sfc__

暴露组件的公共接口

在传统的 Vue 组件中,所有暴露在模板上的东西都隐含地暴露在组件实例上,可以被父组件通过模板引用检索到。也就是说,在这一点上,模板的渲染上下文和组件的公共接口(public interface)是一样的。这是有问题的,因为这两个用例并不总是完全一致。事实上,大多数时候,在公共接口方面是过度暴露的。这就是为什么要在 Expose RFC 中讨论一种明确的方式来定义一个组件的强制性公共接口。

通过 <script setup> 模板可以访问声明的变量,因为它被编译成一个函数,从 setup () 函数范围返回。这意味着所有声明的变量实际上都不会被返回:它们被包含在 setup () 的闭包中。因此,一个使用 <script setup> 的组件将被默认关闭。也就是说,它的公共的强制性性接口将是一个空对象,除非绑定被明确地暴露出来。

要在 <script setup> 组件中明确地暴露属性,可以使用 defineExpose 编译器宏(compiler macro)。

  1. <script setup>
  2. const a = 1
  3. const b = ref(2)
  4. defineExpose({
  5. a,
  6. b,
  7. })
  8. </script>
编译输出
  1. import { defineComponent as _defineComponent } from 'vue'
  2. const __sfc__ = _defineComponent({
  3. setup(__props, { expose }) {
  4. const a = 1
  5. const b = ref(2)
  6. expose({
  7. a,
  8. b,
  9. })
  10. return () => {}
  11. },
  12. })
  13. __sfc__.__file = 'App.vue'
  14. export default __sfc__

当父组件通过模板 refs 获得这个组件的实例时,检索到的实例将是 { a: number, b: number } 的样子。(refs 会像普通实例一样自动解包)。通过编译成的内容和 Expose RFC 中建议的运行时等价。

与普通 script 一起使用

有一些情况下,代码必须在模块范围内执行,例如:。

  • 声明命名的出口
  • 只应执行一次的全局副作用。

在这两种情况下,普通的 <script> 块可以和 <script setup> 一起使用。

  1. <script>
  2. performGlobalSideEffect()
  3. //this can be imported as `import { named } from './*.vue'`
  4. export const named = 1
  5. </script>
  6. <script setup>
  7. let count = 0
  8. </script>
Compile Output
  1. import { ref } from 'vue'
  2. performGlobalSideEffect()
  3. export const named = 1
  4. export default {
  5. setup() {
  6. const count = ref(0)
  7. return {
  8. count,
  9. }
  10. },
  11. }

name 的自动推导

Vue 3 SFC 在以下情况下会自动从组件的文件名推断出组件的 name。

  • 开发警告格式化
  • DevTools 检查
  • 递归的自我引用。例如,一个名为 FooBar.vue 的文件可以在其模板中引用自己为 <FooBar/>

这比明确的注册 / 导入的组件的优先级低。如果你有一个命名的导入与组件的推断名称相冲突,你可以给它取别名。

  1. import { FooBar as FooBarChild } from './components'

在大多数情况下,不需要明确的 name 声明。唯一需要的情况是当你需要 <keep-alive> 包含或排除或直接检查组件的选项时,你需要这个名字。

声明额外的选项

<script setup> 语法提供了表达大多数现有 Options API 选项同等功能的能力,只有少数选项除外。

  • name
  • inheritAttrs
  • 插件或库所需的自定义选项

如果你需要声明这些选项,请使用单独的普通 <script> 块,并使用导出默认值。

  1. <script>
  2. export default {
  3. name: 'CustomName',
  4. inheritAttrs: false,
  5. customOptions: {},
  6. }
  7. </script>
  8. <script setup>
  9. //script setup logic
  10. </script>

使用限制

由于模块执行语义的不同,<script setup> 内的代码依赖于 SFC 的上下文。当移入外部的.js 或.ts 文件时,可能会导致开发人员和工具的混淆。因此,<script setup> 不能与 src 属性一起使用。

缺陷

工具的兼容性

这种新的范围模型将需要在两个方面进行工具调整。

  • 集成开发环境需要为这个新的 <script setup> 模型提供专门的处理,以便提供模板表达式类型检查 / 道具验证等。

    截至目前,Volar 已经在 VSCode 中提供了对这个 RFC 的全面支持,包括所有 TypeScript 相关的功能。它的内部结构也被实现为一个语言服务器,理论上可以在其他 IDE 中使用。

  • ESLint 规则如 no-unused-vars。我们在 eslint-plugin-vue 中需要一个替换规则,将 <script setup><template> 表达式都考虑在内。

采用策略

<script setup> 是可选的。现有的 SFC 使用不受影响。

未解决的问题

  • 纯类型的 props/emits 声明目前不支持使用外部导入的类型。这在跨多个组件重复使用基本道具类型定义时非常有用。

在 Volar 的支持下,类型推理已经可以正常工作了,限制纯粹在于 @vue/compiler-sfc 需要知道 props 的键值,以便生成正确的等效运行时声明。

这在技术上是可行的,如果我们实现了跟踪类型导入、读取和解析导入 source 的逻辑。然而,这更像是一个实现范围的问题,并不从根本上影响 RFC 设计的行为方式。

附录

下面的章节仅适用于需要在各自的 SFC 工具集成中支持 <script setup> 的工具作者。

Transform API

@vue/compiler-sfc 包暴露了用于处理 <script setup> 的 compileScript 方法。

  1. import { parse, compileScript } from '@vue/compiler-sfc'
  2. const descriptor = parse(`...`)
  3. if (descriptor.script || descriptor.scriptSetup) {
  4. const result = compileScript(descriptor) //returns SFCScriptBlock
  5. console.log(result.code)
  6. console.log(result.bindings) //see next section
  7. }

编译时需要提供整个描述符(the entire descriptor ),产生的代码将包括来自 <script setup> 和普通 <script>(如果存在)中的代码。上层工具(如 vite 或 vue-loader)有责任对编译后的输出进行正确组装。

内联与非内联模式

在开发过程中,<script setup> 仍然编译为返回的对象,而不是内联渲染函数,原因有二:

  • Devtools 检查
  • 模板热重载(HMR)

内联模板模式只在生产中使用,可以通过 inlineTemplate 选项启用。

  1. compileScript(descriptor, { inlineTemplate: true })

在内联模式下,一些绑定(例如来自 ref () 调用的返回值)需要用 unref 进行包装。

  1. export default {
  2. setup() {
  3. const msg = ref('hello')
  4. return function render() {
  5. return h('div', unref(msg))
  6. }
  7. },
  8. }

编译器会执行一些启发式方法来尽可能地避免这种情况。例如,带有字面初始值的函数声明和常量声明将不会被 unref 包裹。

模板绑定优化

由 compiledScript 返回的 SFCScriptBlock 也暴露了一个 bindings 对象,这是在编译期间收集的导出的绑定元数据。例如,给定以下 <script setup>

  1. <script setup="props">
  2. export const foo = 1
  3. export default {
  4. props: ['bar'],
  5. }
  6. </script>

bindings 对象将是。

  1. {
  2. foo: 'setup-const',
  3. bar: 'props'
  4. }

然后这个对象可以被传递给模板编译器。

  1. import { compile } from '@vue/compiler-dom'
  2. compile(template, {
  3. bindingMetadata: bindings,
  4. })

有了可用的绑定元数据,模板编译器可以生成代码,直接从相应的源码访问模板变量,而不必通过渲染上下文代理。

  1. <div>{{ foo + bar }}</div>
  1. //code generated without bindingMetadata
  2. //here _ctx is a Proxy object that dynamically dispatches property access
  3. function render(_ctx) {
  4. return createVNode('div', null, _ctx.foo + _ctx.bar)
  5. }
  6. //code generated with bindingMetadata
  7. //bypasses the render context proxy
  8. function render(_ctx, _cache, $setup, $props, $data) {
  9. return createVNode('div', null, $setup.foo + $props.bar)
  10. }

绑定信息也被用于内联模板模式,以生成更有效的代码。

实践

最近,我使用 script setup 语法构建了一个应用 TinyTab —— 一个专注于搜索的新标签页浏览器插件。

  • 多语言
  • 切换主题风格
  • 自定义背景图
  • 在深色和浅色模式之间切换或跟随系统设置
  • 自定义搜索后缀(过滤规则等)
  • 设置任何你想要的搜索引擎为默认
  • 几个开箱即用的引擎集成
  • 通过自定义前缀快速切换搜索引擎
  • 自定义任何支持 OpenSearch 的搜索引擎
  • 导出与导入任何配置细节

如果你有兴趣了解 vue3 script setup 语法,或许可以查看它。

参考资料

vue3 script setup 定稿的更多相关文章

  1. vue3 学习笔记(九)——script setup 语法糖用了才知道有多爽

    刚开始使用 script setup 语法糖的时候,编辑器会提示这是一个实验属性,要使用的话,需要固定 vue 版本. 在 6 月底,该提案被正式定稿,在 v3.1.3 的版本上,继续使用但仍会有实验 ...

  2. 一篇文章讲明白vue3的script setup,拥抱组合式API!

    引言 vue3除了Composition API是一个亮点之外,尤大大又给我们带来了一个全新的玩意 -- script setup,对于setup大家相信都不陌生,而对于script setup有些同 ...

  3. Vue3中setup语法糖学习

    目录 1,前言 2,基本语法 2,响应式 3,组件使用 3.1,动态组件 3.2,递归组件 4,自定义指令 5,props 5.1,TypeScript支持 6,emit 6.1,TypeScript ...

  4. 基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结

    随着Vue3和TypeScript的大浪潮不断袭来,越来越多的Vue项目采用了TypeScript的语法来编写代码,而Vue3的JS中的Setup语法糖也越来越广泛的使用,给我们这些以前用弱类型的JS ...

  5. vue3代码setup中this为什么无效

    结论:setup并没有通过各种方式去绑定this 在vue2中,我们可以在optionsApi中调用this来指向当前组件的实例,但是在vue3的setup中并不能这样做,因为setup位于组件创建成 ...

  6. vue3函数setUp和reactive函数详细讲解

    1 setUp的执行时机 我们都知道,现在vue3是可以正常去使用methods的. 但是我们却不可以在setUp中去调用methods中的方法. 为什么了??? 我们先了解一下下面这两个生命周期函数 ...

  7. 熬夜总结vue3中setUp函数的2个参数详解

    1.setUp函数的第1个参数props setup(props,context){} 第一个参数props: props是一个对象,包含父组件传递给子组件的所有数据. 在子组件中使用props进行接 ...

  8. vue3 学习笔记 (五)——vue3 的 setup 如何实现响应式功能?

    setup 是用来写组合式 api ,内部的数据和方法需要通过 return 之后,模板才能使用.在之前 vue2 中,data 返回的数据,可以直接进行双向绑定使用,如果我们把 setup 中数据类 ...

  9. Vue3 中有哪些值得深究的知识点?

    众所周知,前端技术一直更新很快,这不 vue3 也问世这么久了,今天就来给大家分享下vue3中值得注意的知识点.喜欢的话建议收藏,点个关注! 1.createApp vue2 和 vue3 在创建实例 ...

随机推荐

  1. unity lua require dofile loadfile 区别

    oadfile,加载文件,编译文件,并且返回一个函数,不运行 dofile其实就是包装了Loadfile,根据loadfile的返回函数运行一遍 require加载文件的时候,不用带目录,有lua自己 ...

  2. 关于unity贴图压缩

    unity官方 https://docs.unity3d.com/Manual/class-TextureImporterOverride.html //后续填充内容

  3. VMware vRealize Suite 8.4 发布 - 多云环境的云计算管理解决方案

    VMware vRealize Suite 8.4.0, Release Date: 2021-04-15 概述 VMware vRealize Suite 是一种多云环境的云计算管理解决方案,为 I ...

  4. Django(41)详解异步任务框架Celery

    celery介绍   Celery是由Python开发.简单.灵活.可靠的分布式任务队列,是一个处理异步任务的框架,其本质是生产者消费者模型,生产者发送任务到消息队列,消费者负责处理任务.Celery ...

  5. lms框架应用服务接口和服务条目详解

    目录 应用接口的定义 服务路由特性 服务条目 根据服务条目生成webAPI 服务条目的治理特性 缓存拦截 服务条目的例子 应用接口的实现 开源地址与文档 应用接口的定义 服务应用接口是微服务定义web ...

  6. ResNet网络的训练和预测

    ResNet网络的训练和预测 简介 Introduction 图像分类与CNN 图像分类 是指将图像信息中所反映的不同特征,把不同类别的目标区分开来的图像处理方法,是计算机视觉中其他任务,比如目标检测 ...

  7. YOLOv5目标检测源码重磅发布了!

    YOLOv5目标检测源码重磅发布了! https://github.com/ultralytics/yolov5 该存储库代表了对未来对象检测方法的超解析开源研究,并结合了在使用之前的YOLO存储库在 ...

  8. 编译原理-翻译程序(Translator)

    分为编译程序(compiler)和解释程序(interpreter) 编译程序:把源程序(高级语言编写)转换成目标程序(汇编语言或机器语言编写). 解释程序:对源程序边翻译边执行. 编译型语言 优点: ...

  9. Python 5种方法实现单例模式

    基本介绍 一个对象只允许被一次创建,一个类只能创建一个对象,并且提供一个全局访问点. 单例模式应该是应用最广泛,实现最简单的一种创建型模式. 特点:全局唯一,允许更改 优缺点 优点: 避免对资源的多重 ...

  10. Golang写文件的坑

    Golang写文件一般使用os.OpenFile返回文件指针的Write方法或者WriteString或者WriteAt方法,但是在使用这三个方法时候经常会遇到写入的内容和实际内容有出入,因为这几个函 ...