new 服務()
;Angular 幫你建立並共享實例,只要在元件裡「要」就會給(注入)。一句話:把資料與邏輯往 Service 收斂,Component 變更輕盈、好測試、好維護。
先建立資料型別(介面),再做服務檔。這樣 TS 能幫你在編輯時就抓出型別錯誤。
在 src/app/models/
建兩個檔:
skill.model.ts
export type SkillCategory = 'frontend' | 'backend' | 'tools';
export interface Skill {
name: string;
category: SkillCategory;
}
project.model.ts
export interface Project {
title: string;
tech: string; // 簡短技術堆疊說明
desc: string; // 專案描述
link: string; // Demo 或 Repo 連結
}
在專案根目錄執行:
ng g s services/skills-data
ng g s services/projects-data
會產生:
src/app/services/
├─ skills-data.service.ts
└─ projects-data.service.ts
服務預設使用 providedIn: 'root',代表全域單例,不用手動加到 providers。
skills-data.service.ts
import { Injectable } from '@angular/core';
import { Skill, SkillCategory } from '../models/skill.model';
@Injectable({ providedIn: 'root' })
export class SkillsDataService {
private readonly data: Skill[] = [
{ name: 'HTML / CSS / SCSS', category: 'frontend' },
{ name: 'TypeScript', category: 'frontend' },
{ name: 'Angular / React / Vue', category: 'frontend' },
{ name: 'Node.js / Express', category: 'backend' },
{ name: 'Git / GitHub / Docker', category: 'tools' },
{ name: 'Vite / Webpack', category: 'tools' }
];
// 取得全部
getAll(): Skill[] {
// 回傳拷貝,避免外部直接改到原資料
return [...this.data];
// 若日後要打 API,在這裡換成 HttpClient 呼叫即可
}
// 依類別取得(供未來需要時使用)
getByCategory(cat: SkillCategory): Skill[] {
return this.data.filter(s => s.category === cat);
}
}
projects-data.service.ts
import { Injectable } from '@angular/core';
import { Project } from '../models/project.model';
@Injectable({ providedIn: 'root' })
export class ProjectsDataService {
private readonly data: Project[] = [
{
title: '毛毛購物(寵物電商)',
tech: 'Angular + Node.js|購物車、結帳、RWD',
desc: '主導前端架構,完成商品列表、購物流程與訂單頁。',
link: '#'
},
{
title: 'LINE Bot 預約系統',
tech: 'Cloud Functions + LINE API|時段預約',
desc: '整合 LINE 聊天介面與雲端排程,完成會員預約流程。',
link: '#'
}
];
getAll(): Project[] {
return [...this.data];
}
}
Day 9、10 我們把資料直接寫在元件內。從今天開始,改成「向服務要資料」。功能與畫面不變,但結構更乾淨、可重用。
skills.component.ts
(修改)
import { Component, OnInit } from '@angular/core';
import { Skill, SkillCategory } from '../../models/skill.model';
import { SkillsDataService } from '../../services/skills-data.service';
type Category = 'all' | SkillCategory;
@Component({
selector: 'app-skills',
templateUrl: './skills.component.html',
styleUrls: ['./skills.component.scss']
})
export class SkillsComponent implements OnInit {
current: Category = 'all';
keyword = '';
// 從服務取得的原始資料
private all: Skill[] = [];
// 經過分類/關鍵字過濾後,給模板渲染的資料
view: Skill[] = [];
constructor(private skillsSvc: SkillsDataService) {}
ngOnInit(): void {
this.all = this.skillsSvc.getAll();
this.applyFilter();
}
setFilter(cat: Category) {
this.current = cat;
this.applyFilter();
}
// 關鍵字即時更新(若使用 (input) 事件)
onKeywordChange() {
this.applyFilter();
}
private applyFilter() {
const kw = this.keyword.trim().toLowerCase();
this.view = this.all.filter(s => {
const byCat = this.current === 'all' || s.category === this.current;
const byKw = !kw || s.name.toLowerCase().includes(kw);
return byCat && byKw;
});
}
trackByName(_i: number, s: Skill) { return s.name; }
}
skills.component.html
(幾乎不變,改接 view
與輸入框事件)
<section id="skills" class="container section" aria-labelledby="skills-title">
<div class="section-header">
<h2 id="skills-title">技能 Skillset</h2>
<div role="tablist" aria-label="技能分類" class="filters">
<button role="tab" class="chip" (click)="setFilter('all')" [attr.aria-selected]="current==='all'">全部</button>
<button role="tab" class="chip" (click)="setFilter('frontend')" [attr.aria-selected]="current==='frontend'">前端</button>
<button role="tab" class="chip" (click)="setFilter('backend')" [attr.aria-selected]="current==='backend'">後端</button>
<button role="tab" class="chip" (click)="setFilter('tools')" [attr.aria-selected]="current==='tools'">工具</button>
</div>
</div>
<div class="field" style="margin:12px 0;">
<label for="skill-search">關鍵字搜尋</label>
<input id="skill-search" type="text" [(ngModel)]="keyword" (input)="onKeywordChange()" placeholder="例如:Angular、Docker…" />
<small class="muted">結果:{{ view.length }} 項</small>
</div>
<ul class="skill-grid">
<li *ngFor="let s of view; trackBy: trackByName">
{{ s.name }}
</li>
</ul>
</section>
projects.component.ts
(修改)
import { Component, OnInit } from '@angular/core';
import { ProjectsDataService } from '../../services/projects-data.service';
import { Project } from '../../models/project.model';
@Component({
selector: 'app-projects',
templateUrl: './projects.component.html',
styleUrls: ['./projects.component.scss']
})
export class ProjectsComponent implements OnInit {
projects: Project[] = [];
constructor(private projectsSvc: ProjectsDataService) {}
ngOnInit(): void {
this.projects = this.projectsSvc.getAll();
}
trackByTitle(_i: number, p: Project) { return p.title; }
}
projects.component.html
(加上 trackBy)
<section id="projects" class="container section">
<h2>作品集 Projects</h2>
<div class="project-grid">
<article class="card" *ngFor="let project of projects; trackBy: trackByTitle">
<h3>{{ project.title }}</h3>
<p class="muted">{{ project.tech }}</p>
<p>{{ project.desc }}</p>
<a class="btn small" [href]="project.link" target="_blank">Live Demo</a>
</article>
</div>
</section>
models/
放型別、services/
放資料來源、components/
放 UI。const svc = new SkillsDataService()
constructor(private skillsSvc: SkillsDataService) {}
skills = [...]
寫死元件getAll()
,元件只負責過濾與呈現return [...this.data]
)ngFor="let s of skillsSvc.getAll() | filter:kw"
async
pipeasync
pipe(為接 API 做準備)真接 API(HttpClient.get<Skill[]>())會回傳 Observable。你可以先把服務介面長得像未來的樣子,改成回傳 Observable<Skill[]>,元件用 | async 取值。
服務(節錄)
import { of, Observable } from 'rxjs';
getAll$(): Observable<Skill[]> {
return of(this.data); // 未來換成 this.http.get<Skill[]>(url)
}
元件(節錄)
skills$ = this.skillsSvc.getAll$(); // 在模板用 | async 取值
模板(節錄)
<li *ngFor="let s of (skills$ | async)">{{ s.name }}</li>
開始導入 Routing(路由) 與 頁面結構:
/projects/:id
子頁