本文是 Angular 升级系列教程的第一篇,覆盖 Angular 13、14、15 三个版本。系列文章:
- (一)Angular 13-15 新特性详解(本文)
- (二)Angular 16-18 新特性详解
- (三)Angular 19-21 新特性详解
Angular 13(2021年11月)
1. 彻底移除 View Engine,全面使用 Ivy
Angular 13 是一个分水岭——旧的 View Engine 渲染引擎被完全移除,Ivy 成为唯一的渲染引擎。这意味着:
- 不再需要
enableIvy配置 - 所有库都必须发布 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. HttpClientModule 中 APM 改进
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. 其他变更
TestBed自动清理 DOM(不再需要手动destroyAfterEach)routerLink支持null和undefined(禁用链接)- Adobe Fonts 内联支持
- 移除了 IE11 支持
升级命令:
ng update @angular/core@13 @angular/cli@13
Angular 14(2022年6月)
1. Typed Reactive Forms(类型化表单)⭐
这是 Angular 14 最重要的特性。FormControl、FormGroup、FormArray 终于有了严格的类型推断:
// ❌ 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. 其他变更
- 支持 TypeScript 4.7
ng completion命令行自动补全@angular/cdk支持Dialog和Listbox原语ngOnDestroy可以注入使用- 可选注入器(
inject()支持{ optional: true })
升级命令:
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. DestroyRef 和 takeUntilDestroyed
更优雅地处理组件销毁时的清理工作:
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. 其他变更
- 支持 TypeScript 4.8
@angular/router支持RouterModule.forRoot(routes, { bindToComponentInputs: true })— 路由参数自动绑定到组件 Inputesbuild构建器实验性支持ng generate默认生成 standalone 组件(v15.2+)- MDC-based Angular Material 组件(Material Design Components for Web)
- 移除了
@angular/flex-layout的官方支持
升级命令:
ng update @angular/core@15 @angular/cli@15
# 自动迁移到 standalone(可选)
ng generate @angular/core:standalone
升级路径总结
| 版本 | TypeScript | Node.js | 关键升级动作 |
|---|---|---|---|
| 13 | 4.4 | 12.20+ / 14.15+ | 移除 View Engine 相关代码 |
| 14 | 4.6-4.7 | 14.15+ / 16.10+ | 迁移到 Typed Forms(或先用 Untyped* 过渡) |
| 15 | 4.8 | 14.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