可能很多同學(xué)(包括我)剛上手 Vue 3.0 之后,都會(huì)覺(jué)得開發(fā)過(guò)程似乎變得更繁瑣了,Vue 官方團(tuán)隊(duì)當(dāng)然不會(huì)無(wú)視群眾的呼聲,如果你基于腳手架和 .vue 文件開發(fā),那么可以享受到更高效率的開發(fā)體驗(yàn)。
在閱讀這篇文章之前,需要對(duì) Vue 3.0 的單組件有一定的了解,如果還處于完全沒(méi)有接觸過(guò)的階段,請(qǐng)先抽點(diǎn)時(shí)間閱讀 單組件的編寫 一章。
WARNING
本章節(jié)的部分方案屬于實(shí)驗(yàn)性方案,或者是剛進(jìn)入定稿階段,所以在官網(wǎng)文檔上還暫時(shí)看不到使用說(shuō)明,期間可能還會(huì)有一些功能調(diào)整和 BUG 修復(fù),請(qǐng)留意版本號(hào)說(shuō)明。
所以要體驗(yàn)以下新特性,請(qǐng)確保項(xiàng)目下 package.json 里的 vue (opens new window)和 @vue/compiler-sfc (opens new window)都在 v3.1.4 版本以上,最好同步 NPM 上當(dāng)前最新的 @next 版本,否則在編譯過(guò)程中可能出現(xiàn)一些奇怪的問(wèn)題(這兩個(gè)依賴必須保持同樣的版本號(hào))。
#script-setup
這是一個(gè)比較有爭(zhēng)議的新特性,作為 setup 函數(shù)的語(yǔ)法糖,褒貶不一,不過(guò)經(jīng)歷了幾次迭代之后,目前在體驗(yàn)上來(lái)說(shuō),感受還是非常棒的。
TIP
截止至 2021-07-16 ,<script setup>方案已在 Vue3.2.0-beta.1版本中脫離實(shí)驗(yàn)狀態(tài),正式進(jìn)入 Vue 3.0 的隊(duì)伍,在新的版本中已經(jīng)可以作為一個(gè)官方標(biāo)準(zhǔn)的開發(fā)方案使用(但初期仍需注意與開源社區(qū)的項(xiàng)目兼容性問(wèn)題,特別是 UI 框架)。
另外,Vue 的 3.1.2 版本是針對(duì) script-setup 的一個(gè)分水嶺版本,自 3.1.4 開始 script-setup 進(jìn)入定稿狀態(tài),部分舊的 API 已被舍棄,本章節(jié)內(nèi)容將以最新的 API 為準(zhǔn)進(jìn)行整理說(shuō)明,如果您需要查閱舊版 API 的使用,請(qǐng)參閱 這里 (opens new window)。
#新特性的產(chǎn)生背景
在了解它怎么用之前,可以先了解一下它被推出的一些背景,可以幫助你對(duì)比開發(fā)體驗(yàn)上的異同點(diǎn),以及了解為什么會(huì)有這一章節(jié)里面的新東西。
在 Vue 3.0 的 .vue 組件里,遵循 SFC 規(guī)范要求(注:SFC,即 Single-File Component,.vue 單組件),標(biāo)準(zhǔn)的 setup 用法是,在 setup 里面定義的數(shù)據(jù)如果需要在 template 使用,都需要 return 出來(lái)。
如果你使用的是 TypeScript ,還需要借助 defineComponent 來(lái)幫助你對(duì)類型的自動(dòng)推導(dǎo)。
<!-- 標(biāo)準(zhǔn)組件格式 -->
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
// ...
return {
// ...
}
}
})
</script>
關(guān)于標(biāo)準(zhǔn) setup 和 defineComponent 的說(shuō)明和用法,可以查閱 全新的 setup 函數(shù) 一節(jié)。
script-setup 的推出是為了讓熟悉 3.0 的用戶可以更高效率的開發(fā)組件,減少一些心智負(fù)擔(dān),只需要給 script 標(biāo)簽添加一個(gè) setup 屬性,那么整個(gè) script 就直接會(huì)變成 setup 函數(shù),所有頂級(jí)變量、函數(shù),均會(huì)自動(dòng)暴露給模板使用(無(wú)需再一個(gè)個(gè) return 了)。
Vue 會(huì)通過(guò)單組件編譯器,在編譯的時(shí)候?qū)⑵涮幚砘貥?biāo)準(zhǔn)組件,所以目前這個(gè)方案只適合用 .vue 文件寫的工程化項(xiàng)目。
<!-- 使用 script-setup 格式 -->
<script setup lang="ts">
// ...
</script>
對(duì),就是這樣,代碼量瞬間大幅度減少……
TIP
因?yàn)?script-setup 的大部分功能在書寫上和標(biāo)準(zhǔn)版是一致的,這里只提及一些差異化的表現(xiàn)。
#全局編譯器宏
在 script-setup 模式下,新增了 4 個(gè)全局編譯器宏,他們無(wú)需 import 就可以直接使用。
但是默認(rèn)的情況下直接使用,項(xiàng)目的 eslint 會(huì)提示你沒(méi)有導(dǎo)入,但你導(dǎo)入后,控制臺(tái)的 Vue 編譯助手又會(huì)提示你不需要導(dǎo)入,就很尷尬…
哈哈哈哈不過(guò)不用著急,可以配置一下 lint ,把這幾個(gè)編譯助手寫進(jìn)全局規(guī)則里,就可以了,不需要導(dǎo)入也不會(huì)報(bào)錯(cuò)了。
// 項(xiàng)目根目錄下的 .eslintrc.js
module.exports = {
// 原來(lái)的lint規(guī)則,補(bǔ)充下面的globals...
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
},
}
關(guān)于幾個(gè)宏的說(shuō)明都在下面的文檔部分有說(shuō)明,你也可以從這里導(dǎo)航過(guò)去直接查看。
| 宏 | 說(shuō)明 |
|---|---|
| defineProps | 點(diǎn)擊查看 |
| defineEmits | 點(diǎn)擊查看 |
| defineExpose | 點(diǎn)擊查看 |
| withDefaults | 點(diǎn)擊查看 |
下面我們繼續(xù)了解 script-setup 的變化。
#template 操作簡(jiǎn)化
如果使用 JSX / TSX 寫法,這一點(diǎn)沒(méi)有太大影響,但對(duì)于習(xí)慣使用 <template /> 的開發(fā)者來(lái)說(shuō),這是一個(gè)非常爽的體驗(yàn)。
主要體現(xiàn)在這兩點(diǎn):
#變量無(wú)需進(jìn)行 return
標(biāo)準(zhǔn)組件模式下,setup 里定義的變量,需要 return 后,在 template 部分才可以正確拿到:
<!-- 標(biāo)準(zhǔn)組件格式 -->
<template>
<p>{{ msg }}</p>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
const msg: string = 'Hello World!';
// 要給 template 用的數(shù)據(jù)需要 return 出來(lái)才可以
return {
msg
}
}
})
</script>
在 script-setup 模式下,你定義了就可以直接使用。
<!-- 使用 script-setup 格式 -->
<template>
<p>{{ msg }}</p>
</template>
<script setup lang="ts">
const msg: string = 'Hello World!';
</script>
#子組件無(wú)需手動(dòng)注冊(cè)
子組件的掛載,在標(biāo)準(zhǔn)組件里的寫法是需要 import 后再放到 components 里才能夠啟用:
<!-- 標(biāo)準(zhǔn)組件格式 -->
<template>
<Child />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
// 導(dǎo)入子組件
import Child from '@cp/Child.vue'
export default defineComponent({
// 需要啟用子組件作為模板
components: {
Child
},
// 組件里的業(yè)務(wù)代碼
setup () {
// ...
}
})
</script>
在 script-setup 模式下,只需要導(dǎo)入組件即可,編譯器會(huì)自動(dòng)識(shí)別并啟用。
<!-- 使用 script-setup 格式 -->
<template>
<Child />
</template>
<script setup lang="ts">
import Child from '@cp/Child.vue'
</script>
#props 的接收方式變化
由于整個(gè) script 都變成了一個(gè)大的 setup function ,沒(méi)有了組件選項(xiàng),也沒(méi)有了 setup 入?yún)ⅲ詻](méi)辦法和標(biāo)準(zhǔn)寫法一樣去接收 props 了。
這里需要使用一個(gè)全新的 API :defineProps 。
defineProps 是一個(gè)方法,內(nèi)部返回一個(gè)對(duì)象,也就是掛載到這個(gè)組件上的所有 props ,它和普通的 props 用法一樣,如果不指定為 prop, 則傳下來(lái)的屬性會(huì)被放到 attrs 那邊去。
TIP
前置知識(shí)點(diǎn):接收 props - 組件之間的通信。
#defineProps 的基礎(chǔ)用法
所以,如果只是單純?cè)?template 里使用,那么其實(shí)就這么簡(jiǎn)單定義就可以了:
defineProps([
'name',
'userInfo',
'tags'
])
使用 string[] 數(shù)組作為入?yún)?,?prop 的名稱作為數(shù)組的 item 傳給 defineProps 就可以了。
如果 script 里的方法要拿到 props 的值,你也可以使用字面量定義:
const props = defineProps([
'name',
'userInfo',
'tags'
])
console.log(props.name);
但在作為一個(gè) Vue 老玩家,都清楚不顯性的指定 prop 類型的話,很容易在協(xié)作中引起程序報(bào)錯(cuò),那么應(yīng)該如何對(duì)每個(gè) prop 進(jìn)行類型檢查呢?
有兩種方式來(lái)處理類型定義。
#通過(guò)構(gòu)造函數(shù)檢查 prop
這是第一種方式:使用 JavaScript 原生構(gòu)造函數(shù)進(jìn)行類型規(guī)定。
也就是跟我們平時(shí)定義 prop 類型時(shí)一樣, Vue 會(huì)通過(guò) instanceof 來(lái)進(jìn)行 類型檢查 (opens new window)。
使用這種方法,需要通過(guò)一個(gè) “對(duì)象” 入?yún)?lái)傳遞給 defineProps ,比如:
defineProps({
name: String,
userInfo: Object,
tags: Array
});
所有原來(lái) props 具備的校驗(yàn)機(jī)制,都可以適用,比如你除了要限制類型外,還想指定 name 是可選,并且?guī)в幸粋€(gè)默認(rèn)值:
defineProps({
name: {
type: String,
required: false,
default: 'Petter'
},
userInfo: Object,
tags: Array
});
更多的 props 校驗(yàn)機(jī)制,可以點(diǎn)擊 帶有類型限制的 props 和 可選以及帶有默認(rèn)值的 props 了解更多。
#使用類型注解檢查 prop
這是第二種方式:使用 TypeScript 的類型注解。
和 ref 等 API 的用法一樣,defineProps 也是可以使用尖括號(hào) <> 來(lái)包裹類型定義,緊跟在 API 后面,另外,由于 defineProps 返回的是一個(gè)對(duì)象(因?yàn)?props 本身是一個(gè)對(duì)象),所以尖括號(hào)里面的類型還要用大括號(hào)包裹,通過(guò) key: value 的鍵值對(duì)形式表示,如:
defineProps<{ name: string }>();
注意到了嗎?這里使用的類型,和第一種方法提到的指定類型時(shí)是不一樣的。
TIP
在這里,不再使用構(gòu)造函數(shù)校驗(yàn),而是需要遵循使用 TypeScript 的類型。
比如字符串是 string,而不是 String 。
如果有多個(gè) prop ,就跟寫 interface 一樣:
defineProps<{
name: string;
phoneNumber: number;
userInfo: object;
tags: string[];
}>();
其中,舉例里的 userInfo 是一個(gè)對(duì)象,你可以簡(jiǎn)單的指定為 object,也可以先定義好它對(duì)應(yīng)的類型,再進(jìn)行指定:
interface UserInfo {
id: number;
age: number;
}
defineProps<{
name: string;
userInfo: UserInfo;
}>();
如果你想對(duì)某個(gè)數(shù)據(jù)設(shè)置為可選,也是遵循 TS 規(guī)范,通過(guò)英文問(wèn)號(hào) ? 來(lái)允許可選:
// name 是可選
defineProps<{
name?: string;
tags: string[];
}>();
如果你想設(shè)置可選參數(shù)的默認(rèn)值,需要借助 withDefaults API。
WARNING
需要強(qiáng)調(diào)的一點(diǎn)是:在 構(gòu)造函數(shù) 和 類型注解 這兩種校驗(yàn)方式只能二選一,不能同時(shí)使用,否則會(huì)引起程序報(bào)錯(cuò)
#withDefaults 的基礎(chǔ)用法
這個(gè)新的 withDefaults API 可以讓你在使用 TS 類型系統(tǒng)時(shí),也可以指定 props 的默認(rèn)值。
它接收兩個(gè)入?yún)ⅲ?/p>
| 參數(shù) | 類型 | 含義 |
|---|---|---|
| props | object | 通過(guò) defineProps 傳入的 props |
| defaultValues | object | 根據(jù) props 的 key 傳入默認(rèn)值 |
可能缺乏一些官方描述,還是看參考用法可能更直觀:
withDefaults(defineProps<{
size?: number
labels?: string[]
}>(), {
size: 3,
labels: () => ['default label']
})
如果你要在 TS / JS 再對(duì) props 進(jìn)行獲取,也可以通過(guò)字面量來(lái)拿到這些默認(rèn)值:
// 如果不習(xí)慣上面的寫法,你也可以跟平時(shí)一樣先通過(guò)interface定義一個(gè)類型接口
interface Props {
msg?: string
}
// 再作為入?yún)魅?const props = withDefaults(defineProps<Props>(), {
msg: 'hello'
})
// 這樣就可以通過(guò)props變量拿到需要的prop值了
console.log(props.msg)
#emits 的接收方式變化
和 props 一樣,emits 的接收也是需要使用一個(gè)全新的 API 來(lái)操作,這個(gè) API 就是 defineEmits 。
和 defineProps 一樣, defineEmits 也是一個(gè)方法,它接受的入?yún)⒏袷胶蜆?biāo)準(zhǔn)組件的要求是一致的。
TIP
注意:從3.1.3版本開始,該 API 已被改名,加上了復(fù)數(shù)結(jié)尾,帶有 s,在此版本之前是沒(méi)有 s 結(jié)尾!
前置知識(shí)點(diǎn):接收 emits - 組件之間的通信。
#defineEmits 的基礎(chǔ)用法
由于 emit 并非提供給模板直接讀取,所以需要通過(guò)字面量來(lái)定義 emits。
最基礎(chǔ)的用法也是傳遞一個(gè) string[] 數(shù)組進(jìn)來(lái),把每個(gè) emit 的名稱作為數(shù)組的 item 。
// 獲取 emit
const emit = defineEmits(['chang-name']);
// 調(diào)用 emit
emit('chang-name', 'Tom');
由于 defineEmits 的用法和原來(lái)的 emits 選項(xiàng)差別不大,這里也不重復(fù)說(shuō)明更多的諸如校驗(yàn)之類的用法了,可以查看 接收 emits 一節(jié)了解更多。
#attrs 的接收方式變化
attrs 和 props 很相似,也是基于父子通信的數(shù)據(jù),如果父組件綁定下來(lái)的數(shù)據(jù)沒(méi)有被指定為 props ,那么就會(huì)被掛到 attrs 這邊來(lái)。
在標(biāo)準(zhǔn)組件里, attrs 的數(shù)據(jù)是通過(guò) setup 的第二個(gè)入?yún)?context 里的 attrs API 獲取的。
// 標(biāo)準(zhǔn)組件的寫法
export default defineComponent({
setup (props, { attrs }) {
// attrs 是個(gè)對(duì)象,每個(gè) Attribute 都是它的 key
console.log(attrs.class);
// 如果傳下來(lái)的 Attribute 帶有短橫線,需要通過(guò)這種方式獲取
console.log(attrs['data-hash']);
}
})
但和 props 一樣,由于沒(méi)有了 context 參數(shù),需要使用一個(gè)新的 API 來(lái)拿到 attrs 數(shù)據(jù)。
這個(gè) API 就是 useAttrs 。
TIP
請(qǐng)注意,useAttrsAPI 需要 Vue3.1.4或更高版本才可以使用。
#useAttrs 的基礎(chǔ)用法
顧名思義, useAttrs 可以是用來(lái)獲取 attrs 數(shù)據(jù)的,它的用法非常簡(jiǎn)單:
// 導(dǎo)入 useAttrs 組件
import { useAttrs } from 'vue'
// 獲取 attrs
const attrs = useAttrs()
// attrs是個(gè)對(duì)象,和 props 一樣,需要通過(guò) key 來(lái)得到對(duì)應(yīng)的單個(gè) attr
console.log(attrs.msg);
對(duì) attrs 不太了解的話,可以查閱 獲取非 Prop 的 Attribute
#slots 的接收方式變化
slots 是 Vue 組件的插槽數(shù)據(jù),也是在父子通信里的一個(gè)重要成員。
對(duì)于使用 template 的開發(fā)者來(lái)說(shuō),在 script-setup 里獲取插槽數(shù)據(jù)并不困難,因?yàn)楦鷺?biāo)準(zhǔn)組件的寫法是完全一樣的,可以直接在 template 里使用 <slot /> 標(biāo)簽渲染。
<template>
<div>
<!-- 插槽數(shù)據(jù) -->
<slot />
<!-- 插槽數(shù)據(jù) -->
</div>
</template>
但對(duì)使用 JSX / TSX 的開發(fā)者來(lái)說(shuō),就影響比較大了,在標(biāo)準(zhǔn)組件里,想在 script 里獲取插槽數(shù)據(jù),也是需要在 setup 的第二個(gè)入?yún)⒗锬玫?slots API 。
// 標(biāo)準(zhǔn)組件的寫法
export default defineComponent({
// 這里的 slots 就是插槽
setup (props, { slots }) {
// ...
}
})
新版本的 Vue 也提供了一個(gè)全新的 useSlots API 來(lái)幫助 script-setup 用戶獲取插槽。
TIP
請(qǐng)注意,useSlotsAPI 需要 Vue3.1.4或更高版本才可以使用。
#useSlots 的基礎(chǔ)用法
先來(lái)看看父組件,父組件先為子組件傳入插槽數(shù)據(jù),支持 “默認(rèn)插槽” 和 “命名插槽” :
<template>
<!-- 子組件 -->
<ChildTSX>
<!-- 默認(rèn)插槽 -->
<p>I am a default slot from TSX.</p>
<!-- 默認(rèn)插槽 -->
<!-- 命名插槽 -->
<template #msg>
<p>I am a msg slot from TSX.</p>
</template>
<!-- 命名插槽 -->
</ChildTSX>
<!-- 子組件 -->
</template>
<script setup lang="ts">
import ChildTSX from '@cp/context/Child.tsx'
</script>
在使用 JSX / TSX 編寫的子組件里,就可以通過(guò) useSlots 來(lái)獲取父組件傳進(jìn)來(lái)的 slots 數(shù)據(jù)進(jìn)行渲染:
// 注意:這是一個(gè) .tsx 文件
import { defineComponent, useSlots } from 'vue'
const ChildTSX = defineComponent({
setup() {
// 獲取插槽數(shù)據(jù)
const slots = useSlots()
// 渲染組件
return () => (
<div>
{/* 渲染默認(rèn)插槽 */}
<p>{ slots.default ? slots.default() : '' }</p>
{/* 渲染命名插槽 */}
<p>{ slots.msg ? slots.msg() : '' }</p>
</div>
)
},
})
export default ChildTSX
#ref 的通信方式變化
在標(biāo)準(zhǔn)組件寫法里,子組件的數(shù)據(jù)都是默認(rèn)隱式暴露給父組件的,也就是父組件可以通過(guò) childComponent.value.foo 這樣的方式直接操作子組件的數(shù)據(jù)(參見(jiàn):DOM 元素與子組件 - 響應(yīng)式 API 之 ref)。
但在 script-setup 模式下,所有數(shù)據(jù)只是默認(rèn)隱式 return 給 template 使用,不會(huì)暴露到組件外,所以父組件是無(wú)法直接通過(guò)掛載 ref 變量獲取子組件的數(shù)據(jù)。
在 script-setup 模式下,如果要調(diào)用子組件的數(shù)據(jù),需要先在子組件顯示的暴露出來(lái),才能夠正確的拿到,這個(gè)操作,就是由 defineExpose 來(lái)完成。
#defineExpose 的基礎(chǔ)用法
defineExpose 的用法非常簡(jiǎn)單,它本身是一個(gè)函數(shù),可以接受一個(gè)對(duì)象參數(shù)。
在子組件里,像這樣把需要暴露出去的數(shù)據(jù)通過(guò) key: value 的形式作為入?yún)ⅲㄏ旅娴睦邮怯玫搅?ES6 的 屬性的簡(jiǎn)潔表示法 (opens new window)):
<script setup lang="ts">
// 定義一個(gè)想提供給父組件拿到的數(shù)據(jù)
const msg: string = 'Hello World!';
// 顯示暴露的數(shù)據(jù),才可以在父組件拿到
defineExpose({
msg
});
</script>
然后你在父組件就可以通過(guò)掛載在子組件上的 ref 變量,去拿到暴露出來(lái)的數(shù)據(jù)了。
#頂級(jí) await 的支持
在 script-setup 模式下,不必再配合 async 就可以直接使用 await 了,這種情況下,組件的 setup 會(huì)自動(dòng)變成 async setup 。
<script setup lang="ts">
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
它轉(zhuǎn)換成標(biāo)準(zhǔn)組件的寫法就是:
<script lang="ts">
import { defineComponent, withAsyncContext } from 'vue'
export default defineComponent({
async setup() {
const post = await withAsyncContext(
fetch(`/api/post/1`).then((r) => r.json())
)
return {
post
}
}
})
</script>
點(diǎn)贊加關(guān)注,永遠(yuǎn)不迷路
每天一更新,創(chuàng)作拿命拼