SKILL.md
唯讀
名稱
angular-http
描述
在 Angular v20+ 中使用 resource()、httpResource() 和 HttpClient 實作 HTTP 資料擷取。適用於 API 呼叫、以 signal 載入資料、請求/回應處理以及攔截器。觸發時機:資料擷取、API 整合、載入狀態、錯誤處理,或將基於 Observable 的 HTTP 轉換為基於 signal 的模式。
Angular HTTP 與資料擷取
使用基於 signal 的 resource()、httpResource() 以及傳統的 HttpClient 在 Angular 中擷取資料。
httpResource() - 基於 Signal 的 HTTP
httpResource() 將 HttpClient 包裝成基於 signal 的狀態管理:
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
template: `
@if (userResource.isLoading()) {
<p>載入中...</p>
} @else if (userResource.error()) {
<p>錯誤:{{ userResource.error()?.message }}</p>
<button (click)="userResource.reload()">重試</button>
} @else if (userResource.hasValue()) {
<h1>{{ userResource.value().name }}</h1>
<p>{{ userResource.value().email }}</p>
}
`,
})
export class UserProfile {
userId = signal('123');
// 回應式 HTTP 資源 - 當 userId 改變時重新擷取
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
}
httpResource 選項
// 簡單的 GET 請求
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
// 完整的請求選項
userResource = httpResource<User>(() => ({
url: `/api/users/${this.userId()}`,
method: 'GET',
headers: { 'Authorization': `Bearer ${this.token()}` },
params: { include: 'profile' },
}));
// 設定預設值
usersResource = httpResource<User[]>(() => '/api/users', {
defaultValue: [],
});
// 當參數為 undefined 時跳過請求
userResource = httpResource<User>(() => {
const id = this.userId();
return id ? `/api/users/${id}` : undefined;
});
資源狀態
// 狀態 signal
userResource.value() // 目前值或 undefined
userResource.hasValue() // 布林值 - 是否有已解析的值
userResource.error() // 錯誤或 undefined
userResource.isLoading() // 布林值 - 是否正在載入
userResource.status() // 'idle' | 'loading' | 'reloading' | 'resolved' | 'error' | 'local'
// 動作
userResource.reload() // 手動觸發重新載入
userResource.set(value) // 設定區域值
userResource.update(fn) // 更新區域值
resource() - 通用非同步資料
用於非 HTTP 的非同步操作或自訂擷取邏輯:
import { resource, signal } from '@angular/core';
@Component({...})
export class Search {
query = signal('');
searchResource = resource({
// 回應式參數 - 改變時觸發重新載入
params: () => ({ q: this.query() }),
// 非同步載入器函式
loader: async ({ params, abortSignal }) => {
if (!params.q) return [];
const response = await fetch(`/api/search?q=${params.q}`, {
signal: abortSignal,
});
return response.json() as Promise<SearchResult[]>;
},
});
}
帶預設值的 Resource
todosResource = resource({
defaultValue: [] as Todo[],
params: () => ({ filter: this.filter() }),
loader: async ({ params }) => {
const res = await fetch(`/api/todos?filter=${params.filter}`);
return res.json();
},
});
// value() 回傳 Todo[](絕不會是 undefined)
條件式載入
const userId = signal<string | null>(null);
userResource = resource({
params: () => {
const id = userId();
// 回傳 undefined 以跳過載入
return id ? { id } : undefined;
},
loader: async ({ params }) => {
return fetch(`/api/users/${params.id}`).then(r => r.json());
},
});
// 當 params 回傳 undefined 時,狀態為 'idle'
HttpClient - 傳統方式
用於複雜情境或需要 Observable 運算子時:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({...})
export class Users {
private http = inject(HttpClient);
// 將 Observable 轉換為 Signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] }
);
// 或直接使用 Observable
users$ = this.http.get<User[]>('/api/users');
}
HTTP 方法
private http = inject(HttpClient);
// GET
getUser(id: string) {
return this.http.get<User>(`/api/users/${id}`);
}
// POST
createUser(user: CreateUserDto) {
return this.http.post<User>('/api/users', user);
}
// PUT
updateUser(id: string, user: UpdateUserDto) {
return this.http.put<User>(`/api/users/${id}`, user);
}
// PATCH
patchUser(id: string, changes: Partial<User>) {
return this.http.patch<User>(`/api/users/${id}`, changes);
}
// DELETE
deleteUser(id: string) {
return this.http.delete<void>(`/api/users/${id}`);
}
請求選項
this.http.get<User[]>('/api/users', {
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json',
},
params: {
page: '1',
limit: '10',
sort: 'name',
},
observe: 'response', // 取得完整的 HttpResponse
responseType: 'json',
});
攔截器
函式攔截器(建議使用)
// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(Auth);
const token = authService.token();
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
}
return next(req);
};
// error.interceptor.ts
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
inject(Router).navigate(['/login']);
}
return throwError(() => error);
})
);
};
// logging.interceptor.ts
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const started = Date.now();
return next(req).pipe(
tap({
next: () => console.log(`${req.method} ${req.url} - ${Date.now() - started}ms`),
error: (err) => console.error(`${req.method} ${req.url} failed`, err),
})
);
};
註冊攔截器
// app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor,
errorInterceptor,
loggingInterceptor,
])
),
],
};
錯誤處理
使用 httpResource
@Component({
template: `
@if (userResource.error(); as error) {
<div class="error">
<p>{{ getErrorMessage(error) }}</p>
<button (click)="userResource.reload()">重試</button>
</div>
}
`,
})
export class UserCmpt {
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
getErrorMessage(error: unknown): string {
if (error instanceof HttpErrorResponse) {
return error.error?.message || `Error ${error.status}: ${error.statusText}`;
}
return '發生非預期的錯誤';
}
}
使用 HttpClient
import { catchError, retry } from 'rxjs';
getUser(id: string) {
return this.http.get<User>(`/api/users/${id}`).pipe(
retry(2), // 最多重試 2 次
catchError((error: HttpErrorResponse) => {
console.error('擷取使用者時發生錯誤:', error);
return throwError(() => new Error('無法載入使用者'));
})
);
}
載入狀態模式
@Component({
template: `
@switch (dataResource.status()) {
@case ('idle') {
<p>請輸入搜尋關鍵字</p>
}
@case ('loading') {
<app-spinner />
}
@case ('reloading') {
<app-data [data]="dataResource.value()" />
<app-spinner size="small" />
}
@case ('resolved') {
<app-data [data]="dataResource.value()" />
}
@case ('error') {
<app-error
[error]="dataResource.error()"
(retry)="dataResource.reload()"
/>
}
}
`,
})
export class Data {
query = signal('');
dataResource = httpResource<Data[]>(() =>
this.query() ? `/api/search?q=${this.query()}` : undefined
);
}
如需進階模式,請參閱 references/http-patterns.md。






