大家好,這篇文我們來看一個也可以用來發起 HTTP 請求的庫 Alova.js。Alova.js 是一個請求策略庫(Request strategy library),讓我們可以根據所需用簡潔的程式碼撰寫出符合當下情境的 HTTP 請求。
Alova.js 擁有什麼優勢?
在前端開發領域中,對於想要解決的問題幾乎都能找到對應的框架或庫來節省開發者寶貴的時間。因此當我們想去使用或是將其取代既有的框架或庫時,有很大可能就是因為它擁有一些該領域中的老大哥不具備的優勢,我們來看看 Alova.js 相較老大哥 axios 有什麼優勢。
更輕量
相較於 axios v1.3.6 minizipped size 為 11.4 kb,Alova.js v2.1.2 為 4.4kb,容量是前者的 38%。此外,它還支援 Tree shaking,實際打包的容量能在妥善應用下再更小。
對 Vue.js 開發者和中文開發者更友善
Alova.js 的官方文件中,除了提供簡體中文翻譯外,範例程式碼大量使用了 Vue 3 作為基底:
個人推測這兩種因素可能是因為它的作者是來自中國的開發者。 在 issue 中也能看到許多以中文為主的議題。
請求場景模型(Request Scene Model)
Alova.js 提出了請求場景模型的概念。我們在發起 HTTP 請求時往往需要考慮類似情況:
- 什麼時候該發出請求?
- 請求失敗需要進行重試嗎?
- 網速過慢或無網路時如何操作請求?
- …
以往我們在使用 axios 時,上述狀況我們都需要自行撰寫程式碼去實作,Alova.js 針對上述及更多情境從準備發起請求直到取得資料並進行處理後的所有階段進行抽象,讓開發者能寫出更好維護的程式碼。
開始使用
我們可以先建立一個測試用 API 伺服器。打開一個全新專案,輸入 npm init -y
後,輸入 npm install express cors
,接著建立一個 index.js
:
const express = require('express');
const app = express();
const cors = require('cors');
app.use(cors());
app.get('/api/', function(req, res) {
setTimeout(() => {
res.json(['John', 'Jane', 'Bob']);
}, 2000);
});
app.get('/api/names', function(req, res) {
const nameList = ['John', 'Jane', 'Bob'];
const { search='' } = req.query;
setTimeout(() => {
res.json(nameList.filter(name => name === search));
}, 2000);
});
app.get('/api/names/:page', function(req, res) {
const { page } = req.params;
const index = Number(page) - 1;
let responseJson = [['John', 'Jane', 'Bob'], ['Joe', 'Johnson', 'Tom'], ['Albert', 'Rose', 'Zod']];
setTimeout(() => {
res.json(responseJson[index] || []);
}, 2000);
})
app.listen(3000, function() {
console.log('Example app listening on port 3000!');
});
在目錄中輸入 node index.js
先啟動測試用 API 伺服器。
以 vite
的專案為起始,我們可以在一個基於 Vue 的 vite
專案中使用 npm 安裝:
npm install alova --save
我們直接採用並改寫官方入門提供的範例。把這段直接貼在 vite 專案中的 src\App.vue ,並在專案目錄輸入 npm run dev
啟動開發環境:
// 把這段直接貼在 vite 專案中的 src\App.vue
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<p v-for="(name, index) in data" :key="index">{{ name }}</p>
</div>
</template>
<script setup>
import { createAlova, useRequest } from 'alova';
import GlobalFetch from 'alova/GlobalFetch';
import VueHook from 'alova/vue';
const alovaInstance = createAlova({
baseURL: 'http://localhost:3000/',
statesHook: VueHook,
requestAdapter: GlobalFetch(),
responded: response => response.json()
});
const { loading, data, error } = useRequest(
alovaInstance.Get('/api/')
);
</script>
我們來看一下這段 Code,首先是建立實例。就像 axios
,在請求前我們可以先使用 createAlova
建立一個實例:
const alovaInstance = createAlova({
//...
});
- baseURL:和 axios 的用法類似,此為我們請求的根路徑
- statesHook:這邊我們使用由
alova/vue
引入的VueHook
。它的概念類似在告訴alova
:「請在我使用 use Hook 時照著此指南回傳我我需要的狀態資料」。除了VueHook
外,alova
也提供分別名為ReactHook
和SvelteHook
給同樣流行的前端框架React
及Svelte
。 - requestAdapter:請求配接器,這邊使用
alova
提供,基於fetch
API 實作的GlobalFetch
。 - responded:請求回來時的 Hook,上例是把 response 轉為 json。也可傳入一個包含
onSuccess
和onError
函式的物件,例如改寫自官方提供的範例:
const alovaInstance = createAlova({
baseURL: 'http://localhost:3000/',
statesHook: VueHook,
requestAdapter: GlobalFetch(),
responded: {
// 請求成功的攔截器
onSuccess: async (response) => {
if (response.status >= 400) {
throw new Error(response.statusText);
}
const json = await response.json();
if (json.code !== 200) {
throw new Error(json.message);
}
return json.data;
},
// 請求失敗的攔截器
onError: (err) => {
console.error(err)
alert(err);
}
}
});
值得注意的是,onError
並不是在伺服器端回傳客戶端錯誤(4XX)或伺服器端錯誤(5XX)的 Status Code,而是請求本身發生問題時才觸發,例如當我提供一個請求的時間限制 timeout
:
const alovaInstance = createAlova({
baseURL: 'http://localhost:3000/',
statesHook: VueHook,
requestAdapter: GlobalFetch(),
timeout: 200,
responded: {
// 請求成功的攔截器
onSuccess: async (response) => {
if (response.status >= 400) {
throw new Error(response.statusText);
}
const json = await response.json();
if (json.code !== 200) {
throw new Error(json.message);
}
return json.data;
},
// 請求失敗的攔截器
onError: (err) => {
console.error(err)
alert(err);
}
}
});
由於範例使用的 API 伺服器會延遲兩秒回應,超過了我們請求設定的 timeout
,這樣的情況下才會進到 onError
的攔截器。
接下來,我們看一下實際的請求:
const { loading, data, error } = useRequest(
alovaInstance.Get('/api/')
);
使用 useRequest
進行請求時,可以解構出 loading
, data
和 error
三個變數,分別代表:
- loading:請求是否已回應
- data:回應的資料
- error:請求是否發生客戶端或伺服器端錯誤
搭配 template
部分看:
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<p v-for="(name, index) in data" :key="index">{{ name }}</p>
</div>
</template>
就能看出「當請求等待回應時,我們渲染 Loading... 文字;當請求已被回應且發生錯誤時,我們渲染錯誤的資訊;否則遍歷並渲染回應的資料」。
除了 useRequest
外,我們也能使用對應七種 HTTP 請求方法 GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH 等包含在 alovaInstance
的 method 進行請求:
const alovaInstance = createAlova({
baseURL: 'http://localhost:3000/',
requestAdapter: GlobalFetch(),
});
const apiGetter = alovaInstance.Get('/api/');
alovaInstance.Get(‘/api/’)
這段語法看起來很像 axios.get(‘/api’)
乍看之下很像已經發出請求了,實際上它是建立了一個等待發出的請求實例。要真的發出請求,需要呼叫它的 send
方法:
const apiGetter = alovaInstance.Get('/api/');
const response = await apiGetter.send();
const parsedData = await response.json();
console.log(parsedData); // ['John', 'Jane', 'Bob']
除了 useRequest
外,useWatcher
也是一個很棒的 use hook,它可以在監測到指定的狀態發生變化時立即(或依照開發者的定義延遲)發送請求。改寫自官方的範例:
// 整份覆蓋掉 App.vue
<template>
<input v-model="search" />
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<p v-for="(name, index) in data" :key="index">{{ name }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { createAlova, useWatcher } from 'alova';
import GlobalFetch from 'alova/GlobalFetch';
import VueHook from 'alova/vue';
const alovaInstance = createAlova({
baseURL: 'http://localhost:3000/',
statesHook: VueHook,
requestAdapter: GlobalFetch(),
responsed: (response) => response.json()
});
const filterNames = search => {
return alovaInstance.Get('/api/names', {
params: {
search
}
});
};
const search = ref('');
const { loading, data, error } = useWatcher(
() => filterNames(search.value),
[search],
{
debounce: 500
}
);
</script>
我們先透過 alovaInstance.get
建立一個請求實例,並透過
const { loading, data, error } = useWatcher(
() => filterNames(search.value),
[search],
{
debounce: 500
}
);
將其作為第一個回呼函式中呼叫的函式,並將我們要監聽的 search
列在第二個參數的陣列中作為觀察名單,最後則傳入 debounce:500
讓 Alova.js 避免我們的請求在使用者完成填寫前就送出。
快取機制
上面 useWatcher 的例子進行改寫:
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<p v-for="(name, index) in data" :key="index">{{ name }}</p>
</div>
<div class="pagination">
<a v-for="page in totalPages" :key="page" href="javascript:;" @click="setCurrentPage(page)">{{ page }}</a>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { createAlova, useWatcher } from 'alova';
import GlobalFetch from 'alova/GlobalFetch';
import VueHook from 'alova/vue';
const alovaInstance = createAlova({
baseURL: 'http://localhost:3000/',
statesHook: VueHook,
requestAdapter: GlobalFetch(),
responsed: (response) => response.json()
});
const getNamesByPage = currentPage => {
return alovaInstance.Get(`/api/names/${currentPage}`,{
localCache: {
expire: 0
}
});
};
const currentPage = ref(1);
const totalPages = ref(3);
const setCurrentPage = (newPage) => {
currentPage.value = newPage
}
const { data, loading, error, onSuccess } = useWatcher(() => getNamesByPage(currentPage.value), [currentPage], {
immediate: true
});
</script>
<style>
.pagination {
display: flex;
gap: 4px;
justify-content: center;
}
</style>
預設情況下,Alova.js 會為我們快取住資料回傳的結果,例如上面的例子,在取得資料後,即使使用者頻繁切換頁面,Alova.js 也不會真的向伺服器端發出請求,直到使用者重新整理頁面。
我們可以透過將 expire
時間歸零,例如:
const getNamesByPage = currentPage => {
return alovaInstance.Get(`/api/names/${currentPage}`,{
localCache: {
expire: 0
}
});
};
Alova.js 就不會為我們快取住結果。如果希望即使重整頁面後,也為我們保留資料,我們可以則可以透過 restore
模式,例如:
const getNamesByPage = currentPage => {
return alovaInstance.Get(`/api/names/${currentPage}`,{
localCache: {
mode: 'restore',
expire: 60 * 10 * 1000
}
});
};
至於資料的儲存位置,目前的觀察是存在 LocalStorage
:
以上就是關於 Alova.js 我這邊試用的心得,我自己在照著官方文件操作時,有碰到 useFetcher
預加載資料的問題,由於試了很久都沒有找到問題,最後是提交了 issue 給作者。若作者回應了,我會再回頭更新 useFetcher
的使用心得(因為我覺得 useFetcher
的設計真的非常棒)。
除此之外,這個庫的使用方式很直觀、官方文件也很清楚,我自己覺得他跟我接觸 Master Style 時的感覺很像,認為是一個非常有潛力的 Library,未來若有機會也一定會嘗試在實作中使用他。
References: