Skip to content

Angular 升级指南(一):Angular 13-15 新特性详解

Published: at 20:004 min read
目录

本文是 Angular 升级系列教程的第一篇,覆盖 Angular 13、14、15 三个版本。系列文章:


Angular 13(2021年11月)

1. 彻底移除 View Engine,全面使用 Ivy

Angular 13 是一个分水岭——旧的 View Engine 渲染引擎被完全移除,Ivy 成为唯一的渲染引擎。这意味着:

升级要点: 如果你的项目依赖了还在用 View Engine 的第三方库,需要等库作者更新或寻找替代方案。

2. 动态组件创建简化

不再需要 ComponentFactoryResolver,可以直接传递组件类:

// ❌ Angular 12 及之前
@Component({
  selector: "app-parent",
  template: "<ng-container #container></ng-container>",
})
export class ParentComponent {
  @ViewChild("container", { read: ViewContainerRef })
  container!: ViewContainerRef;

  constructor(private cfr: ComponentFactoryResolver) {}

  createComponent() {
    const factory = this.cfr.resolveComponentFactory(ChildComponent);
    this.container.createComponent(factory);
  }
}

// ✅ Angular 13+
@Component({
  selector: "app-parent",
  template: "<ng-container #container></ng-container>",
})
export class ParentComponent {
  @ViewChild("container", { read: ViewContainerRef })
  container!: ViewContainerRef;

  createComponent() {
    this.container.createComponent(ChildComponent);
  }
}

3. HttpClientModuleAPM 改进

Angular 13 改进了 HttpContext,允许在 HTTP 请求中传递元数据:

import { HttpContext, HttpContextToken } from '@angular/common/http';

const IS_CACHE_ENABLED = new HttpContextToken<boolean>(() => false);

// 发送请求时附带上下文
this.http.get('/api/data', {
  context: new HttpContext().set(IS_CACHE_ENABLED, true)
});

// 在拦截器中读取
intercept(req: HttpRequest<any>, next: HttpHandler) {
  if (req.context.get(IS_CACHE_ENABLED)) {
    // 走缓存逻辑
  }
  return next.handle(req);
}

4. Angular CLI 持久化构建缓存

默认启用构建缓存,显著提升重复构建速度:

// angular.json
{
  "cli": {
    "cache": {
      "enabled": true,
      "path": ".angular/cache",
      "environment": "all"
    }
  }
}

5. TypeScript 4.4 支持

支持 TypeScript 4.4,带来更好的类型推断和控制流分析。

6. 其他变更

升级命令:

ng update @angular/core@13 @angular/cli@13

Angular 14(2022年6月)

1. Typed Reactive Forms(类型化表单)⭐

这是 Angular 14 最重要的特性。FormControlFormGroupFormArray 终于有了严格的类型推断:

// ❌ Angular 13 及之前 — 所有值都是 any
const form = new FormGroup({
  name: new FormControl(""),
  age: new FormControl(0),
});
const name = form.value.name; // any 😢

// ✅ Angular 14+ — 完整类型推断
const form = new FormGroup({
  name: new FormControl("", { nonNullable: true }),
  age: new FormControl(0, { nonNullable: true }),
  email: new FormControl<string | null>(null),
});

const name = form.value.name; // string ✅
const age = form.value.age; // number ✅
const email = form.value.email; // string | null ✅

nonNullable 选项确保 reset() 时恢复初始值而非 null

const name = new FormControl("默认值", { nonNullable: true });
name.reset(); // 值变为 '默认值',而不是 null

FormBuilder 也支持了类型化:

// 使用 NonNullableFormBuilder
constructor(private fb: NonNullableFormBuilder) {}

ngOnInit() {
  this.form = this.fb.group({
    name: [''],
    age: [0],
    tags: this.fb.array(['angular', 'typescript']),
  });
  // 所有字段自动推断类型,且 nonNullable
}

升级要点: 如果现有代码有大量 FormControl,可以先用 UntypedFormControl 等过渡 API 保持兼容,再逐步迁移:

// 过渡方案 — 行为与旧版完全一致
import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";

const form = new UntypedFormGroup({
  name: new UntypedFormControl(""),
});

2. Standalone Components(独立组件)开发者预览版 🧪

Angular 14 引入了 Standalone Components 的概念——组件不再必须属于某个 NgModule

// 独立组件 — 不需要声明在任何 NgModule 中
@Component({
  standalone: true,
  selector: "app-hello",
  imports: [CommonModule], // 直接在组件级别导入依赖
  template: `
    <h1>Hello {{ name }}</h1>
    <p *ngIf="showDetails">这是一个独立组件</p>
  `,
})
export class HelloComponent {
  @Input() name = "World";
  showDetails = true;
}

// 在其他组件中使用
@Component({
  standalone: true,
  imports: [HelloComponent], // 直接导入组件
  template: '<app-hello name="Angular" />',
})
export class AppComponent {}

独立组件也可以用于路由:

// 路由配置 — 直接使用独立组件
const routes: Routes = [
  {
    path: "dashboard",
    loadComponent: () =>
      import("./dashboard.component").then(m => m.DashboardComponent),
  },
];

3. inject() 函数

可以在构造函数之外使用 inject() 获取依赖:

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

@Component({
  selector: "app-user",
  template: "...",
})
export class UserComponent {
  // 不再需要 constructor(private http: HttpClient)
  private http = inject(HttpClient);
  private router = inject(Router);
  private activatedRoute = inject(ActivatedRoute);

  loadUser() {
    return this.http.get("/api/user");
  }
}

inject() 在创建可复用的工具函数时特别有用:

// 可复用的工具函数
function injectDestroy() {
  const subject = new Subject<void>();
  const ref = inject(DestroyRef);
  ref.onDestroy(() => {
    subject.next();
    subject.complete();
  });
  return subject.asObservable();
}

// 在组件中使用
@Component({ ... })
export class MyComponent {
  private destroy$ = injectDestroy();

  ngOnInit() {
    this.someObservable$.pipe(
      takeUntil(this.destroy$)
    ).subscribe();
  }
}

4. 页面标题策略(Title Strategy)

路由配置中可以直接设置页面标题:

const routes: Routes = [
  { path: "", component: HomeComponent, title: "首页" },
  { path: "about", component: AboutComponent, title: "关于我们" },
  { path: "products", component: ProductsComponent, title: "产品列表" },
];

// 自定义标题策略
@Injectable({ providedIn: "root" })
export class CustomTitleStrategy extends TitleStrategy {
  constructor(private title: Title) {
    super();
  }

  override updateTitle(routerState: RouterStateSnapshot) {
    const title = this.buildTitle(routerState);
    if (title) {
      this.title.setTitle(`我的应用 | ${title}`);
    }
  }
}

// 注册自定义策略
providers: [{ provide: TitleStrategy, useClass: CustomTitleStrategy }];

5. 增强的模板诊断

编译器能检测出更多模板中的问题:

// 编译器会警告:双向绑定中的空值合并操作符无效
<input [(ngModel)]="user.name ?? '默认值'" />
// ⚠️ Warning: nullish coalescing in two-way binding is not supported

// 编译器会警告:未使用的 ngFor 变量
<div *ngFor="let item of items; let i = index">
  {{ item.name }}
  <!-- ⚠️ 如果 i 未使用会有提示 -->
</div>

6. 其他变更

升级命令:

ng update @angular/core@14 @angular/cli@14

Angular 15(2022年11月)

1. Standalone APIs 正式稳定 ✅

Angular 15 将 Standalone Components/Directives/Pipes 标记为稳定版,并提供了完整的独立应用引导方式:

// main.ts — 不再需要 AppModule
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()],
});

提供了 provide* 系列函数替代 Module imports:

// ❌ 旧方式 — 通过 Module
@NgModule({
  imports: [
    RouterModule.forRoot(routes),
    HttpClientModule,
    BrowserAnimationsModule,
  ],
})
export class AppModule {}

// ✅ 新方式 — 通过 provide 函数
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(
      withInterceptors([authInterceptor]),
      withFetch() // 使用 fetch API 替代 XMLHttpRequest
    ),
    provideAnimations(),
  ],
});

2. 函数式路由守卫和解析器

不再需要创建类来实现守卫,直接用函数:

// ❌ Angular 14 及之前 — 类守卫
@Injectable({ providedIn: "root" })
export class AuthGuard implements CanActivate {
  constructor(
    private auth: AuthService,
    private router: Router
  ) {}

  canActivate(): boolean {
    if (this.auth.isLoggedIn()) return true;
    this.router.navigate(["/login"]);
    return false;
  }
}

// ✅ Angular 15+ — 函数式守卫
export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isLoggedIn()) return true;
  return router.createUrlTree(["/login"]);
};

// 路由配置
const routes: Routes = [
  {
    path: "dashboard",
    canActivate: [authGuard],
    component: DashboardComponent,
  },
];

函数式解析器:

// 函数式 Resolver
export const userResolver: ResolveFn<User> = route => {
  const userService = inject(UserService);
  return userService.getUser(route.paramMap.get("id")!);
};

const routes: Routes = [
  {
    path: "user/:id",
    component: UserDetailComponent,
    resolve: { user: userResolver },
  },
];

3. 函数式 HTTP 拦截器

// ❌ 旧方式 — 类拦截器
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const token = inject(AuthService).getToken();
    const authReq = req.clone({
      headers: req.headers.set("Authorization", `Bearer ${token}`),
    });
    return next.handle(authReq);
  }
}

// ✅ Angular 15+ — 函数式拦截器
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();
  const authReq = req.clone({
    headers: req.headers.set("Authorization", `Bearer ${token}`),
  });
  return next(authReq);
};

// 注册
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor, loggingInterceptor])),
  ],
});

4. Directive Composition API(指令组合 API)

可以在组件的 hostDirectives 中组合多个指令:

// 定义可复用的指令
@Directive({
  standalone: true,
  selector: "[tooltip]",
  host: { "(mouseenter)": "show()", "(mouseleave)": "hide()" },
})
export class TooltipDirective {
  @Input() tooltip = "";
  show() {
    /* 显示提示 */
  }
  hide() {
    /* 隐藏提示 */
  }
}

@Directive({
  standalone: true,
  selector: "[highlight]",
  host: { "(mouseenter)": "onHover()", "(mouseleave)": "onLeave()" },
})
export class HighlightDirective {
  @Input() highlightColor = "yellow";
  onHover() {
    /* 高亮 */
  }
  onLeave() {
    /* 取消高亮 */
  }
}

// 在组件中组合指令
@Component({
  selector: "app-fancy-button",
  hostDirectives: [
    {
      directive: TooltipDirective,
      inputs: ["tooltip"],
    },
    {
      directive: HighlightDirective,
      inputs: ["highlightColor"],
      outputs: [],
    },
  ],
  template: "<button><ng-content /></button>",
})
export class FancyButtonComponent {}

// 使用时自动拥有两个指令的能力
// <app-fancy-button tooltip="点击提交" highlightColor="blue">提交</app-fancy-button>

5. Image Directive(NgOptimizedImage)稳定版

Angular 15 将图片优化指令标记为稳定版:

import { NgOptimizedImage } from "@angular/common";

@Component({
  standalone: true,
  imports: [NgOptimizedImage],
  template: `
    <!-- 自动优化:lazy loading、fetchpriority、srcset 等 -->
    <img ngSrc="hero.jpg" width="800" height="600" priority />

    <!-- 自动生成 srcset -->
    <img
      ngSrc="product.jpg"
      width="400"
      height="300"
      ngSrcset="200w, 400w, 800w"
    />

    <!-- 填充模式 -->
    <div style="position: relative; width: 100%; height: 300px;">
      <img ngSrc="banner.jpg" fill />
    </div>
  `,
})
export class ImageExampleComponent {}

配置图片 CDN loader:

// app.config.ts
import { provideImageKitLoader } from "@angular/common";

providers: [provideImageKitLoader("https://ik.imagekit.io/your_id")];

6. DestroyReftakeUntilDestroyed

更优雅地处理组件销毁时的清理工作:

import { DestroyRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

@Component({
  selector: "app-data",
  template: "{{ data }}",
})
export class DataComponent {
  data = "";

  constructor() {
    // 方式一:takeUntilDestroyed — 组件销毁时自动取消订阅
    inject(HttpClient)
      .get("/api/data")
      .pipe(takeUntilDestroyed())
      .subscribe(res => (this.data = res));
  }

  // 方式二:DestroyRef — 手动注册清理回调
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    const timer = setInterval(() => console.log("tick"), 1000);
    this.destroyRef.onDestroy(() => clearInterval(timer));
  }
}

7. 路由懒加载改进

支持懒加载独立组件和路由配置:

const routes: Routes = [
  // 懒加载独立组件
  {
    path: "profile",
    loadComponent: () =>
      import("./profile.component").then(m => m.ProfileComponent),
  },
  // 懒加载子路由配置
  {
    path: "admin",
    loadChildren: () =>
      import("./admin/admin.routes").then(m => m.ADMIN_ROUTES),
  },
];

// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
  { path: "", component: AdminDashboardComponent },
  { path: "users", component: AdminUsersComponent },
];

8. 其他变更

升级命令:

ng update @angular/core@15 @angular/cli@15

# 自动迁移到 standalone(可选)
ng generate @angular/core:standalone

升级路径总结

版本TypeScriptNode.js关键升级动作
134.412.20+ / 14.15+移除 View Engine 相关代码
144.6-4.714.15+ / 16.10+迁移到 Typed Forms(或先用 Untyped* 过渡)
154.814.20+ / 16.13+迁移到 Standalone + provide* 函数

每次升级建议使用 Angular 官方的升级指南:https://angular.dev/update-guide

# 逐版本升级(推荐)
ng update @angular/core@13 @angular/cli@13
ng update @angular/core@14 @angular/cli@14
ng update @angular/core@15 @angular/cli@15

下一篇:Angular 升级指南(二):Angular 16-18 新特性详解