iT邦幫忙

2025 iThome 鐵人賽

DAY 30
1
Vue.js

作為 Angular 專家探索 Vue 3 和 Svelte 5系列 第 30

第 29 天 - 新增 Loader 與 Error 狀態

  • 分享至 

  • xImage
  •  

在第 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

在 Home 實作載入與錯誤狀態

Vue 3 application

安裝 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 元件。

SvelteKit application

載入指示器和錯誤訊息顯示在 +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 錯誤。

Angular 20 application

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

在 Post 實作載入與錯誤狀態

Vue 3 application

首先,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 時顯示載入器,錯誤訊息非空白時顯示錯誤訊息。模板在兩者資料皆成功載入後顯示貼文與使用者名稱。

SvelteKit application

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 會延遲一秒以模擬載入行為。

Angular 20 application

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 顯示貼文標題、貼文內容和使用者名稱。

我們已成功在每個頁面實作簡易的載入器和錯誤指示器。

Github Repositories

資源


上一篇
第 28 天 - 取得貼文作者
系列文
作為 Angular 專家探索 Vue 3 和 Svelte 530
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言