全书目录

第二十六篇:Angular 完全指南 —— 企业中控园区宇宙

第十章:完整代码示例 —— 一个最小企业园区通讯录系统

1 分钟 349 字 第 471 / 962 个阅读单元

下面这个示例把 Angular 的关键主线串在一起:

  • standalone
  • component
  • template
  • directive
  • service
  • dependency injection
  • router
  • RxJS
#1. main.ts
ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app.component';
import { routes } from './app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ],
});
#2. app.routes.ts
ts
import { Routes } from '@angular/router';
import { DirectoryComponent } from './directory.component';
import { DepartmentPageComponent } from './department-page.component';

export const routes: Routes = [
  {
    path: '',
    component: DirectoryComponent,
    title: '园区通讯录',
  },
  {
    path: 'departments/:id',
    component: DepartmentPageComponent,
    title: '部门详情',
  },
];
#3. dispatch.service.ts
ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Department {
  id: string;
  name: string;
  manager: string;
  online: boolean;
}

@Injectable({
  providedIn: 'root',
})
export class DispatchService {
  private http = inject(HttpClient);

  search(keyword: string): Observable<Department[]> {
    return this.http.get<Department[]>('/api/departments', {
      params: { q: keyword },
    });
  }

  getOne(id: string): Observable<Department> {
    return this.http.get<Department>(`/api/departments/${id}`);
  }
}
#4. online-badge.directive.ts
ts
import { Directive, ElementRef, Input, OnChanges, inject } from '@angular/core';

@Directive({
  selector: '[appOnlineBadge]',
  standalone: true,
})
export class OnlineBadgeDirective implements OnChanges {
  private el = inject(ElementRef<HTMLElement>);

  @Input('appOnlineBadge') online = false;

  ngOnChanges() {
    const native = this.el.nativeElement;
    native.style.paddingLeft = '12px';
    native.style.borderLeft = this.online ? '4px solid #10893e' : '4px solid #8a8a8a';
  }
}
#5. directory.component.ts
ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import {
  debounceTime,
  distinctUntilChanged,
  startWith,
  switchMap,
} from 'rxjs/operators';
import { DispatchService } from './dispatch.service';
import { OnlineBadgeDirective } from './online-badge.directive';

@Component({
  selector: 'campus-directory',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule, RouterLink, OnlineBadgeDirective],
  template: `
    <main class="panel">
      <h1>企业中控园区通讯录</h1>

      <input
        [formControl]="keyword"
        placeholder="搜索部门或负责人"
      />

      <p>输入会经过防抖,再触发最新请求。</p>

      <ul>
        <li
          *ngFor="let item of departments$ | async"
          [appOnlineBadge]="item.online"
        >
          <a [routerLink]="['/departments', item.id]">{{ item.name }}</a>
          <span>负责人:{{ item.manager }}</span>
          <strong>{{ item.online ? '在线' : '离线' }}</strong>
        </li>
      </ul>
    </main>
  `,
})
export class DirectoryComponent {
  private dispatch = inject(DispatchService);

  keyword = new FormControl('', { nonNullable: true });

  departments$ = this.keyword.valueChanges.pipe(
    startWith(''),
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((keyword) => this.dispatch.search(keyword))
  );
}
#6. department-page.component.ts
ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { map, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { DispatchService } from './dispatch.service';

@Component({
  selector: 'department-page',
  standalone: true,
  imports: [CommonModule, RouterLink],
  template: `
    <section *ngIf="department$ | async as department; else loading">
      <a routerLink="/">返回通讯录</a>
      <h2>{{ department.name }}</h2>
      <p>负责人:{{ department.manager }}</p>
      <p>状态:{{ department.online ? '在线' : '离线' }}</p>
    </section>

    <ng-template #loading>
      <p>部门信息加载中...</p>
    </ng-template>
  `,
})
export class DepartmentPageComponent {
  private route = inject(ActivatedRoute);
  private dispatch = inject(DispatchService);

  department$ = this.route.paramMap.pipe(
    map((params) => params.get('id') ?? ''),
    distinctUntilChanged(),
    switchMap((id) => this.dispatch.getOne(id))
  );
}
#7. app.component.ts
ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: `
    <router-outlet />
  `,
})
export class AppComponent {}
#8. 这套代码背后的流程图
text
用户输入关键字
      │
      ▼
FormControl.valueChanges
      │
      ▼
debounceTime / distinctUntilChanged
      │
      ▼
switchMap -> DispatchService.search()
      │
      ▼
HttpClient 返回 Observable<Department[]>
      │
      ▼
模板 async pipe 订阅
      │
      ▼
目录列表渲染

点击某个部门
      │
      ▼
Router 导航到 /departments/:id
      │
      ▼
ActivatedRoute.paramMap
      │
      ▼
switchMap -> DispatchService.getOne(id)
      │
      ▼
详情组件渲染

这就是 Angular 的真实工作方式:

组件负责楼,模板负责面板,路由负责道路,服务负责公共能力,DI 负责供给,RxJS 负责事件流。