Vue Composition API 筆記(下)

Eason Lin
12 min readOct 2, 2020

--

接續上篇,這篇主要記錄使用 Composition API 將 API Calls 模組化、

在前端作業中,串接 API 是我們很常進行的一個行為,在串接過程中,我們也可能會在每一次等待伺服器回傳結果前後進行一些固定的事情,例如加上 loading 提示、或在捕捉到錯誤時進行處理。假設我底下是我的一個組件:

<template> 
<div>
Input name: <input type="number" v-model="searchInput" />
<div>
<p>loading: {{ isLoading }}</p>
<ul>
<li v-for="(user, index) in users" :key="index">
{{ user }}
</li>
</ul>
</div>
</div>
</template>
<script>
import usersAPI from "@/apis/user.js";
import { ref, watch } from "vue";
export default {
name: "HelloWorld",
setup() {
const searchInput = ref(0);
const searchTimer = ref(null);
const isLoading = ref(false);
const users = ref([]);
async function fetchUsersByNumber(searchInput) {
isLoading.value = true;
users.value = [];
try {
users.value = await usersAPI.getUsersByNumber(searchInput);
} catch (error) {
alert(error);
} finally {
isLoading.value = false;
}
}
watch(searchInput, () => {
clearTimeout(searchTimer.value);
searchTimer.value = setTimeout(() => {
if (!Number.isNaN(searchInput.value) && searchInput.value > 0) {
fetchUsersByNumber(searchInput.value);
} else {
users.value = [];
}
}, 1500);
});
return { searchInput, isLoading, users };
},
};
</script>

searchInput 被使用者輸入停止後 1.5 秒,輸入為數字並且大於 0 時會透過 usersAPI.getUsersByNumber 向 API 伺服器抓取輸入數字並回傳該數量的名字到 users 陣列,最後透過 v-for 將結果渲染出來。

假設我在每一個組件中都一定會在串接 API 函式被呼叫時先將 isLoading 設為 true 提示使用者,並在錯誤時 alert 錯誤訊息,並在完成後將 isLoading 設為 false ,我就必須在每個組件中都進行重複的事情。這時就可以嘗試將共用的部分提取出來:

要提取的部分

utils 資料夾新增 APICaller.js ,加上提取出來的程式碼:

import { ref } from "vue";
export default function(fn) {
const isLoading = ref(false);
const results = ref(null);
const makeAPICall = async function(...args) {
isLoading.value = true;
results.value = null;
try {
results.value = await fn(...args);
} catch (error) {
alert(error);
} finally {
isLoading.value = false;
}
};
return { isLoading, results, makeAPICall };
}

將其撰寫為一個函式,其接收一個函式作為參數,並會透過 async/await 方式去呼叫它,運用擴展運算子 讓它可以接收多個參數,並會在接收到回傳結果時將值給予 results 。回到組件中將 script 改寫:

<script>
import usersAPI from "@/apis/user.js";
import APICaller from "@/utils/APICaller.js";
import { ref, watch } from "vue";
export default {
name: "HelloWorld",
setup() {
const searchInput = ref(0);
const searchTimer = ref(null);
const getAPIResponse = APICaller((search) =>
usersAPI.getUsersByNumber(search.value)
);
watch(searchInput, () => {
clearTimeout(searchTimer.value);
searchTimer.value = setTimeout(() => {
if (!Number.isNaN(searchInput.value) && searchInput.value > 0) {
getAPIResponse.makeAPICall(searchInput);
} else {
getAPIResponse.results.value = [];
}
}, 1500);
});
return { searchInput, ...getAPIResponse };
},
};
</script>

template 中的 v-for 也要改為

<li v-for=”(user, index) in results” :key=”index”>

(search) => usersAPI.getUsersByNumber(search.value) 這個箭頭函式傳入到 APICaller 中,並將 watch 中的函式稍作改寫,當呼叫 API 函式的條件達成時,透過 getAPIResponse.makeAPICall(searchInput) 去呼叫 APICaller 中的 async/await 函式。在 return 時將 getAPIResponse 擴展開來,讓 APICaller 中的 isLoading, results 可以被 Vue 解讀為模板中會用到的 ref ,如此一來我們就可以在任何組件中使用 APICaller 並節省下撰寫 isLoading,捕捉錯誤等的程式碼。

Suspense

假如我在一個父組件中有許多子組件,每個子組件都會在 setup 時進行 API 呼叫:

<template>
<img :src="randomImg" alt="randomImg" />
</template>
<script>
import usersAPI from "@/apis/user.js";
import { ref } from "vue";
export default {
name: "RandomUserImage",
async setup() {
const randomImg = ref(null);
randomImg.value = await usersAPI.getRandomUserImage();
return { randomImg };
},
};
</script>

我們可能會希望在所有 setup 完成前,可以在最上層使用一個 Spinner 等等的效果放在前面提示使用者稍候,這時候就可以使用 Suspense

<template>
<Suspense>
<template #default>
<div>
<RandomUserImage />
<RandomUserImage />
</div>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import RandomUserImage from "./components/RandomUserImage.vue";
export default {
name: "App",
components: {
RandomUserImage,
},
};
</script>

<template #default> 為我們使用的組件 RandomUserImage ,其在 setup 階段時會使用 async/await 進行 API 呼叫,最後將取得的照片回傳至模板;Suspense 會等待所有 API 呼叫時,在畫面上呈現 <template #fallback> 的內容,完成後,才將結果渲染出來:

進行 API 呼叫時,會先呈現 Loading…

Teleport

最後要記錄的則是 Teleport,這個功能可以將某個部份的 HTML 移動到另一個區塊中。

<Teleport> 其接收兩個屬性,to 代表它要被傳送到的地方,例如 .container #navbar [data-modal] 也可以是動態屬性 :to=”teleportTargetAttribute”disabled 為 false 時會被傳送到 to 指定的地方,反之則會回到原始的地點。承接 Suspense 的程式碼,加入一個可以操控 teleport 的參數 notTeleported

<template>
<Suspense>
<template #default>
<div>
<RandomUserImage />
<teleport to=".end" :disabled="notTeleported">
<RandomUserImage />
</teleport>
<br />
<button @click="notTeleported = !notTeleported">
Teleport the image!!
</button>
<p>notTeleported:{{ notTeleported }}</p>
<h1>--App.vue content stops here--</h1>
</div>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import RandomUserImage from "./components/RandomUserImage.vue";
export default {
name: "App",
components: {
RandomUserImage,
},
data() {
return {
notTeleported: true,
};
},
};
</script>

public/index.html 中加入 <div class=”end”></div>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<div class="end"></div>
</body>
</html>

notTeleported 的值為 false 時,圖片就會被傳送到 <div class=”end”></div> 的區塊。

這兩篇文多數內容是節錄於 Vue Mastery 的教學自行作的筆記,像是 Suspense<template> 中已經被調整為只能存在一個 root element ,這部分就和 Vue Mastery 不太一樣,不過如果打開 DevTool 也會發現 Console 清楚明瞭地寫著:<Suspense> is an experimental feature and its API will likely change. 表示這功能目前還有可能被調整。

關於 Composition API 的記錄就到這邊,如果內容有任何錯誤還請大大們指出,當然如果這篇文有幫助到你就太好了。

--

--