在第 29 天,我新增了一個載入器(一個 <div>Loading ...</div>
)來顯示頁面正在載入資料。
在 Angular 20 中非常簡單,因為它是 httpResource
內建的功能。在 Vue 3 中,我安裝了 vueuse
並使用 useFetch
composable 來發起網路請求。
Framework | Approach |
---|---|
Vue 3 | vueuse's useFetch Composable |
SvelteKit | navigating and the error helper function |
Angular 20 | built-in in httpResource |
安裝 vueuse/core
npm install --save-exact @vueuse/core
將 usePost
composable 替換為 useFetch
composable。
export const postsUrl = 'https://jsonplaceholder.typicode.com/posts'
export const usersUrl = 'https://jsonplaceholder.typicode.com/users'
<script setup lang="ts">
import PostCard from '@/components/PostCard.vue';
import { postsUrl } from '@/constants/apiEndpoints';
import type { Post } from '@/types/post';
import { useFetch } from '@vueuse/core';
const {
data: posts,
isFetching,
error,
} = useFetch<Post[]>(postsUrl).json()
</script>
userFetch
接受一個 URL,該 URL 可以是字串或者是 ref
/shallowRef
。.json()
函式將回傳 JSON 格式的資料給 data
。
當資料正在載入時,isFetching
為 true,載入完成則為 false。
error
回傳在網路請求中發生的任何錯誤。
<template>
<div v-if="isFetching" class="text-center mb-10">Loading ...</div>
<div v-if="error" class="text-center mb-10">{{ error }}</div>
<div v-if="posts" class="flex flex-wrap flex-grow">
<p class="ml-2 w-full">Number of posts: {{ posts.length }}</p>
<PostCard v-for="post in posts" :key="post.id" :post="post" />
</div>
</template>
當 isFetching
為 true 時,div
元素會顯示靜態文字 Loading...
。當發生錯誤時,{{ error }}
顯示網路錯誤訊息。當端點成功回傳貼文時,v-for
指令會迭代陣列並渲染 PostCard
元件。
載入指示器和錯誤訊息顯示在 +layout.svelte
中。
<script lang="ts">
import { page, navigating } from '$app/state';
let { children } = $props();
</script>
{#if navigating.to}
<div>Loading page...</div>
{:else if page.error}
{page.error.message}
{:else}
<div class="container">
{@render children?.()}
</div>
{/if}
當 navigation.to
不為 null 時,頁面正在導覽並載入資料。因此,div
元素會顯示靜態文字 Loading page...
。
如果 page.error
是一個 Error
物件,則 page.error.message
顯示錯誤訊息。
import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
// retreive all posts
export const load: PageServerLoad = async ({ fetch }) => {
const postResponse = await fetch(`${BASE_URL}/posts`);
if (!postResponse.ok) {
error(404, {
message: 'Failed to fetch posts'
});
}
const posts = (await postResponse.json()) as Post[];
return { posts };
};
load
函式會檢查回應是否不正常,並使用 error
輔助函式 (helper function) 丟出包含自訂訊息的 404 錯誤。
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { PostcardComponent } from '../post/postcard.component';
import { PostsService } from '../post/services/posts.service';
@Component({
selector: 'app-home',
imports: [PostcardComponent],
template: `... inline template ...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {
postService = inject(PostsService);
postsRes = this.postService.posts;
posts = computed(() => (this.postsRes.hasValue() ? this.postsRes.value() : []));
error = computed<string>(() =>
this.postsRes.status() === 'error' ? 'Error loading the posts.' : '',
);
}
當 postsRes
資源的狀態為錯誤時,error
計算信號會回傳自訂的錯誤訊息。否則,錯誤訊息為空字串。
@if (postsRes.isLoading()) {
<div>Loading...</div>
} @else if (error()) {
<div>Error: {{ error() }}</div>
}
@if (posts(); as allPosts) {
<div class="flex flex-wrap flex-grow">
<p class="ml-2 w-full">Number of posts: {{ allPosts.length }}</p>
@for (post of allPosts; track post.id) {
<app-postcard [post]="post" />
}
</div>
}
當 postsRes.isLoading()
為 true 時,div
元素會顯示靜態文字 Loading...
。
當 error
計算信號 (computed signal) 評估為 true 時,會顯示錯誤訊息。
當 allPosts
已定義時,模板會顯示貼文數量並迭代陣列來渲染 PostComponent
。
首先,useFetch
組合函式透過路徑參數取得貼文。這很簡單,因為此 URL 僅依賴路徑參數 (path param),且可從 useRoute
組合函式解構取得。
<script setup lang="ts">
import { postsUrl, usersUrl } from '@/constants/apiEndpoints'
import type { Post } from '@/types/post'
import type { User } from '@/types/user'
import { useFetch } from '@vueuse/core'
import { computed, shallowRef, watch } from 'vue'
import { useRoute } from 'vue-router'
const { params } = useRoute()
const url = `${postsUrl}/${params.id}`
const {
data: post,
isFetching: isFetchingPost,
error: errorPost
} = useFetch<Post>(url).json()
</script>
當貼文取得後,使用 useFetch
組合函式根據貼文的使用者 ID 取得使用者。
使用者 URL 會隨使用者 ID 變動,因此它是個 shallowRef
。
const userUrl = shallowRef('')
const {
data: user,
isFetching: isFetchingUser,
error: errorUser,
} = useFetch<User>(userUrl, { refetch: true, immediate: false }).json()
refetch: true
表示當 userUrl
改變時會發出新的請求。此外,userUrl
初始為空字串,因此不希望它立即觸發。最終的 useFetchOptions
如下:
{
refetch: true,
immediate: true,
}
修改watcher
來追蹤貼文並以程式方式更新 userUrl
的 shallowRef。
watch(
() => ({ ...post.value }),
({ userId = undefined }) => (userUrl.value = userId ? `${usersUrl}/${userId}` : ''),
)
當 userUrl
非空白時,useFetch
組合函式會自動根據使用者 ID 取得使用者。
const isFetching = computed(() => isFetchingPost.value || isFetchingUser.value)
新增 isFetching
計算參考 (computed ref) 以在貼文或使用者載入時顯示載入器。
const error = computed(() => {
if (errorPost.value) {
return errorPost instanceof Error ? errorPost.message : 'Error retrieving a post.'
}
if (errorUser.value) {
return errorUser instanceof Error ? errorUser.message : 'Error retrieving a user.'
}
return ''
})
新增 error
計算參考 (computed ref) 以顯示任何錯誤訊息。
<template>
<div v-if="isFetching" class="text-center my-10">Loading...</div>
<div v-if="error" class="text-center my-10">{{ error }}</div>
<div v-if="post && user" class="mb-10">
<h1 class="text-3xl">{{ post.title }}</h1>
<div class="text-gray-500 mb-10">by {{ user.name }}</div>
<div class="mb-10">{{ post.body }}</div>
</div>
</template>
當 isFetching
為 true 時顯示載入器,錯誤訊息非空白時顯示錯誤訊息。模板在兩者資料皆成功載入後顯示貼文與使用者名稱。
import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import type { PostWitUser, User } from '$lib/types/user';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
// retreive a post by an ID
export const load: PageServerLoad = async ({ params, fetch }): Promise<PostWitUser> => {
const post = (await retrieveResource(fetch, `posts/${+params.id}`, 'Post')) as Post;
const user = (await retrieveResource(fetch, `users/${post.userId}`, 'User')) as User;
return {
post,
user,
};
};
retrieveResource
輔助函式 (helper function) 使用原生的 fetch
函式根據貼文 ID 取得貼文。當貼文成功取得後,此輔助函式使用貼文的使用者 ID 取得使用者。接著,load
函式會將貼文和使用者一併回傳給 +page.svelte
。
type FetchFunction = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
async function retrieveResource(fetch: FetchFunction, subPath: string, itemName: string) {
const url = `${BASE_URL}/${subPath}`;
const response = await fetch(url);
if (!response.ok) {
error(404, {
message: `Failed to fetch ${itemName}`
});
}
const item = await response.json();
if (!item) {
error(404, {
message: `${itemName} does not exist`
});
}
return new Promise((resolve) => {
setTimeout(() => resolve(item), 1000);
});
}
retrieveResource
會抓取該項目並檢查回應,當回應不正常時,會丟出帶有自訂訊息 Failed to fetch ${itemName}
的 404 錯誤。await response.json()
會將 Promise 解析成物件,若該物件為 undefined,也會丟出帶有自訂訊息 ${itemName} does not exist
的 404 錯誤。
return new Promise((resolve) => {
setTimeout(() => resolve(item), 1000);
});
該 Promise 會延遲一秒以模擬載入行為。
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
import { UserService } from './services/user.service';
import { Post } from './types/post.type';
@Component({
selector: 'app-post',
styles: `
@reference "../../styles.css";
:host {
@apply flex m-2 gap-2 items-center w-1/4 flex-grow rounded overflow-hidden w-full;
}
`,
template: `... inline template ...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class PostComponent {
readonly userService = inject(UserService);
post = input<Post>();
userRef = this.userService.createUserResource(this.post);
user = computed(() => (this.userRef.hasValue() ? this.userRef.value() : undefined));
error = computed<string>(() =>
this.userRef.status() === 'error' ? 'Error loading the post.' : '',
);
}
@let myUser = user();
@let myPost = post();
@if (userRef.isLoading()) {
<div>Loading...</div>
} @else if (error()) {
<div>Error: {{ error() }}</div>
} @else if (myPost && myUser) {
<div class="mb-10">
<h1 class="text-3xl">{{ myPost.title }}</h1>
<div class="text-gray-500 mb-10">by {{ myUser.name }}</div>
<div class="mb-10">{{ myPost.body }}</div>
</div>
} @else {
<div>Post not found</div>
}
當 userRef.isLoading()
為 true 時,div
元素會顯示靜態文字 Loading...
。
當 error
計算信號評估為 true 時,會顯示錯誤訊息。
最後的 elseif
顯示貼文標題、貼文內容和使用者名稱。
我們已成功在每個頁面實作簡易的載入器和錯誤指示器。