接下來幾天會以「在 Vue 3 Composition API 處理非同步( 發 API )」為主軸,從新手的角度出發,告訴大家可以在哪些地方、時機發 API,會講到 Lifecycle Hooks 和 Vue Router 的 Navigation Guard,最後會介紹狀態管理器(Pinia)哦!
在上一篇我們已經將 axios 實例和請求方法都封裝好了,可以直接透過 hotelAPI.GET(url)
去做 HTTP request,今天就不會再講到 axios 和封裝方式,好奇的人可以到上一篇去看,今天要直接來發 API 了!
API 的部份是拿第二屆 F2E 提供的旅館預約 API 來實作。
當初第一個想法,就是在 setup()
/ <script setup>
裡面直接發 API,但發 API 是非同步欸,我要渲染的資料怎麼辦?
那 await
等他一下吧!反正文件說 setup 函式可以是非同步的嘛...
如果你覺得這段 murmur 很笨...那你可能不需要我今天的文章QQ
如果你已經試過,就知道會碰到不少問題,這篇文章會解析警告訊息,並說明解決方法。
<Suspense>
在 Composition API 中使用的 setup()
函式,其實可以作為非同步函式執行,但會有一些限制,這也是今天跟明天文章會講到的部份。
如果是使用 <script setup>
,他會辨認 Top-level await
,也就是說我們可以在 <script setup>
裡面最外層的 scope 使用 await
,Vue 會將 script 編譯成一個非同步的 setup 函式。
所以,我當初最直覺的作法就是:
//<script setup> 內
import hotelAPI from "@/api/service";
const rooms = await hotelAPI.GET("rooms").items;
console.log(`rooms`, rooms);
<template>
<div class="wrapper">
<p v-for="room in rooms" :key="room">{{ room }}</p>
</div>
</template>
使用上面這個寫法,畫面不會渲染出 API 拿回來的資料(rooms
);從 Network 中可以到看這次請求有成功,但畫面就是沒有渲染出來,而 console 會收到兩則警告訊息。
接下來會先分析報錯和警告的原因,再說明正確的作法。
報錯訊息指出,在渲染這個元件的時候,沒有找到 rooms
這個變數。
菜的心聲:「明明就有 rooms
,還寫說要 await
等他拿來到資料!為什麼他連 rooms
都讀取不到?」
你可能以為 Vue 後面的渲染函式會 await setup()
,等到 setup()
函式執行完才執行。(也可能只有我以為QQ)
但不是的。
當 setup 函式遇到 await
時,它就會先暫停、返回,在 setup
返回後,會繼續跑元件的生命週期,這個元件就直接被掛載(渲染)上去了,所以 Vue 才會在渲染的時候找不到 rooms
這個變數。
這裡還有很多可以延伸討論的!因為這代表在 await
之後才定義的內容,不會在同步期間被執行,而部份 Vue API 沒有在同步期間被初始化或註冊,調用上會出問題,這裡我們明天再一併說明。
今天目標先放在將從 API 拿到的資料成功渲染到畫面上。
<Anonymous>
: setup function returned a promise, but no <Suspense>
boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense>
in order to be rendered.當某個元件的 setup()
返回 Promise 時,Vue 沒有預設該如何處理 Promise,他不知道非同步動作什麼時候完成,還有元件在非同步處理時,該顯示什麼。
所以 Vue 團隊開發了 <Suspense>
元件,幫助開發者處理非同步依賴的狀態,能根據 pending
和 resolve
,切換對應顯示的元件或模板。
而當非同步依賴沒有引入到 <Suspense>
元件內時,Vue 就會提出警告。
報錯訊息中提到的 <Suspense>
我們晚點會用到。
Reactive Sync 指的是將非同步函式「變成」同步的響應式資料。
先宣告一個響應性的變數,並給予預設值空值(null
或 []
),等待非同步(發 API)執行完成後,再將從 API 拿到的內容賦值給變數,Vue 偵測響應數據的變動,並即時更新模板。
.then
串接處理//inside <script setup>
const rooms = ref([]);
hotelAPI
.GET("/rooms")
.then((res) => (rooms.value = res.items))
.catch((err) => console.log(err));
await
)//inside <script setup>
const rooms = ref([]);
async function getRooms() {
try {
const { items } = await hotelAPI.GET("rooms");
rooms.value = items;
} catch (error) {
console.log(error);
}
}
getRooms();
生命週期鉤子可以傳入非同步 callback。
將發 API 和重新賦值的函式傳入 onBeforeMount()
,會在元件掛載之前被呼叫。
//inside <script setup>
const rooms = ref([]);
onBeforeMount(async () => {
try {
const { items } = await hotelAPI.GET("/rooms");
rooms.value = items;
} catch (error) {
console.log(error);
}
});
const rooms = ref([]);
watchEffect(async () => {
const response = await hotelAPI.GET("/rooms");
rooms.value = response.items;
console.log(rooms.value);
});
註:我在這裡的寫法是直接重新賦值,所以用 ref()
去做資料的響應。
<Suspense>
註:<Suspense>
大概在 2020 年推出,但目前在官網還是標註為實驗性的功能。
<Suspense>
是 Vue 內建的元件,主要用來處理元件樹中「非同步的依賴」,以下兩種非同步依賴可以由 <Suspense>
處理:
defineAsyncComponent
定義的非同步元件
<Suspense>
元件提供兩個 slot 區塊:
resolve
,會掛載這個區塊的元件和內容pending
期間要顯示的內容,可以在這裡引入自訂的 loading 元件注意這兩個 slot 都只接受單一 node 節點,如果想裡面渲染多個元件或元素,可以用 <template>
包起來。
會根據非同步行為的狀態,切換顯示的 slot 區塊;如果 <Suspense>
在初始渲染的時候,沒有接收到非同步狀態,就會直接掛載 default 內容,並進入完成狀態,除非 default slot 的根節點被替換掉,才有可能重新回到加載狀態。
注意 <Suspense>
是要包在外層引用的地方,而不是元件層的 <template>
<App>
└─ <RoomView>(有 async setup()的非同步元件)
<RoomView>
(非同步元件)
const { items: rooms } = await hotelAPI.GET("/rooms");
<template>
<div class="wrapper">
<div v-for="room in rooms" :key="room.id" class="room">
<img :src="room.imageUrl" />
</div>
</div>
</template>
<RoomView>
處
<div class="wrapper">
<h2>Rooms View</h2>
<Suspense>
<template #default>
~這裡放非同步元件~
<RoomsView />
</template>
<template #fallback>
~這裡放 Promise pending 期間顯示的內容~
<p>Loading...</p>
</template>
</Suspense>
</div>
<Suspense>
搭配 <router-view>
的正確結構寫法。
<router-view v-slot="{ Component }">
<suspense>
<template #default>
<component :is="Component"></component>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</suspense>
</router-view>
<router-view>
要放在最外層,而透過 router 和 <router-view>
所切換的元件/頁面可以從 v-slot 指令拿到,將從 v-slot 拿到的元件放到內層 <Suspense>
中,使用動態元件的寫法即可順利切換。
註:如果今天還組合了更多 Vue 內建元件一起使用,可以參考官方文件這邊。
有一些函式庫有提供支援 Composition API 的 composable 函式可以使用,例如 VueUse 的 useAsyncState
和 useAsyncQueue
、vue-composition-toolkit 的 useAsyncState
,這裡就不特別示範,大家可以用關鍵字去找相關資源。
下一篇會先延伸說明 async setup 的注意事項,會提及一點點 Vue 實作的部份,我覺得蠻有趣的,就是不知道自己能不能寫好哈哈哈
那就明天見了~