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

摘要

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

基础示例

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

声明 Props 和 Emits

<script setup>
//expects props options
const props = defineProps({
foo: String,
})
//expects emits options
const emit = defineEmits(['update', 'delete'])
</script>

动机

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

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

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

设计细节

使用 script setup 语法

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

<script setup>
//syntax enabled
</script>

暴露顶级绑定

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

<script setup>
const msg = 'Hello!'
</script> <template>
<div>{{ msg }}</div>
</template>
编译输出
export default {
setup() {
const msg = 'Hello!' return function render() {
//has access to everything inside setup () scope
return h('div', msg)
}
},
}

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

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

使用组件

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

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

使用动态组件

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

使用指令

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

<script setup>
import { directive as vClickOutside } from 'v-click-outside'
</script> <template>
<div v-click-outside />
</template>
编译输出
import { directive as vClickOutside } from 'v-click-outside'

export default {
setup() {
return function render() {
return withDirectives(h('div'), [[vClickOutside]])
}
},
}

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

声明 props 和 emits

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

<script setup>
const props = defineProps({
foo: String,
}) const emit = defineEmits(['change', 'delete'])
//setup code
</script>
编译输出
export default {
props: {
foo: String,
},
emits: ['change', 'delete'],
setup(props, { emit }) {
//setup code
},
}
  • 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)。

<script setup>
import { useSlots, useAttrs } from 'vue' const slots = useSlots()
const attrs = useAttrs()
</script>

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

纯类型的 props 或 emit 声明

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

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

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

使用类型声明时的默认 props

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

interface Props {
msg?: string
} const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
})

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

顶级作用域 await

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

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

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

编译输出
import { withAsyncContext as _withAsyncContext } from 'vue'

const __sfc__ = {
async setup(__props) {
let __temp, __restore const post =
(([__temp, __restore] = _withAsyncContext(() =>
fetch(`/api/post/1`).then(r => r.json())
)),
(__temp = await __temp),
__restore(),
__temp) return () => {}
},
}
__sfc__.__file = 'App.vue'
export default __sfc__

暴露组件的公共接口

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

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

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

<script setup>
const a = 1
const b = ref(2) defineExpose({
a,
b,
})
</script>
编译输出
import { defineComponent as _defineComponent } from 'vue'

const __sfc__ = _defineComponent({
setup(__props, { expose }) {
const a = 1
const b = ref(2) expose({
a,
b,
}) return () => {}
},
})
__sfc__.__file = 'App.vue'
export default __sfc__

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

与普通 script 一起使用

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

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

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

<script>
performGlobalSideEffect() //this can be imported as `import { named } from './*.vue'`
export const named = 1
</script> <script setup>
let count = 0
</script>
Compile Output
import { ref } from 'vue'

performGlobalSideEffect()

export const named = 1

export default {
setup() {
const count = ref(0)
return {
count,
}
},
}

name 的自动推导

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

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

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

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

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

声明额外的选项

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

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

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

<script>
export default {
name: 'CustomName',
inheritAttrs: false,
customOptions: {},
}
</script> <script setup>
//script setup logic
</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 方法。

import { parse, compileScript } from '@vue/compiler-sfc'

const descriptor = parse(`...`)

if (descriptor.script || descriptor.scriptSetup) {
const result = compileScript(descriptor) //returns SFCScriptBlock
console.log(result.code)
console.log(result.bindings) //see next section
}

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

内联与非内联模式

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

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

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

compileScript(descriptor, { inlineTemplate: true })

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

export default {
setup() {
const msg = ref('hello') return function render() {
return h('div', unref(msg))
}
},
}

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

模板绑定优化

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

<script setup="props">
export const foo = 1 export default {
props: ['bar'],
}
</script>

bindings 对象将是。

{
foo: 'setup-const',
bar: 'props'
}

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

import { compile } from '@vue/compiler-dom'

compile(template, {
bindingMetadata: bindings,
})

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

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

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

实践

最近,我使用 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. Windows 下QT程序发布

    方法1:利用QT自带打包工具 1.新建文件夹,把编译好的exe文件放入该文件夹 2.使用QT下的命令行 3.进入该exe所在文件,执行windeployqt xxx.exe,若出现找不到命令的情况 4 ...

  2. Could not get JDBC Connection排查

    最近在维护的一个比较旧的项目,发现总是隔一段时间JDBC就报错: Could not get JDBC Connection; nested exception is org.apache.commo ...

  3. [leetcode] 30. 与所有单词相关联的字串(cn第653位做出此题的人~)

    30. 与所有单词相关联的字串 这个题做了大概两个小时左右把...严重怀疑leetcode的judge机器有问题.同样的代码交出来不同的运行时长,能不能A题还得看运气? 大致思路是,给words生成一 ...

  4. Linux 挂载盘

    在192.168.6.203上,挂接第二块硬盘 fdisk -l 1.fdisk /dev/vdb 命令(输入 m 获取帮助):n Partition type: p primary (0 prima ...

  5. Python+Selenium - 三种等待方式

    元素:存在 > 可见 > 可用 需要判断元素状态 等待方式1:强制等待  -- 辅助 设置等待几秒,就必须等待几秒 示例: from time import sleepsleep(3) 强 ...

  6. synchronized使用及java中的原子性问题

    1.Synchronized关键字使用 class X { // 修饰非静态方法 synchronized void foo() { // 临界区 } // 修饰静态方法 synchronized s ...

  7. Mobileye 自动驾驶策略(二)

    Mobileye 自动驾驶策略(二) 与多方都成功进行了合作,其中比较大型的合作包括法雷奥.百度和中国 ITS. 法雷奥是最近的的 Tier 1 合作伙伴,法雷奥和 Mobileye 签署协议,表示未 ...

  8. 适用于CUDA GPU的Numba 随机数生成

    适用于CUDA GPU的Numba 随机数生成 随机数生成 Numba提供了可以在GPU上执行的随机数生成算法.由于NVIDIA如何实现cuRAND的技术问题,Numba的GPU随机数生成器并非基于c ...

  9. 如何在TVM上集成Codegen(下)

    如何在TVM上集成Codegen(下) Bring DNNL to TVM: JSON Codegen/Runtime 现在实现将中继图序列化为JSON表示的DNNL codegen,然后实现DNNL ...

  10. python_selenium_PO模式下显示等待、隐式等待封装,结合Excel读取元素可取默认等待时间配置

    basepage中等待的封装 def implicitly_wait(self): self.driver.implicitly_wait(5)def wait(self): time.sleep(5 ...