文档
Angular Hello World — 组件与双向绑定
目标
创建 Angular 应用,展示组件、双向绑定、服务注入、路由和 HTTP 请求。
完整代码
项目创建
ng new angular-demo --routing --style=scss
cd angular-demo
ng generate component counter
ng generate component todo
ng generate service todo
ng serve
根组件 src/app/app.component.ts
import { Component } from "@angular/core";
@Component({
selector: "app-root",
template: `
<main class="app">
<h1>🅰️ Angular Demo</h1>
<p class="subtitle">企业级前端框架的 Hello World</p>
<nav>
<a routerLink="/counter">计数器</a>
<a routerLink="/todo">待办事项</a>
</nav>
<router-outlet></router-outlet>
</main>
`,
styles: [`
.app {
max-width: 560px; margin: 40px auto; padding: 0 16px;
font-family: -apple-system, 'Microsoft YaHei', sans-serif;
}
h1 { text-align: center; }
.subtitle { text-align: center; color: #666; }
nav { display: flex; gap: 16px; justify-content: center; margin: 20px 0; }
nav a {
padding: 8px 20px; border-radius: 8px;
text-decoration: none; background: #f1f5f9; color: #333;
transition: 0.2s;
}
nav a:hover, nav a.active { background: #3b82f6; color: white; }
`],
})
export class AppComponent {
title = "angular-demo";
}
路由 src/app/app.routes.ts
import { Routes } from "@angular/router";
import { CounterComponent } from "./counter/counter.component";
import { TodoComponent } from "./todo/todo.component";
export const routes: Routes = [
{ path: "", redirectTo: "/counter", pathMatch: "full" },
{ path: "counter", component: CounterComponent },
{ path: "todo", component: TodoComponent },
];
计数器组件 src/app/counter/counter.component.ts
import { Component, signal } from "@angular/core";
@Component({
selector: "app-counter",
template: `
<div class="card">
<h2>🔢 计数器</h2>
<p class="hint">(使用 Angular Signals API)</p>
<div class="count">{{ count() }}</div>
<div class="buttons">
<button class="btn-inc" (click)="increment()">+ 1</button>
<button class="btn-dec" (click)="decrement()">- 1</button>
<button class="btn-reset" (click)="reset()">重置</button>
</div>
@if (count() >= 10) {
<div class="achievement">🎉 达到 {{ count() }} 次!</div>
}
</div>
`,
styles: [`
.card { text-align: center; padding: 24px; }
.hint { color: #888; font-size: 0.85rem; }
.count { font-size: 3rem; font-weight: 700; margin: 12px 0; }
.buttons button {
padding: 10px 22px; border: none; border-radius: 8px;
font-size: 1rem; cursor: pointer; margin: 4px; color: white;
}
.btn-inc { background: #10b981; }
.btn-dec { background: #ef4444; }
.btn-reset { background: #64748b; }
.achievement {
margin-top: 16px; padding: 12px;
background: #fef3c7; border-radius: 8px;
}
`],
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update((c) => c + 1);
}
decrement() {
this.count.update((c) => c - 1);
}
reset() {
this.count.set(0);
}
}
Todo 服务 src/app/todo.service.ts
import { Injectable } from "@angular/core";
export interface TodoItem {
id: number;
text: string;
done: boolean;
}
@Injectable({ providedIn: "root" })
export class TodoService {
private todos: TodoItem[] = [
{ id: 1, text: "学 Angular", done: true },
{ id: 2, text: "写项目", done: false },
];
getAll(): TodoItem[] {
return this.todos;
}
add(text: string): void {
this.todos.push({
id: Date.now(),
text,
done: false,
});
}
toggle(id: number): void {
const todo = this.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
}
remove(id: number): void {
this.todos = this.todos.filter((t) => t.id !== id);
}
}
Todo 组件 src/app/todo/todo.component.ts
import { Component, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { TodoService, TodoItem } from "../todo.service";
@Component({
selector: "app-todo",
standalone: true,
imports: [FormsModule],
template: `
<div class="card">
<h2>📋 待办事项</h2>
<div class="input-row">
<input
[(ngModel)]="newTodo"
(keydown.enter)="addTodo()"
placeholder="添加新任务..."
/>
<button class="btn-primary" (click)="addTodo()">添加</button>
</div>
<ul>
@for (todo of todos; track todo.id) {
<li class="todo-item">
<span [class.done]="todo.done" (click)="toggleTodo(todo.id)">
{{ todo.text }}
</span>
<div>
<button
class="btn-sm"
[style.background]="todo.done ? '#f59e0b' : '#10b981'"
(click)="toggleTodo(todo.id)"
>
{{ todo.done ? "↩" : "✓" }}
</button>
<button class="btn-sm btn-del" (click)="removeTodo(todo.id)">
✕
</button>
</div>
</li>
}
</ul>
<p class="status">
{{ doneCount() }} / {{ todos.length }} 项完成
</p>
</div>
`,
styles: [`
.card { padding: 24px; }
.input-row { display: flex; gap: 8px; margin-bottom: 16px; }
.input-row input {
flex: 1; padding: 10px; border: 2px solid #e2e8f0;
border-radius: 8px; font-size: 1rem;
}
.btn-primary {
padding: 10px 20px; border: none; border-radius: 8px;
background: #3b82f6; color: white; cursor: pointer;
}
.todo-item {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid #f1f5f9;
}
.done { text-decoration: line-through; color: #94a3b8; }
.btn-sm { border: none; border-radius: 6px; padding: 4px 10px;
color: white; cursor: pointer; margin-left: 4px; }
.btn-del { background: #ef4444; }
.status { margin-top: 12px; color: #888; font-size: 0.9rem; }
`],
})
export class TodoComponent {
todos: TodoItem[] = [];
newTodo = "";
constructor(private todoService: TodoService) {
this.todos = this.todoService.getAll();
}
doneCount() {
return this.todos.filter((t) => t.done).length;
}
addTodo() {
if (this.newTodo.trim()) {
this.todoService.add(this.newTodo.trim());
this.newTodo = "";
}
}
toggleTodo(id: number) {
this.todoService.toggle(id);
}
removeTodo(id: number) {
this.todoService.remove(id);
this.todos = this.todoService.getAll();
}
}
运行步骤
ng serve
# 访问 http://localhost:4200
预期输出
- 导航栏切换计数器 / 待办事项两个页面
- 计数器使用 Signal 响应式更新
- 待办事项支持添加、完成、删除
- 服务注入(Dependency Injection)自动管理 TodoService 单例