1. 程式人生 > 其它 >Vue3 script setup 語法糖詳解

Vue3 script setup 語法糖詳解


theme: cyanosis
highlight: zenburn

目前setup sugar已經進行了定稿,vue3 + setup sugar + TS的寫法看起來很香,寫本文時 Vue 版本是"^3.2.6"

script setup 語法糖

新的setup選項是在元件建立之前,props被解析之後執行,是組合式 API 的入口。

WARNING\
setup中你應該避免使用this,因為它不會找到元件例項。setup的呼叫發生在dataproperty、computedproperty 或methods被解析之前,所以它們無法>在setup中被獲取。

setup選項是一個接收props

context的函式,我們將在之後進行討論。此外,我們將setup返回的所有內容都暴露給元件的其餘部分 (計算屬性、方法、生命週期鉤子等等) 以及元件的模板。

它是 Vue3 的一個新語法糖,在setup函式中。所有 ES 模組匯出都被認為是暴露給上下文的值,幷包含在 setup() 返回物件中。相對於之前的寫法,使用後,語法也變得更簡單。

在添加了setup的script標籤中,我們不必宣告和方法,這種寫法會自動將所有頂級變數、函式,均會自動暴露給模板(template)使用\
這裡強調一句 “暴露給模板,跟暴露給外部不是一回事”

使用方式極其簡單,僅需要在script標籤加上setup

關鍵字即可。示例:

<script setup></script>

該setup功能是新的元件選項。它是元件內部暴露出所有的屬性和方法的統一API。

使用後意味著,script標籤內的內容相當於原本元件宣告中setup()的函式體,不過也有一定的區別。

使用 script setup 語法糖,元件只需引入不用註冊,屬性和方法也不用返回,也不用寫setup函式,也不用寫export default ,甚至是自定義指令也可以在我們的template中自動獲得。基本語法

呼叫時機

建立元件例項,然後初始化 props ,緊接著就呼叫setup 函式。從生命週期鉤子的視角來看,它會在 beforeCreate 鉤子之前被呼叫.

模板中使用

如果 setup 返回一個物件,則物件的屬性將會被合併到元件模板的渲染上下文

<template>
<div>
{{count}}{{object.foo}}
</div>
</template>

setup 引數

  1. 「props」第一個引數接受一個響應式的props,這個props指向的是外部的props。如果你沒有定義props選項,setup中的第一個引數將為undifined。props和vue2.x並無什麼不同,仍然遵循以前的原則;
  • 不要在子元件中修改props;如果你嘗試修改,將會給你警告甚至報錯。
  • 不要結構props。結構的props會失去響應性。

2.「context」第二個引數提供了一個上下文物件,從原來 2.x 中 this 選擇性地暴露了一些 property。

<script setup="props, context" lang="ts">
context.attrs
context.slots
context.emit
<script>

像這樣,只要在setup處宣告即可自動匯入,同時也支援解構語法:

<script setup="props, { emit }" lang="ts">
<script>

元件自動註冊

匯入 component 或 directive 直接import即可,無需額外宣告

import { MyButton } from "@/components"
import { directive as clickOutside } from 'v-click-outside'

與原先一樣,模板中也支援使用kabab-case來建立元件,如<my-button />

在 script setup 中,引入的元件可以直接使用,無需再通過components進行註冊,並且無法指定當前元件的名字,它會自動以檔名為主,也就是不用再寫name屬性了。示例:

<template>
<HelloWorld />
</template>
<script setup>
import HelloWorld from "./components/HelloWorld.vue"; //此處使用 Vetur 外掛會報紅
</script>

如果需要定義類似 name 的屬性,可以再加個平級的 script 標籤,在裡面實現即可。

元件核心 API 的使用

定義元件的 props

通過defineProps指定當前 props 型別,獲得上下文的props物件。示例:

<script setup>
import { defineProps } from 'vue'
const props = defineProps({
title: String,
})
</script>
<!-- 或者 -->
<script setup lang="ts">
import { ref,defineProps } from 'vue';
type Props={
msg:string
}
defineProps<Props>();
</script>

定義 emit

使用defineEmit定義當前元件含有的事件,並通過返回的上下文去執行 emit。示例:

<script setup>
import { defineEmits } from 'vue'
const emit = defineEmits(['change', 'delete'])
</script>

父子元件通訊

defineProps用來接收父元件傳來的 props ;defineEmits用來宣告觸發的事件。

//父元件
<template>
<my-son foo="??????" @childClick="childClick" />
</template>
<script lang="ts" setup>
import MySon from "./MySon.vue";
let childClick = (e: any):void => {
console.log('from son:',e); //??????
};
</script>
//子元件
<template>
<span @click="sonToFather">資訊:{{ props.foo }}</span>
</template>
<script lang="ts" setup>
import { defineEmits, defineProps} from "vue";
const emit = defineEmits(["childClick"]); // 宣告觸發事件 childClick
const props = defineProps({ foo: String }); // 獲取props
const sonToFather = () =>{
emit('childClick' , props.foo)
}
</script>

子元件通過 defineProps 接收父元件傳過來的資料,子元件通過 defineEmits 定義事件傳送資訊給父元件

增強的props型別定義:

const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmit<(e: 'update' | 'delete', id: number) => void>()

不過注意,採用這種方法將無法使用props預設值。

定義響應變數、函式、監聽、計算屬性computed

<script setup lang="ts">
import { ref,computed,watchEffect } from 'vue';
const count = ref(0); //不用 return ,直接在 templete 中使用
const addCount=()=>{ //定義函式,使用同上
count.value++;
}
//定義計算屬性,使用同上
const howCount=computed(()=>"現在count值為:"+count.value);
//定義監聽,使用同上 //...some code else
watchEffect(()=>console.log(count.value));
</script>

watchEffect

用於有副作用的操作,會自動收集依賴。

和watch區別

無需區分deep,immediate,只要依賴的資料發生變化,就會呼叫

reactive

此時name只會在初次建立的時候進行賦值,如果中間想要改變name的值,那麼需要藉助composition api 中的reactive。

<script setup lang="ts">
import { reactive, onUnmounted } from 'vue'
const state = reactive({
counter: 0
})
// 定時器 每秒都會更新資料
const timer = setInterval(() => {
state.counter++
}, 1000);
onUnmounted(() => {
clearInterval(timer);
})
</script>
<template>
<div>{{state.counter}}</div>
</template>

使用ref也能達到我們預期的'counter',並且在模板中,vue進行了處理,我們可以直接使用counter而不用寫counter.value.

ref和reactive的關係:

ref是一個{value:'xxxx'}的結構,value是一個reactive物件

ref 暴露變數到模板

曾經的提案中,如果需要暴露變數到模板,需要在變數前加入export宣告:

export const count = ref(0)

不過在新版的提案中,無需export宣告,編譯器會自動尋找模板中使用的變數,只需像下面這樣簡單的宣告,即可在模板中使用該變數

<script setup lang="ts">
import { ref } from 'vue'
const counter = ref(0);//不用 return ,直接在 templete 中使用
const timer = setInterval(() => {
counter.value++
}, 1000)
onUnmounted(() => {
clearInterval(timer);
})
</script>
<template>
<div>{{counter}}</div>
</template>

生命週期方法

因為setup是圍繞beforeCreatecreated生命週期鉤子執行的,所以不需要顯式地定義它們。換句話說,在這些鉤子中編寫的任何程式碼都應該直接在setup函式中編寫。

可以通過在生命週期鉤子前面加上 “on” 來訪問元件的生命週期鉤子。官網:生命週期鉤子

下表包含如何在setup ()內部呼叫生命週期鉤子:

選項式 APIHook insidesetup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated
<script setup lang="ts">
import { onMounted } from 'vue';
onMounted(() => { console.log('mounted!'); });
</script>

獲取 slots 和 attrs

注:useContext API 被棄用,取而代之的是更加細分的 api。

可以通過useContext從上下文中獲取 slots 和 attrs。不過提案在正式通過後,廢除了這個語法,被拆分成了useAttrsuseSlots

  1. useAttrs:見名知意,這是用來獲取 attrs 資料,但是這和 vue2 不同,裡面包含了class屬性方法
<template>
<component v-bind='attrs'></component>
</template>
<srcipt setup lang='ts'>
const attrs = useAttrs();
<script>
  1. useSlots: 顧名思義,獲取插槽資料。

使用示例:

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

其他 Hook Api

  1. useCSSModule:CSS Modules 是一種 CSS 的模組化和組合系統。vue-loader 整合 CSS Modules,可以作為模擬 scoped CSS。允許在單個檔案元件的setup中訪問CSS模組。此 api 本人用的比較少,不過多做介紹。
  2. useCssVars: 此 api 暫時資料比較少。介紹v-bind in styles時提到過。
  3. useTransitionState: 此 api 暫時資料比較少。
  4. useSSRContext: 此 api 暫時資料比較少。

defineExpose API

傳統的寫法,我們可以在父元件中,通過 ref 例項的方式去訪問子元件的內容,但在 script setup 中,該方法就不能用了,setup 相當於是一個閉包,除了內部的template模板,誰都不能訪問內部的資料和方法。

如果需要對外暴露 setup 中的資料和方法,需要使用 defineExpose API。示例:

const a = 1
const b = ref(2)
defineExpose({ a, b, })

注意:目前發現defineExpose暴露出去的屬性以及方法都是unknown型別,如果有修正型別的方法,歡迎評論區補充。

//父元件

<template>
<Daughter ref="daughter" />
</template>

<script lang="ts" setup>
import { ref } from "vue";
import Daughter from "./Daughter.vue";

const daughter = ref(null)
console.log('????~daughter',daughter)
</script>

//子元件

<template>
<div>妾身{{ msg }}</div>
</template>

<script lang="ts" setup>
import { ref ,defineExpose} from "vue";
const msg = ref('貂蟬')
defineExpose({
msg
})
</script>

### 屬性和方法無需返回,直接使用!
這可能是帶來的較大便利之一,在以往的寫法中,定義資料和方法,都需要在結尾 return 出去,才能在模板中使用。在 script setup 中,定義的屬性和方法無需返回,可以直接使用!示例:
```html
<template>
<div>
<p>My name is {{name}}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const name = ref('Sam')
</script>

支援 async await 非同步

注意在vue3的原始碼中,setup執行完畢,函式 getCurrentInstance 內部的有個值會釋放對 currentInstance 的引用,await 語句會導致後續程式碼進入非同步執行的情況。所以上述例子中最後一個 getCurrentInstance() 會返回 null,建議使用變數儲存第一個 getCurrentInstance() 返回的引用.

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

<script setup>中可以使用頂層await。結果程式碼會被編譯成async setup()

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

另外,await 的表示式會自動編譯成在await之後保留當前元件例項上下文的格式。

注意\
async setup()必須與Suspense組合使用,Suspense目前還是處於實驗階段的特性。我們打算在將來的某個釋出版本中開發完成並提供文件 - 如果你現在感興趣,可以參照tests看它是如何工作的。

定義元件其他配置

配置項的缺失,有時候我們需要更改元件選項,在setup中我們目前是無法做到的。我們需要在上方再引入一個script,在上方寫入對應的export即可,需要單開一個 script。

<script setup>可以和普通的<script>一起使用。普通的<script>在有這些需要的情況下或許會被使用到:

  • 無法在<script setup>宣告的選項,例如inheritAttrs或通過外掛啟用的自定義的選項。
  • 宣告命名匯出。
  • 執行副作用或者建立只需要執行一次的物件。

在script setup 外使用export default,其內容會被處理後放入原元件宣告欄位。

<script>
// 普通 `<script>`, 在模組範圍下執行(只執行一次)
runSideEffectOnce()
// 宣告額外的選項
export default {
name: "MyComponent",
inheritAttrs:false,
customOptions:{}
}
</script>
<scriptsetup>
import HelloWorld from '../components/HelloWorld.vue'
// 在 setup() 作用域中執行 (對每個例項皆如此)
//yourcode
</script>
<template>
<div>
<HelloWorld msg="Vue3 + TypeScript + Vite"/>
</div>
</template>

注意:Vue 3 SFC 一般會自動從元件的檔名推斷出元件的 name。在大多數情況下,不需要明確的 name 宣告。唯一需要的情況是當你需要<keep-alive>包含或排除或直接檢查元件的選項時,你需要這個名字。

關於 TS 與 ESLint 的不完美

  1. @typescript-eslint/no-unused-vars規則不相容,此規則含義為定義了,未進行使用。該規則其實影響不大,關閉即可。

  2. 與匯入的型別宣告不相容,當你通過解構的方式去匯入型別,setup sugar會進行自動匯出。這時候,你就會收到 TS 的一條報錯:此為型別,但被當作值使用。解決辦法:型別匯出使用export default匯出或者引入時使用import * as xx來進行引入,也可以使用import type { test } from "./test";解決。

語法糖實現

vue檔案程式碼

<template>
<div>{{ msg }}</div>
</template>
<script setup>
const msg = 'Hello!'
</script>

編譯後的js程式碼:

export default {
setup() {
const msg = 'Hello!'
return function render() {
// has access to everything inside setup() scope
// 在函式 setup 作用域,函式 render 能訪問 setup 的一切,
return h('div', msg)
}
}
}

注意到,即使普通變數也能作為模版被置入 template 中被編譯,某些人認為這不合適,不夠分離。

vscode配套外掛

volar是一個vscode外掛,用來增強vue編寫體驗,使用volar外掛可以獲得script setup語法的最佳支援。

vetur相同,volar是一個針對vuevscode外掛,不過與vetur不同的是,volar提供了更為強大的功能,讓人直呼臥槽

安裝的方式很簡單,直接在vscode的外掛市場搜尋volar,然後點選安裝就可以了。

vscode中使用的時候,先禁用Vetur,再下載使用Volar

使用習慣

options api切換到composition api最大的問題無異於最大的問題就是沒有強制的程式碼分割槽,如果書寫的人沒有很好的程式碼習慣,那麼後續的人將會看的十分難受。目前我是這麼解決的:

  • 自我程式碼分割槽並且儘量抽離方法(寫好註釋),分割槽如下:

    1. 相關引入
    2. 響應式資料、props、emit 定義
    3. 生命週期以及 watch 書寫
    4. 方法定義
    5. 方法、屬性暴露
  • 元件抽離:將頁面拆成兩個資料夾,一個為views,一個為components。views 和 components 資料夾下有各自的檔案。views 資料夾中為頁面入口,掌管資料,而 components 則為頁面中一些元件抽離。如果是公共元件,再抽離到 components 資料夾下其他位置。

  • hook 抽離:儘可能將邏輯抽離,並不一定要進行復用。

寫在最後

寫作不易,希望可以獲得你的一個「贊」。如果文章對你有用,可以選擇「關注 + 收藏」。 如有文章有錯誤或建議,歡迎評論指正,謝謝你。❤️