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.slots
和 setupContext.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,它可以被其他工具進一步處理。
- 在 dev 模式下,編譯器將嘗試從類型中推斷出相應的運行時驗證。例如這裏的
- 截至目前,類型聲明參數必須是以下之一,以確保正確的靜態分析。
- 一個類型的字面意義
- 對同一文件中的接口或類型字的引用
目前不支持複雜類型和從其他文件導入的類型。理論上,將來有可能支持類型導入。
使用類型聲明時的默認 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 語法,或許可以查看它。