iT邦幫忙

2025 iThome 鐵人賽

DAY 29
1

第 28 天 - 取得貼文作者

在第 28 天,我使用貼文的 userId 呼叫 users 端點來取得使用者名稱。

在 Vue 3 中,我建立了另一個 composable,回傳一個帶有 name 屬性的 user 物件。在 Angular 20 中,是一個使用實驗性 httpResourceUser 服務,當貼文更新時會取得使用者。在 SvelteKit 中,我在 load 函式中新增邏輯,透過貼文的 userId 查詢使用者。

Framework Approach
Vue 3 useUser Composable
SvelteKit Make a new request to the users endpoint to retrieve a user by user id
Angular 20 A user httpResource to retrieve a user by user id in a service

貼文與使用者類型

建立一個 User 類型以取得 ID 和名稱。

export type User = {
  id: number;
  name: string;
}

載入使用者資料

Vue 3 application

src/composables 資料夾下建立一個 useUser composable。

實作一個 fetchOne 函式並宣告一個 user 的 ref,以透過 ID 取得使用者。

import type { User } from '@/types/user'
import { ref } from 'vue'

export function useUser() {
  const user = ref<User | null>(null)
  const baseUrl = `https://jsonplaceholder.typicode.com/users/${id}`

  function fetchOne(id: number, signal: AbortSignal) {
    return fetch(baseUrl, { signal })
      .then((response) => response.json() as Promise<User>)
      .then((data) => (user.value = data))
  }

  return {
    user,
    fetchOne,
  }
}

fetchOne 函式會向 baseUrl 發出請求取得使用者,並將該使用者指派給 user.value

接著,該 composable 回傳 userfetchOne,以便 Post 元件可以存取。

SvelteKit application

我修改了程式碼,使用 async/await 來取得貼文和貼文作者。

export type PostWitUser = {
	post: Post | undefined;
	user: User | undefined;
};
import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import type { PostWitUser, User } from '$lib/types/user';
import type { PageLoad } from './$types';

// retreive a post by an ID
export const load: PageLoad = async ({ params, fetch }): Promise<PostWitUser> => {
	console.log('params', params);

	try {
		const response = await fetch(`${BASE_URL}/posts/${params.id}`);
		const post = (await response.json()) as Post;

		const userResponse = post ? await fetch(`${BASE_URL}/users/${post?.userId}`) : undefined;
		const user = userResponse ? ((await userResponse.json()) as User) : undefined;

		return {
			post,
			user
		};
	} catch (error) {
		console.error('Error fetching a post:', error);
		return {
			post: undefined,
			user: undefined
		};
	}
};

當第一個請求成功取得貼文後,第二個請求會使用 userId 來取得使用者。

Angular 20 application

安裝 zod 以解析 httpResource 的結果。

npm install --save-exact zod
import z from 'zod';

export const userSchema = z.object({
    id: z.number(),
    name: z.string(),
});

export type User = z.infer<typeof userSchema>;

建立一個 userSchema 並推斷 User 類型。

import { httpResource } from '@angular/common/http';
import { Injectable, Signal } from '@angular/core';
import { Post } from '../types/post.type';
import { User, userSchema } from '../types/user.type';

const BASE_URL = 'https://jsonplaceholder.typicode.com/users';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  createUserResource(post: Signal<Post | undefined>) {
    return httpResource<User>(
      () => {
        const result = post();
        return result ? `${BASE_URL}/${result.userId}` : undefined;
      },
      {
        defaultValue: undefined,
        equal: (a, b) => a?.id === b?.id,
        parse: userSchema.parse,
      },
    ).asReadonly();
  }
}

這是一種在服務中建立 httpResource 的技術。createUserResource 方法接受一個 post 信號以追蹤其變化。當貼文更新時,httpResource 會發出 HTTP 請求以根據使用者 ID 取得使用者。

{
    defaultValue: undefined,
    equal: (a, b) => a?.id === b?.id,
    parse: userSchema.parse,
}

預設值為 undefined。當兩個使用者具有相同 ID 且不發出 HTTP 請求時,視為相等。userSchema.parse 將原始資料解析為 User 物件。

將使用者模擬資料替換為 fetch 呼叫

Vue 3 application

Post 檢視中,匯入 usePost 組合函式並解構 fetchuser

Post 元件中,匯入 usePost 並調用 fetchOne 以藉由 ID 取得貼文。

import { useRoute } from 'vue-router'
import { usePost } from '@/composables/usePost'
import { useUser } from '@/composables/useUser'

const { post, fetchOne } = usePost()
const { user, fetchOne: fetchUser } = useUser()

const { params } = useRoute();
fetchOne(+params.id)
fetchUser(post?.userId)
<template>
  <div v-if="isLoading" class="text-center my-10">
    Loading...
  </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>

在模板中,將硬編碼的名稱替換為 {{ user.name }}

SvelteKit application

<script lang="ts">
    import type { PageProps } from './$types';

    const { data }: PageProps = $props();
    const { post, user } = data;
</script>

posts/[id]/+page.svelte 中,load 函式透過 ID 取得貼文,並根據貼文的 userId 取得使用者。

<div 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>

現在,模板可以透過插值 { user?.name } 顯示使用者名稱。

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({
  ... no change ...
})
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));
}

PostComponent 注入 userService 並呼叫 createUserResource 方法來建立 userRef

user 是一個計算信號 (computed signal),用來計算 user 的值。當 userRef 有值時,this.userRef.value() 會回傳有效的 User 物件。否則,user 會取得 undefined。

內嵌模板無需更改。

修正使用者錯誤(僅限 Vue 3)

此時,Post 檢視中有一個錯誤,導致 user 未定義。當在貼文資料到達之前調用 fetchUser(post?.userId) 時會發生此情況。解決方法是使用 watch 監聽 post ref 的變化。當 ref 收到新值時,監聽器回呼函式會呼叫 fetchUser 函式以取得使用者名稱。

watch(() => ({ ...post.value }), (newPost, oldPost, onCleanup) => {
  const controller = new AbortController()

  if (newPost?.userId) {
    fetchUser(newPost.userId, controller.signal)
  }

  onCleanup(() => {
    controller.abort()
  })
})

將一個 getter 函式傳遞給 watch 以追蹤貼文的更新。當新的貼文有使用者 ID 時,即會用該 ID 取得一個新的使用者並顯示。

我們已成功顯示貼文的使用者。

Github Repositories

Resources


上一篇
第27天 - 建立一個簡單的部落格頁面
下一篇
第 29 天 - 新增 Loader 與 Error 狀態
系列文
作為 Angular 專家探索 Vue 3 和 Svelte 530
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言