其實 Vue 不一定要在 onMounted 取 API

do-not-always-fetch-api-in-on-mounted

大家好,我是鱈魚。(。・∀・)ノ゙

最近看到大家討論取得 API 時機,有許多人都提到「一定要」或者「習慣」,在 onMounted 這個 hook 內呼叫 API 取得資料。

其實這也沒不好,但是也沒什麼好處就是了。(。・ω・。)

讓我們來思考一下下,有關於 Vue 取得 API 資料這檔事。(不會轉生,也沒有史萊姆

「一定要」在 onMounted

有人說「一定要」是因為不這麼做會壞掉,這個可以從 2 個部分來看。

首先是 onMounted 這個 hook 是甚麼?讓我們看看官網的定義:

Registers a callback to be called after the component has been mounted.

意思就是「註冊一個會在元件安裝完成後被呼叫的 callback」。

甚麼意思呢?也就是他和有沒有資料一點關係都沒有。換句話說 onMounted 沒辦法幫你保證有資料、template 不會壞,從此天下太平,再也不用加班,。(´。_。`)

所以就牽扯到了第二部分:為甚麼會壞掉?

馬上取會壞,onMounted 取就不會壞,這表示你的資料可能和 DOM 或元件發生耦合,這其實不是好現象,不但違反了資料驅動的概念還埋了隱形的翅膀…地雷。

具體原因可能如下:

  • API 參數耦合了某個 DOM 的資料,導致「沒有在 onMounted 取值」等於「對 undefined 動手動腳」,當然就壞掉惹。(〃` 3′〃)
  • API 參數或操作耦合了某個子元件,而 Vue 的 onMounted 會保證子元件都 onMounted 完後,自己的 onMounted 才會觸發,導致 onMounted 取才沒事。

所以總結來說,所謂的「壞掉」很有可能只是資料流沒考慮 undefined 的情境。

可能啦,我巫力不足,只能通靈到這裡惹。 ╮(╯▽╰)╭

如果有使用 TypeScript 就很少會出現這類問題了,因為 TypeScript 會先不開心、滿江紅給你看。ᕦ(ò_óˇ)ᕤ

「習慣」在 onMounted

習慣上可能會這樣寫:

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { getUser, User } from './api'

const user = ref<User>();

onMounted(async () => {
  try {
    user.value = await getUser();
  } catch (error) {
    console.error('QQ');
  }
});
</script>

<template>
  <h1>Classic</h1>
  <div>
    user: {{ user?.name }}
  </div>
</template>

考慮讀取狀態則可能再加個 loading:

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { getUser, User } from './api'

const user = ref<User>();
const isLoading = ref(false);

onMounted(async () => {
  try {
    isLoading.value = true;
    user.value = await getUser();
  } catch (error) {
    console.error('QQ');
  } finally {
    isLoading.value = false;
  }
});
</script>

<template>
  <h1>Classic</h1>
  <div v-if="isLoading">
    資料讀取中...(~﹃~)~zZ
  </div>
  <div v-else>
    user: {{ user?.name }}
  </div>
</template>

剛剛說了,其實不用在 onMounted 才取資料,早一點取也行,所以可以變成這樣:

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { getUser, User } from './api'

const user = ref<User>();
const isLoading = ref(true);

getUser().then((data) => {
  user.value = data;
}).catch(() => {
  console.error('QQ');
}).finally(() => {
  isLoading.value = false;
});
</script>
...

路人:「這樣真的有比較好嗎?」

鱈魚:「當然沒有。(≧∇≦)ノ」

路人:(抄起球棒)

鱈魚:「冷靜冷靜,還沒說完啊。(っ °Д °;)っ」

來點 Composition

讓我們回想一下 Vue 3 的 Composition API 精神,一但要取得資料變多後,仔細觀察可以發現「資料、狀態(isLoading)、取資料的 function」會重複出現。

讓我們抽離、重組一下,新增一個檔案。

useAsyncData.ts

import { ref } from 'vue';

interface UseAsyncDataParam<Data> {
  executor: () => Promise<Data>;
  onError?: (error: unknown) => void;
}

export function useAsyncData<Data>(param: UseAsyncDataParam<Data>) {
  const data = ref<Data>();
  const isLoading = ref(true);

  param.executor().then((result) => {
    data.value = result;
  }).catch((error) => {
    param.onError?.(error);
  }).finally(() => {
    isLoading.value = false;
  });

  return {
    data,
    isLoading,
    execute: param.executor,
  }
}
寫過 Nuxt 的朋友們一定會有熟悉的感覺。(. ❛ ᴗ ❛.)

現在讓我們用這個 useAsyncData 改寫一下剛剛的例子看看。

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { getUser, User } from './api'
import { useAsyncData } from './useAsyncData'

const {
  data: user,
  isLoading,
} = useAsyncData({
  executor: () => getUser(),
  onError() {
    console.error('QQ');
  },
});
</script>

<template>
  <h1>Composable</h1>
  <div v-if="isLoading">
    資料讀取中...(~﹃~)~zZ
  </div>
  <div v-else>
    user: {{ user?.name }}
  </div>
</template>

就算有多個狀態要取得也變得更好組織程式碼了,是不是看起來乾淨許多呢?

恭喜我們重複造了 Nuxt 的 useAsyncData 與 VueUse 的 useAsyncState 的輪子了!

✧⁑。٩(ˊᗜˋ*)و✧⁕。

沒錯,雖然目前這個 useAsyncData 的功能很簡陋,有很多需要改進的部分,但是這就是上述兩個 API 的基礎精神。

不過 Nuxt 還包含了 SSR 行為,所以更加複雜,推薦大家可以看看 VueUse useAsyncState 的文件原始碼,一定可以獲益良多。

文章到此結束了,感謝您的閱讀,以上範例程式碼可以在此取得

有其他想法請不吝告訴我,鱈魚感謝您!(o゜▽゜)o☆

總結 🐟

  • 不一定要在 onMounted 取 API,這樣做可能會讓程式碼更複雜
  • 使用 Composition API 可以更好組織程式碼
  • 重複造輪子是進步的關鍵,但是也不要隨意在正式專案造輪子,以免埋下地雷