Vue3之script-setup全面解析

可能很多同學(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> 方案已在 Vue 3.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 的接收方式變化

attrsprops 很相似,也是基于父子通信的數(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)注意,useAttrs API 需要 Vue 3.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)注意,useSlots API 需要 Vue 3.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)作拿命拼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容