iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Vue.js

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

第18天 - Github Card 專案 第一部分 - 資料擷取

  • 分享至 

  • xImage
  •  

第18天,我開始在 Vue 3、Angular 19 和 Svelte 5 中進行 Github 使用者個人資料的練習。

我的 CSS 技術不強,因此我會遵循講師的建議使用 DaisyUI 來設計卡片樣式。我會把樣式放最後,集中精力在資料擷取和父子元件間的溝通。

這個小專案將分成三個部分:

  • 第一部分:資料擷取
  • 第二部分:父子元件溝通
  • 第三部分:樣式和模板
  • 第四部分:部署

讓我們先從資料擷取開始,你會發現三個框架在呼叫 API 取得資料的方法各不相同。

Vue 3 使用 Composable。
Angular 20 使用實驗性的 httpResource,因為我想嘗試不同。
Svelte 5 在 page.ts 中使用 data loader。

建立 Github Personal Token

在透過使用者名稱取得 Github 個人資料之前,必須先建立 Github personal access token

  • 點擊右上角的頭像選單以展開選單
  • 選擇 Settings
  • 點擊 Developer Settings
  • 展開 Personal access tokens,再點擊 Fine-grained tokens 以建立新的 token

定義 Github 環境變數

在 Vue 3 和 Svelte 5 中,由於使用 Vite,建立環境變數相對簡單,與 Angular 不同。

  • 建立一個 .env-example 檔案
VITE_GITHUB_TOKEN=<github personal access token>

建立一個環境變數來儲存 Github personal access token,該變數必須以 VITE 為前綴 (prefix)。

  • 建立一個 .env 檔案
VITE_GITHUB_TOKEN=github_xxx

建立 GithubProfile 類型

export type GithubProfile = {
    login: string;
    name: string;
    followers: number;
    following: number;
    html_url: string;
    avatar_url: string;
    bio: string | null;
    public_repos: number;
}

GithubProfile 類型在所有三個應用程式中相同。

一個 Github 個人資料包含使用者名稱、姓名、追蹤者數量、追蹤中數量、HTML URL、頭像 URL、可為 null 的個人簡介(bio)以及公開的 repositories 數量。

資料擷取

Vue 3 application

建立 Github Profile Composable

  • 在 src 目錄下建立一個 composables 資料夾
  • composables 資料夾中建立一個 useGithubProfile.ts 的 composable,用來根據使用者名稱取得 Github 個人資料。

此新的 composable 定義了三個 refs,分別用來儲存使用者名稱、個人資料和錯誤訊息。

export function useGithubProfile() {
    ... composable logic ...
}
export function useGithubProfile() {
    const username = ref('')
    const profile = ref<GithubProfile | null>(null)
    const error = ref('');
}

該 composable 會在 watch 中監控 username ref,並發出 fetch 呼叫以取得個人資料。

export function useGithubProfile() {
    const username = ref('')
    const profile = ref<GithubProfile | null>(null)
    const error = ref('');
    
    watch(username, async (newUserName) => {
        if (!newUserName) {
            profile.value = null;
            error.value = '';
            return;
        }

        profile.value = null;
        error.value = '';

        fetch(`https://api.github.com/users/${newUserName}`, {
            headers: {
                Authorization: `Bearer ${import.meta.env.VITE_GITHUB_TOKEN}`,
            }
        })
        .then( async response => {
            if (!response.ok) {
                error.value = 'Network response was not ok.'
            }
            profile.value = await response.json() as GithubProfile;
        })
        .catch(err => {
            console.error('There has been a problem with your fetch operation:', err);
            if (err instanceof Error) {
                error.value = err.message
            }
            error.value = 'Failed to fetch profile.'
        });
    });
}

watch 回呼函式 (callback) 會重設 profileerror 的 refs。Fetch 呼叫 Github API 以取得使用者個人資料。Authorization 標頭 (header) 會將個人 Github Token 作為 bearer token 儲存。

當 HTTP 請求成功時,profile ref 會更新為 JSON 回應;當發生 HTTP 錯誤時,error ref 會儲存錯誤訊息。

export function useGithubProfile() {
    ... previous logic ... 

    return {
        username,
        profile,
        error,
    }
}

最後,composable 會回傳 ref,以便在元件中可以存取它。

SvelteKit application

載入資料

已建立 routes/+page.ts 檔案來從GitHub 使用者名稱清單中載入資料。該 TypeScript 檔案包含一個 load 函式,此函式會迭代使用者名稱,呼叫 fetch 取得個人資料,並將這些資料以清單形式回傳。

export const load: PageLoad = async () => {
    const usernames = ['johnsoncodehk', 'antfu', 'railsstudent', 'danielkellyio', 'hootlex', 'MooseSaeed'];
    
    const profilesSettled = await Promise.allSettled(
        usernames.map((username) => fetchGithubProfile(username))
    );

    const profiles = profilesSettled.reduce((acc, result, idx) => {
        if (result.status === 'fulfilled') {
            return acc.concat({
                key: idx,
                profile: result.value,
            });
        } else {
            return acc.concat({
                key: idx,
                error: `Error fetching profile: ${result.reason}`
            });

        }
    }, [] as GithubProfileItem[])
    
	return {
		data: profiles
	};
};

Promise.allSettled 確保 fetch 呼叫無論成功或失敗都會完成。當請求成功時,清單會儲存唯一 key 和個人資料。否則,清單元素會包含唯一的 key 和錯誤訊息。

唯一 key 是在 Github Profile List 元件的列表中渲染個人資料所需的。

fetchGithubProfile 函式與 Vue Composable 中的 fetch 呼叫類似。

export function fetchGithubProfile(username: string, ): Promise<GithubProfile> {
    const url = `https://api.github.com/users/${username}`;
    return fetch(url, {
        headers: {
            Authorization: `Bearer ${import.meta.env.VITE_GITHUB_TOKEN}`,
        }
    })
    .then(async (response) => {
        if (!response.ok) {
            throw new Error(`GitHub profile not found for user: ${username}`);
        }
        
        const result = await response.json() as Promise<GithubProfile>;
        return result;
    })
    .catch((error) => {
        console.error('Error fetching GitHub profile:', error);
        throw error;
    });
}

當 HTTP 回應成功時,該函式會等待 JSON 回應並回傳 JSON 資料。當 HTTP 回應失敗時,該函式會記錄錯誤並拋出錯誤訊息。

Angular 20 application

定義 Github Token 建置變數

angular.json 中,定義 GITHUB_TOKEN 變數。

"define": {
    "GITHUB_TOKEN": "'secret value'"
}

secret_value 是一個虛擬值,會 command line 中被覆寫。

新增 src/types.d.ts 並宣告 GITHUB_TOKEN 常數。

declare const GITHUB_TOKEN: string;
export GITHUB_TOKEN=<github personal token>
ng build --define GITHUB_TOKEN=\'$GITHUB_TOKEN\'
serve dist

可於瀏覽器中開啟 http://localhost:3000 以啟動 Angular 應用程式。

另外,在 angular.json 中,architect -> build -> options 下,請新增 output_path 設定。將編譯輸出目錄設為 dist 資料夾,而非 dist/angular-github-profile 子資料夾。

"outputPath": {
  "base": "dist",
  "browser": ""
}

提供 HttpClient 和 fetch 功能

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withFetch()),
  ]
};

為了在 Angular 應用程式中使用 httpResource,app 設定的 providers 陣列必須包含 provideHttpClient 提供者 (provider)。HttpClient 預設使用 XmlHttpRequest,我透過 withFetch() 功能將其覆寫成使用 Fetch API。此做法是為了模擬 Vue 3 和 Svelte 5 範例的行為。

在 GithubProfileCardComponent 中定義 httpResource

在 Angular 元件中定義 httpResource 是最簡單的方法。因此,我建立了一個新的 GithubProfileCardComponent

@Component({
    selector: 'app-github-profile-card',
    template: `Working`,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GithubProfileCardComponent {}

該元件目前顯示靜態文字,因為尚無資料。

username = input.required<string>();

profileResource = httpResource<GithubProfile>(() => this.username() ? { 
    url: `https://api.github.com/users/${this.username()}`,
        method: 'GET',
        headers: {
            Authorization: `Bearer ${GITHUB_TOKEN}`
        }
    }: undefined, {
        equal: (a, b) => a?.login === b?.login,
    }
);

username 是一個 input signal,用來保存 Github 使用者名稱。

username 是響應式的,當使用者名稱變更時,profileResource 會發出 HTTP 請求以取得個人資料資源。

{
    equal: (a, b) => a?.login === b?.login,
}

當兩個個人資料具有相同的登入帳號時,profileResource 不會觸發變更偵測,且模板不會重新渲染。

profile = computed(() => this.profileResource.hasValue() ?this.profileResource.value() : undefined);

error = computed(() => this.profileResource.error()?.message || '');

profile 是一個計算信號 (computed signal),用來取得 Github 個人資料的值。它會顯示資源值或未定義。

error 計算信號 (computed signal) 會顯示錯誤訊息或空字串。

我們已成功在三個框架中取得 Github 個人資料。

Github Repositories

Github Pages

Resources


上一篇
第 17 天 - 在 HTML 模板中渲染動態內容
系列文
作為 Angular 專家探索 Vue 3 和 Svelte 519
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言