iT邦幫忙

2022 iThome 鐵人賽

DAY 21
1
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 21

Day 21: 來發 API 吧!Async Composition API setup() feat. <Suspense>

  • 分享至 

  • xImage
  •  

前言

接下來幾天會以「在 Vue 3 Composition API 處理非同步( 發 API )」為主軸,從新手的角度出發,告訴大家可以在哪些地方、時機發 API,會講到 Lifecycle Hooks 和 Vue Router 的 Navigation Guard,最後會介紹狀態管理器(Pinia)哦!

上一篇我們已經將 axios 實例和請求方法都封裝好了,可以直接透過 hotelAPI.GET(url) 去做 HTTP request,今天就不會再講到 axios 和封裝方式,好奇的人可以到上一篇去看,今天要直接來發 API 了!
API 的部份是拿第二屆 F2E 提供的旅館預約 API 來實作。

今日目標:從 API 拿到資料,直接渲染到畫面上。

當初第一個想法,就是在 setup() / <script setup> 裡面直接發 API,但發 API 是非同步欸,我要渲染的資料怎麼辦?
await 等他一下吧!反正文件說 setup 函式可以是非同步的嘛...

如果你覺得這段 murmur 很笨...那你可能不需要我今天的文章QQ
如果你已經試過,就知道會碰到不少問題,這篇文章會解析警告訊息,並說明解決方法。


Outline

  • async setup
  • 報錯/警告原因解析
  • 解決方法
    • Reactive Sync
    • <Suspense>

async setup

在 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 會收到兩則警告訊息。

接下來會先分析報錯和警告的原因,再說明正確的作法。

報錯/警告原因解析

  1. 警告訊息:[Vue warn]: Property "rooms" was accessed during render but is not defined on instance.

報錯訊息指出,在渲染這個元件的時候,沒有找到 rooms 這個變數。

菜的心聲:「明明就有 rooms ,還寫說要 await 等他拿來到資料!為什麼他連 rooms 都讀取不到?

你可能以為 Vue 後面的渲染函式會 await setup() ,等到 setup() 函式執行完才執行。(也可能只有我以為QQ)

但不是的。

當 setup 函式遇到 await 時,它就會先暫停、返回, setup 返回後,會繼續跑元件的生命週期,這個元件就直接被掛載(渲染)上去了,所以 Vue 才會在渲染的時候找不到 rooms 這個變數。

這裡還有很多可以延伸討論的!因為這代表在 await 之後才定義的內容,不會在同步期間被執行,而部份 Vue API 沒有在同步期間被初始化或註冊,調用上會出問題,這裡我們明天再一併說明。
今天目標先放在將從 API 拿到的資料成功渲染到畫面上。

  1. 警告訊息:[Vue warn]: Component <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> 元件,幫助開發者處理非同步依賴的狀態,能根據 pendingresolve,切換對應顯示的元件或模板。

而當非同步依賴沒有引入到 <Suspense> 元件內時,Vue 就會提出警告。

報錯訊息中提到的 <Suspense> 我們晚點會用到。


解決方式

1. Reactive Sync

Reactive Sync 指的是將非同步函式「變成」同步的響應式資料
先宣告一個響應性的變數,並給予預設值空值(null[],等待非同步(發 API)執行完成後,再將從 API 拿到的內容賦值給變數,Vue 偵測響應數據的變動,並即時更新模板。

1-a. 透過 .then 串接處理

//inside <script setup>
const rooms = ref([]);
hotelAPI
  .GET("/rooms")
  .then((res) => (rooms.value = res.items))
  .catch((err) => console.log(err));

1-b 直接呼叫非同步函式(不 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();

1-c. 調用生命週期鉤子

生命週期鉤子可以傳入非同步 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);
  }
});

1-d. 使用 Vue 的 watchEffect API

const rooms = ref([]);
watchEffect(async () => {
  const response = await hotelAPI.GET("/rooms");
  rooms.value = response.items;
  console.log(rooms.value);
});

註:我在這裡的寫法是直接重新賦值,所以用 ref() 去做資料的響應。

2. <Suspense>

註:<Suspense> 大概在 2020 年推出,但目前在官網還是標註為實驗性的功能。

<Suspense> 是 Vue 內建的元件,主要用來處理元件樹中「非同步的依賴」,以下兩種非同步依賴可以由 <Suspense> 處理:

  1. 帶有 async setup 的元件
  2. defineAsyncComponent 定義的非同步元件

<Suspense> 元件提供兩個 slot 區塊:

  • default
    會在這裡引入非同步依賴,等到非同步依賴的 Promise 狀態為 resolve ,會掛載這個區塊的元件和內容
  • fallback
    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>
    

補充(2022/10/17)

<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 的 useAsyncStateuseAsyncQueue、vue-composition-toolkit 的 useAsyncState,這裡就不特別示範,大家可以用關鍵字去找相關資源。


下一篇會先延伸說明 async setup 的注意事項,會提及一點點 Vue 實作的部份,我覺得蠻有趣的,就是不知道自己能不能寫好哈哈哈
那就明天見了~

參考資料


上一篇
Day 20: 在發 API 之前 - 先學 axios 基礎與封裝管理 API
下一篇
Day 22: Composition API async setup() + await 的限制
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言