Angular

技术栈
前端框架
typescriptsparxjsgoogleenterprise

概览

Angular 技术栈概览

Angular 是由 Google 维护的企业级前端框架,使用 TypeScript 开发。它提供完整的 SPA 解决方案,内置依赖注入、RxJS 响应式编程、模块化路由、表单验证等一整套工程化能力。

Angular 是什么?

  • 完整的 MVC/MVVM 框架,不像 React 只是视图库
  • 基于 TypeScript,强类型约束
  • CLI 脚手架,一键生成组件/服务/管道/模块
  • RxJS 驱动的事件流与异步处理

解决什么问题?

  • 大型企业应用的工程化架构
  • 团队协作需要强约束和规范
  • 表单密集型应用(内置 Reactive Forms)
  • 需要长期维护的大型项目

关键特性:

  • 双向数据绑定 [(ngModel)]
  • 依赖注入(DI)系统
  • Angular Universal(SSR)
  • Angular Material 组件库
  • 信号(Signals)— v16+ 新增细粒度响应式

安装

环境准备

  • 操作系统:macOS / Linux / Windows
  • Node.js:>= 18.x(推荐 20 LTS)
  • 包管理器:npm(Angular 官方推荐)
  • 全局 Angular CLInpm install -g @angular/cli
  • 编辑器:VS Code(安装 Angular Language Service 扩展)
  • TypeScript 基础:Angular 强制使用 TypeScript

安装命令

使用 Angular CLI 创建新项目

# 全局安装 CLI
npm install -g @angular/cli

# 创建项目
ng new my-angular-app

# 交互式选择:
# ┌  Would you like to add Angular routing? → Yes
# ├  Which stylesheet format would you like to use? → SCSS
# └  SSR / SSG? → 可先选 No

运行开发服务器

cd my-angular-app
ng serve --open
# 访问 http://localhost:4200

使用特定模板

# 带 Material Design
ng new my-app --style=scss --routing=true
cd my-app &;& ng add @angular/material

# 最小模板
ng new my-app --minimal --inline-template --inline-style

常见安装问题

1. ng 命令未找到

  • 解决:确保全局安装路径在 PATH 中;用 npx @angular/cli new my-app 替代

2. npm install 慢或失败

  • 解决:使用国内镜像
    npm config set registry https://registry.npmmirror.com
    ng config -g cli.packageManager npm
    

3. Windows 上 PowerShell 权限错误

  • 解决:以管理员运行 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

4. TypeScript 严格模式报错

  • 解决:初学者可在 tsconfig.json 中设置 "strict": false,但建议习惯严格模式

5. ng serve 端口冲突

  • 解决ng serve --port 4201

示例

Angular 学生列表组件

目标

创建典型的 Angular 组件:展示学生列表,支持搜索过滤和添加学生——涵盖 Component / Service / 双向绑定 / *ngFor / 管道。

完整代码

1. 创建项目并生成文件

ng new student-demo --routing --style scss
cd student-demo
ng g s services/student

2. src/app/services/student.service.ts

import { Injectable } from '@angular/core';

export interface Student {
  id: number;
  name: string;
  major: string;
  grade: number;
}

@Injectable({ providedIn: 'root' })
export class StudentService {
  private students: Student[] = [
    { id: 1, name: '张三', major: '计算机科学', grade: 89 },
    { id: 2, name: '李四', major: '软件工程', grade: 92 },
    { id: 3, name: '王五', major: '人工智能', grade: 78 },
  ];

  private nextId = 4;

  getStudents(): Student[] {
    return [...this.students];
  }

  addStudent(student: Omit<Student, 'id'>): void {
    this.students.push({ ...student, id: this.nextId++ });
  }

  deleteStudent(id: number): void {
    this.students = this.students.filter(s => s.id !== id);
  }
}

3. src/app/app.component.ts

import { Component, OnInit } from '@angular/core';
import { StudentService, Student } from './services/student.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  students: Student[] = [];
  searchTerm = '';
  newStudent = { name: '', major: '', grade: null as number | null };

  constructor(private studentService: StudentService) {}

  ngOnInit() {
    this.students = this.studentService.getStudents();
  }

  get filteredStudents(): Student[] {
    return this.students.filter(s =>
      s.name.includes(this.searchTerm) ||
      s.major.includes(this.searchTerm)
    );
  }

  addStudent() {
    if (!this.newStudent.name || !this.newStudent.major || !this.newStudent.grade) return;
    this.studentService.addStudent({ ...this.newStudent, grade: this.newStudent.grade! });
    this.newStudent = { name: '', major: '', grade: null };
    this.students = this.studentService.getStudents();
  }

  deleteStudent(id: number) {
    this.studentService.deleteStudent(id);
    this.students = this.studentService.getStudents();
  }
}

4. src/app/app.component.html

<div class="container">
  <h1>📋 学生管理系统</h1>

  <!-- 搜索 -->
  <input
    type="text"
    [(ngModel)]="searchTerm"
    placeholder="搜索姓名或专业..."
    class="search-input"
  />

  <!-- 添加表单 -->
  <div class="add-form">
    <input [(ngModel)]="newStudent.name" placeholder="姓名" />
    <input [(ngModel)]="newStudent.major" placeholder="专业" />
    <input [(ngModel)]="newStudent.grade" placeholder="成绩" type="number" />
    <button (click)="addStudent()">➕ 添加</button>
  </div>

  <!-- 学生列表 -->
  <table>
    <thead>
      <tr>
        <th>ID</th><th>姓名</th><th>专业</th><th>成绩</th><th>评级</th><th>操作</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let student of filteredStudents">
        <td>{{ student.id }}</td>
        <td>{{ student.name }}</td>
        <td>{{ student.major }}</td>
        <td>{{ student.grade }}</td>
        <td>{{ student.grade >= 90 ? '优秀' : student.grade >= 80 ? '良好' : '及格' }}</td>
        <td><button (click)="deleteStudent(student.id)">🗑️</button></td>
      </tr>
    </tbody>
  </table>
</div>

5. src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule],
  bootstrap: [AppComponent]
})
export class AppModule { }

6. src/styles.scss

body { font-family: system-ui; background: #f5f5f5; margin: 0; padding: 20px; }
.container { max-width: 800px; margin: 0 auto; }
.search-input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 6px; }
.add-form { display: flex; gap: 10px; margin: 10px 0;
  input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
  button { padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; }
}
table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden;
  th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
  th { background: #f8fafc; }
}

运行步骤

ng serve --open

预期输出

  • 首页显示预置的 3 名学生
  • 搜索框输入"计算机"过滤出张三
  • 添加新学生后自动刷新列表
  • 点击删除按钮移除学生

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 单例

教程

Angular 入门教程:企业级框架核心概念

一、Angular 是什么?

Angular 是由 Google 维护的企业级前端框架(2016 年发布 Angular 2,与 2010 年的 AngularJS 完全不同)。它是一个完整的解决方案——包含路由、表单、HTTP 客户端、状态管理、测试工具、SSR,开箱即用。使用 TypeScript 开发,强制采用 RxJS 进行异步编程。

核心哲学

"电池全包含"——不让你纠结选什么路由库、状态管理,Angular 都提供了官方方案。

二、核心概念地图

NgModule(模块)
  ├── Component(组件)—— 视图 + 逻辑
  │     ├── Template(HTML 模板)
  │     └── Class(TypeScript 类)
  ├── Service(服务)—— 业务逻辑,可注入
  │     └── @Injectable()
  ├── Pipe(管道)—— 数据转换
  └── Directive(指令)—— 行为增强

Standalone Components(Angular 15+)

@Component({
  standalone: true,           // 不再需要 NgModule
  imports: [CommonModule],    // 直接导入依赖
  template: `<h1>{{ title }}</h1>`,
})
export class MyComponent {
  title = "Hello";
}

三、数据绑定四种方式

<!-- 1. 插值:组件 → 模板 -->
<h1>{{ title }}</h1>

<!-- 2. 属性绑定:组件 → 模板(用方括号) -->
<img [src]="imageUrl" />

<!-- 3. 事件绑定:模板 → 组件(用圆括号) -->
<button (click)="onSave()">保存</button>

<!-- 4. 双向绑定:双向同步(香蕉盒 [()]) -->
<input [(ngModel)]="username" />

四、控制流(Angular 17+ 新语法)

<!-- @if / @else -->
@if (user(); as user) {
  <p>欢迎, {{ user.name }}</p>
} @else {
  <p>请登录</p>
}

<!-- @for(替代 *ngFor) -->
@for (item of items; track item.id; let i = $index) {
  <li>{{ i + 1 }}. {{ item.name }}</li>
} @empty {
  <li>列表为空</li>
}

<!-- @switch -->
@switch (status) {
  @case ("loading") { <spinner /> }
  @case ("error") { <error /> }
  @default { <content /> }
}

五、依赖注入(DI)

Angular 的核心优势之一:

@Injectable({ providedIn: "root" })  // 应用级单例
export class AuthService {
  private user = signal<User | null>(null);

  login(credentials: Credentials): Observable<User> {
    return this.http.post<User>("/api/login", credentials).pipe(
      tap((user) => this.user.set(user))
    );
  }
}

@Component({ /* ... */ })
export class LoginComponent {
  // 自动注入,无需手动创建
  constructor(private auth: AuthService) {}
}

六、Signal vs RxJS

Angular 16+ 引入 Signals,与 RxJS 互补:

Signal RxJS Observable
读取 count() 同步 .subscribe() 异步
用途 组件状态 异步流、HTTP、WebSocket
示例 count = signal(0) http.get("/api")
转换 computed() .pipe(map())
// 互操作
const obs$ = this.http.get<User[]>("/api/users");   // Observable
const users = toSignal(obs$, { initialValue: [] }); // 转为 Signal

const count = signal(0);
const count$ = toObservable(count);                  // 转为 Observable

七、路由

const routes: Routes = [
  { path: "", component: HomeComponent },
  { path: "products", component: ProductListComponent },
  { path: "products/:id", component: ProductDetailComponent },
  {
    path: "admin",
    canActivate: [AuthGuard],                    // 路由守卫
    loadChildren: () => import("./admin/admin.routes"), // 懒加载
  },
  { path: "**", component: NotFoundComponent },  // 通配 404
];
<!-- router-outlet 是路由组件的渲染出口 -->
<router-outlet />

<!-- routerLink 声明式导航 -->
<a routerLink="/products" routerLinkActive="active">产品</a>

八、HTTP 客户端

import { HttpClient } from "@angular/common/http";

@Injectable({ providedIn: "root" })
export class ProductService {
  constructor(private http: HttpClient) {}

  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>("/api/products");
  }

  createProduct(data: Partial<Product>): Observable<Product> {
    return this.http.post<Product>("/api/products", data);
  }

  deleteProduct(id: number): Observable<void> {
    return this.http.delete<void>(`/api/products/${id}`);
  }
}

九、最佳实践

  1. 使用 Standalone Components(Angular 15+),告别 NgModule 样板
  2. Signals 优先于传统双向绑定,逐步替代 RxJS 在组件状态中的使用
  3. OnPush 变更检测changeDetection: ChangeDetectionStrategy.OnPush 提升性能
  4. Lazy Loading:每个功能模块独立懒加载
  5. track 函数@for 中始终提供 track 表达式

十、思考题

  1. Angular 的依赖注入与 React Context 有什么本质区别?
  2. 为什么 Angular 选择 RxJS 作为异步基础,而不是 Promise?
  3. Signals 会完全取代 RxJS 在 Angular 中的地位吗?

Angular 毕设项目架构入门

背景

Angular 是 Google 维护的企业级前端框架。很多同学第一次接触 Angular 时会被它的"全家桶"吓到——模块、服务、依赖注入、装饰器、RxJS……概念太多。但实际上毕设项目用到的只是冰山一角,掌握核心模式即可高效开发。


核心概念

1. Angular 的三驾马车

概念 作用 类比
Component(组件) 页面 UI 单元 Vue 的 .vue 文件
Service(服务) 业务逻辑、数据获取 Vue 的 composable
Module(模块) 组织组件和服务 项目分区

2. 装饰器(Decorator)

Angular 大量使用 TypeScript 装饰器标记类:

@Component({ selector: 'app-student', templateUrl: './student.component.html' })
export class StudentComponent { }

@Injectable({ providedIn: 'root' })
export class ApiService { }

3. 依赖注入(DI)

这是 Angular 最核心的设计模式。不需要手动 new 对象,Angular 自动注入:

// 不需要:const api = new ApiService();
// 只需在构造函数声明:
constructor(private apiService: ApiService) { }

分步操作

第一步:创建项目

ng new biye-project --routing --style scss
cd biye-project
ng serve --open

第二步:规划模块结构

毕设推荐按功能划分模块:

src/app/
├── pages/
│   ├── home/          # ng g m pages/home --routing
│   ├── student/       # ng g m pages/student --routing
│   └── dashboard/     # ng g m pages/dashboard --routing
├── shared/            # 共享组件
│   └── components/    # ng g c shared/components/header
├── services/          # ng g s services/api
└── models/            # 接口/类型定义

第三步:路由配置

// app-routing.module.ts
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'students', loadChildren: () => import('./pages/student/student.module').then(m => m.StudentModule) },
  { path: '**', redirectTo: '' },
];

第四步:HTTP 服务

// services/api.service.ts
@Injectable({ providedIn: 'root' })
export class ApiService {
  constructor(private http: HttpClient) { }

  getStudents(): Observable<Student[]> {
    return this.http.get<Student[]>('/api/students');
  }

  addStudent(data: Student): Observable<Student> {
    return this.http.post<Student>('/api/students', data);
  }
}

第五步:组件中使用

export class StudentListComponent implements OnInit {
  students$ = this.api.getStudents();  // 直接订阅 Observable

  constructor(private api: ApiService) { }
}
<!-- 用 async 管道自动订阅 -->
<div *ngFor="let s of students$ | async">{{ s.name }}</div>

毕设常见场景

管理后台

  • Angular Material + 表格排序分页
  • Reactive Forms 表单验证

数据大盘

  • ECharts + Angular 封装
  • WebSocket 实时更新

前端独立部署

  • ng build --prod 输出 dist/
  • 部署到 Nginx / Vercel

思考题

  1. Angular 的依赖注入与手动 new 相比有什么优势?
  2. async 管道与手动 .subscribe() 的区别?为什么推荐 async 管道?
  3. 懒加载模块(loadChildren)对毕设项目有什么实际好处?

小结

Angular 虽然学习曲线较陡,但掌握 Component + Service + Routing 三板斧就可以完成大多数毕设需求。它的强约束反而让团队协作更规范,代码结构更统一。

参考资料

  1. [1] Google. Angular Documentation. 2024. https://angular.dev
  2. [2] Aristeidis Bampakos. Learning Angular. 2023.
  3. [3] Asim Hussain. Angular: From Theory to Practice. 2021.
  4. [4] Paul P. Daniels, Luis Atencio. RxJS in Action. 2017.
  5. [5] Google Angular 团队. Angular 官方文档. 2024. https://angular.dev/
  6. [6] Angular 团队. Angular Signals 深度指南. 2024. https://angular.dev/guide/signals
  7. [7] Angular 团队. Angular 风格指南. 2024. https://angular.dev/style-guide