一、为什么要学习 Angular 新特性
1.1 旧版 Angular 开发痛点
- 配置冗余:NgModule 套娃,组件复用需导入多个模块
- 性能瓶颈:首屏加载慢(RTTI 长),非必要资源打包进首屏
- 开发低效:模板嵌套复杂(*ngIf/*ngFor),响应式编程学习成本高(RxJS)
- 稳定性差:输入空值 Bug 多,缺乏编译时校验
二、核心新特性介绍
2.1 独立组件(Standalone Components)
2.1.1 特性介绍
独立组件通过在 @Component 装饰器中配置 standalone: true,将「依赖管理能力」从 NgModule 下沉到组件本身,可直接通过 imports 数组导入所需的组件、指令、管道,无需封装到 NgModule 中,本质是简化项目层级、提升组件复用性。
2.1.2 基础实践:独立组件的使用
ts
// 1. 独立组件定义(无需关联 NgModule)
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyPipe } from './my.pipe'; // 直接导入管道/指令
@Component({
selector: 'app-demo',
standalone: true,
imports: [CommonModule, MyPipe], // 组件内直接导入依赖
template: `<div>{{ 'test' | myPipe }}</div>`
})
export class DemoComponent {}
// 传统 NgModule 组件(冗余)
@NgModule({
declarations: [DemoComponent, MyPipe], // 声明组件/管道
imports: [CommonModule],
exports: [DemoComponent]
})
export class DemoModule {}
2.1.3 进阶实践:独立组件共享依赖(避免重复导入)
ts
// shared-directives.ts
import { CommonModule } from '@angular/common';
import { CustomInputDirective } from './custom-input.directive';
import { FormatDatePipe } from './format-date.pipe';
// 导出共享依赖,供其他独立组件批量导入
export const SharedDirectives = [
CommonModule,
CustomInputDirective,
FormatDatePipe
];
// user-form.component.ts
import { Component } from '@angular/core';
import { SharedDirectives } from './shared-directives'; // 批量导入共享依赖
@Component({
selector: 'app-user-form',
standalone: true,
imports: SharedDirectives, // 无需逐个导入指令/管道
template: `
<input [appCustomInput]="true" placeholder="用户名">
<p>注册时间:{{ registerTime | formatDate }}</p>
`
})
export class UserFormComponent {
registerTime = new Date();
}
2.1.4 独立组件路由配置
ts
import { Routes } from "@angular/router";
import { HomeComponent } from "./home.component"; // 独立组件
export const routes: Routes = [
{ path: "", component: HomeComponent }, // 直接使用独立组件
// 懒加载独立组件(无需包装 NgModule)
{
path: "user/:id",
loadComponent: () => import("./user-profile.component"),
},
// 独立组件下还存在子路由时,可直接引入路由配置常量
{
path: "user",
loadChildren: () => import("./user.route.ts").then((m) => m.UserRoutes),
},
];
2.1.5 优缺点分析
| 优点 | 缺点 |
|---|---|
| 1. 简化项目结构:减少冗余的 NgModule 文件 | 1. 需手动管理组件间的导入关系,无 NgModule 统一视角 |
| 2. 提升组件复用性:独立组件可直接跨项目导入(无需携带关联模块) | 2. 旧项目兼容成本:需为旧模块组件补充 imports: [CommonModule] 适配新语法 |
| 3. 优化懒加载性能:loadComponent 比 loadChildren 轻量(减少模块包装开销) | 3. 部分第三方库适配滞后:少数旧库仍依赖 NgModule,需通过 imports: [LegacyModule] 兼容 |
2.2 新控制流语法
2.2.1 特性介绍
控制流是一种将流程控制直接写入模板的新声明性语法,从而无须使用 *ngIf、*ngFor 和 *ngSwitch 这种基于指令(Directive)的控制流
2.2.2 场景 1:@if 语法(含 as 字符、@error 捕获)
html
<!-- 1. @if + else if + else(替代嵌套 *ngIf) -->
<div class="order-status">
@if (order.status === 'pending') {
<span class="status-pending">待支付</span>
<button (click)="payOrder()">立即支付</button>
} @else if (order.status === 'paid') {
<span class="status-paid">已支付</span>
<button (click)="viewLogistics()">查看物流</button>
} @else if (order.status === 'shipped') {
<span class="status-shipped">已发货</span>
} @else {
<span class="status-completed">已完成</span>
}
</div>
2.2.3 场景 2:@for 语法(含 track、@empty)
html
<!-- 1. 基础用法:track 配置(替代 trackBy 函数) -->
<ul class="product-list">
@for (product of products; track product.id;let i = $index, let even = $even) {
<li class="product-item">
<img [src]="product.image" alt="{{ product.name }}">
<h4>{{ product.name }}</h4>
<p class="price">¥{{ product.price }}</p>
</li>
} @empty {
<!-- 列表为空时显示,替代 *ngIf="products.length === 0" -->
<li class="empty">暂无商品数据</li>
}
</ul>
支持原*ngFor中的变量
- $index 获取当前项的索引
- $first 当前项是否是第一个
- $last 当前项是否是最后一项
- $even 当前项是否处于偶数索引
- $odd 当前项是否处于奇数索引
- $count 获取集合的长度
2.2.4 场景 3:@error语法
xml
<!-- @error 仅作用于 @if/@for 结合 async 管道的上下文-->
@if (user$ | async; as user; loading isLoading; error errorInfo) {
<div>{{ 数据变量名.属性 }}</div>
} @loading {
<!-- 可选:异步流未完成时的加载状态
<div>加载中...</div>
} @error {
<!-- @error 语义:渲染失败时的兜底逻辑 -->
<div>错误:{{ errorInfo.message }}</div>
}
可监听的错误类型
-
异步流自身抛出的错误
| HTTP 请求错误 | 接口返回 404/403/500 状态码、网络中断、CORS 配置错误 | | ------------------- | --------------------------------------------------------------------------- | | 异步操作超时 |
data$.pipe(timeout(3000))超时触发TimeoutError| | Promise 执行错误 |new Promise((_, reject) => reject(new Error('执行失败')))| | 业务逻辑主动抛错 |data$.pipe(map(res => { if (!res.id) throw new Error('ID缺失'); }))| | 流取消 / 终止错误 | 异步流被手动unsubscribe且抛错、流内部未捕获的执行错误 | -
异步数据解析 / 转换错误
| 子类型 | 触发示例 | | ---------------- | ------------------------------------------------------------------------------------------- | | 数据格式不匹配 | 期望数组却返回对象:
list$ = of({ name: 'test' })+@for渲染 | | JSON 解析错误 |data$ = http.get('/api/data').pipe(map(res => JSON.parse(res)))(非 JSON 字符串) | | 类型转换错误 | 异步返回字符串却做数字运算:{{ data * 2 }}(data$ = of('abc')) | -
模板渲染异步数据的运行时错误
| 子类型 | 触发示例 | -------------------- | --------------------| | 空值 / 未定义访问 |
data$ = of(null)+ 模板中{{ data.name }}| | 数组方法调用错误 |data$ = of(123)+ 模板中{{ data.filter(item => item > 0) }}| | 管道执行错误 |data$ = of('2025-13-01')+ 模板中{{ data \| date }}(非法日期) | | 模板内函数调用错误 |data$ = of('test')+ 模板中{{ formatData(data) }}(formatData 抛错) |
2.2.5 场景 4:@switch 语法(替代 *ngSwitch)
html
<div class="role-container">
@switch (user.role) {
@case ('admin') {
<div class="role-tag admin">
<span>管理员</span>
<button (click)="showAdminMenu()">管理菜单</button>
</div>
}
@case ('editor') {
<div class="role-tag editor">
<span>编辑</span>
<button (click)="showEditorTools()">编辑工具</button>
</div>
}
@case ('viewer') {
<div class="role-tag viewer">
<span>查看者</span>
</div>
}
@default {
<div class="role-tag guest">
<span>访客</span>
<button (click)="goToLogin()">登录</button>
</div>
}
}
</div>
2.2.6 优缺点分析
| 优点 | 缺点 |
|---|---|
| 1. 性能提升:编译后减少 DOM 操作次数,比旧指令快 15%-20% | 1. 迁移成本:旧项目需批量修改模板,复杂嵌套逻辑需手动适配 |
| 2. 语法直观:支持 @else if/@empty,减少嵌套层级(如替代 *ngIf 嵌套) | 2. IDE 支持滞后:部分旧版 IDE 语法高亮不完整 |
| 3. 减少错误:@for 强制要求 track,避免因忘记 trackBy 导致的列表重渲染性能问题 | 3. 兼容性限制:仅 Angular17+ 支持,无法降级到旧版本 |
2.3 信号(Signals)
2.3.1 核心概念与基础 API 总览
Signals 是 Angular17 推出的轻量级响应式状态管理方案,核心解决传统 BehaviorSubject 需手动订阅、变更检测冗余的问题,API 分为三类:
| API 类型 | 具体 API | 作用 | 适用场景 |
|---|---|---|---|
| 基础信号操作 | signal(initialValue) | 创建基础信号(初始值不可变) | 定义组件内 / Service 内状态 |
| set(newValue) | 全量替换信号值(覆盖旧值) | 直接赋值(如表单输入、状态重置) | |
| update(prev => new) | 基于旧值计算新值(函数式更新) | 累加、过滤、修改部分属性 | |
| 计算信号 | computed(() => value) | 依赖其他信号的衍生值(自动响应变化) | 计算总价、筛选列表、格式转换 |
| 副作用监听 | effect(() => void) | 信号变化时执行副作用(如日志、请求) | 状态变化后触发 API、更新 DOM |
2.3.2 基础 API 实战
ts
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div class="counter">
<h3>当前计数:{{ count() }}</h3>
<button (click)="resetCount()">重置为 0</button>
<button (click)="increment()">+1</button>
</div>
`,
})
export class CounterComponent {
// 创建基础信号(初始值为 0)
count = signal(0);
// 计算信号
totalCount = computed(() => this.count ()*2);
// set:全量替换信号值
resetCount() {
this.count.set(0);
}
// update:基于旧值累加 1
increment() {
this.count.update(prev => prev + 1);
}
}
2.3.4 effect 副作用清理(避免内存泄漏)
当effect中包含订阅(如定时器、API 订阅)时,需通过清理函数释放资源,避免组件销毁后仍执行:
ts
import { Component, signal, effect, OnDestroy } from '@angular/core';
import { interval } from 'rxjs';
@Component({
selector: 'app-timer',
standalone: true,
template: `<p>当前计数:{{ count() }}</p>`
})
export class TimerComponent implements OnDestroy {
count = signal(0);
// 存储effect清理函数(用于组件销毁时调用)
private effectCleanup?: () => void;
constructor() {
// effect返回清理函数,用于释放资源
this.effectCleanup = effect(() => {
// 场景:当count>5时,启动定时器(需清理)
if (this.count() > 5) {
const timer = interval(1000).subscribe(() => {
this.count.update(c => c + 1);
});
// 清理函数:组件销毁时取消订阅
return () => timer.unsubscribe();
}
});
}
// 组件销毁时执行清理
ngOnDestroy() {
this.effectCleanup?.();
}
// 外部触发count增加
increment() {
this.count.update(c => c + 1);
}
}
2.3.5 signal 与 Observable 互转(适配异步场景)
通过toSignal()(Observable 转信号)和fromSignal()(信号转 Observable),适配既有 RxJS 代码:
ts
import { Component } from '@angular/core';
import { signal, toSignal } from '@angular/core';
import { fromSignal } from '@angular/core/rxjs-interop';
import { interval, switchMap, debounceTime } from 'rxjs';
@Component({
selector: 'app-signal-observable',
standalone: true,
template: `
<p>当前用户:{{ user()?.name }}</p>
<input [(ngModel)]="searchInput()" placeholder="搜索...">
<p>搜索结果:{{ searchResult()?.length || 0 }} 条</p>
`
})
export class SignalObservableComponent {
// 1. Observable转信号:适配API请求(如HttpClient返回Observable)
userId = signal(1);
// 当userId变化时,自动重新请求用户信息(switchMap切换请求)
user$ = fromSignal(this.userId).pipe(
switchMap(id => fetch(`/api/user/${id}`).then(res => res.json()))
);
// 将Observable转为信号,供模板使用(initialValue避免undefined)
user = toSignal(this.user$, { initialValue: { name: '默认用户' } });
// 2. 信号转Observable:适配RxJS操作符(如debounceTime)
searchInput = signal('');
// 将信号转为Observable,添加防抖
searchResult$ = fromSignal(this.searchInput).pipe(
debounceTime(300), // 输入停止300ms后执行搜索
switchMap(keyword => fetch(`/api/search?kw=${keyword}`).then(res => res.json()))
);
// 转为信号供模板使用
searchResult = toSignal(this.searchResult$, { initialValue: [] });
}
2.3.6 信号(Signals)与 Observable 订阅的场景对比
| 适用场景 | 选择信号(Signals) | 选择 Observable 订阅 |
|---|---|---|
| 组件内简单状态管理 | ✅ 优先选择:计数器、弹窗显隐、表单输入状态 | ❌ 不推荐:语法繁琐,需手动管理订阅 |
| 多依赖衍生值计算 | ✅ 优先选择:购物车总价、筛选列表、格式转换 | ❌ 不推荐:需用 combineLatest 等操作符,语法复杂 |
| 跨组件共享简单状态 | ✅ 优先选择:主题切换、登录状态、全局开关 | ❌ 不推荐:需用 Subject 封装,内存管理复杂 |
| 复杂异步流(多请求合并) | ❌ 不推荐:需通过 toSignal() 适配,无原生操作符 | ✅ 优先选择:switchMap/forkJoin 等操作符原生支持 |
| 高频事件处理(防抖 / 节流) | ❌ 不推荐:需转 Observable 后使用操作符 | ✅ 优先选择:debounceTime/throttleTime 原生支持 |
| 实时数据流(WebSocket) | ❌ 不推荐:需转 Observable 处理持续事件流 | ✅ 优先选择:原生支持 next/complete 事件 |
2.4 延迟加载模板(@defer)- 全场景覆盖
2.4.1 语法原理
@defer 是 Angular17 新增的模板级延迟加载语法,核心是 "按需加载非首屏内容"(如弹窗、折叠面板内组件),避免首屏加载冗余的 JS/CSS 资源。支持四大核心能力:
- 触发条件:on(用户交互)、when(条件满足);
- 状态提示:
@placeholder(加载中)、@loading(加载中)、@error(加载失败)、@empty(内容为空); - 预加载:prefetch(提前加载即将用到的资源);
- 性能优化:加载完成后自动替换占位内容,无闪烁。
2.4.2 基础用法(默认触发:组件初始化后延迟加载)
适用于 "首屏非关键内容"(如页面底部的推荐列表):
html
<!-- 首屏优先加载核心内容(用户信息) -->
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<!-- 延迟加载非关键内容(推荐列表) -->
@defer {
<app-recommended-list [userId]="user.id"></app-recommended-list>
} @placeholder (minimum 500ms){
<!-- 加载前状态:占位内容 -->
<div class="comment-skeleton">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-avatar"></div>
</div>
} @loading {
<!-- 加载中状态:替代ngIf+loading变量 -->
<div class="skeleton">推荐内容加载中...</div>
}
2.4.3 on 触发(用户交互时加载)
适用于 "用户主动触发才显示的内容"(如点击按钮显示的详情面板):
html
<!-- 按钮触发:点击后加载详情组件 -->
<button #detailBtn>查看订单详情</button>
@defer (on click(detailBtn)) { <!-- 点击按钮时触发加载 -->
<app-order-detail [orderId]="currentOrderId"></app-order-detail>
} @loading {
<div class="loading-spinner">加载详情中...</div>
} @error {
<div class="error">详情加载失败,请重试</div>
}
<!-- 其他触发事件:hover/focus -->
@defer (on hover(detailCard)) { <!-- 鼠标悬浮时加载 -->
<div class="card-tooltip">订单创建时间:{{ order.createTime }}</div>
}
<!-- 其他触发事件:scroll -->
@defer (on scroll(detailCard, 200px)) {
<div class="loading-more">加载更多商品中...</div>
<ng-container *ngIf="moreProducts$ | async as more">
@for (item of more; track item.id) {
<app-product-card [product]="item"></app-product-card>
}
</ng-container>
}
2.4.4 when 触发(条件满足时加载)
适用于 "数据就绪 / 状态变化时加载"(如筛选条件选择后加载表格):
html
<!-- 条件触发:筛选条件选中后加载表格 -->
<select [(ngModel)]="selectedType" (change)="onTypeChange()">
<option value="">请选择订单类型</option>
<option value="all">全部订单</option>
<option value="paid">已支付</option>
</select>
@defer (when selectedType !== '') { <!-- 当selectedType非空时加载 -->
<app-order-table [type]="selectedType"></app-order-table>
} @loading {
<div class="table-skeleton">表格加载中...</div>
} @empty {
<div>暂无{{ selectedType === 'paid' ? '已支付' : '全部' }}订单</div>
}
2.4.5 prefetch 预加载(提前加载即将用到的内容)
适用于 "即将触发加载" 的场景(如滚动到接近位置时预加载),减少用户等待时间:
html
<!-- 预加载:当用户滚动到距离组件500px时,提前加载 -->
@defer (on scroll(container, 500px); prefetch on scroll(container, 1000px)) {
<!-- 滚动到距离组件1000px时预加载资源,滚动到500px时显示 -->
<app-long-list [page]="currentPage"></app-long-list>
}
<!-- 容器滚动监听:指定scroll的参考容器 -->
<div #container class="scroll-container" style="height: 500px; overflow-y: auto;">
<!-- 其他内容 -->
</div>
2.4.6 @error语法
专为「defer 懒加载全流程」设计的错误捕获块,仅作用于 defer 块内部,捕获从「懒加载资源下载」到「组件初始化 / 渲染」的全链路错误,属于「资源加载层错误兜底」。
html
@defer
<!-- 懒加载目标组件 -->
<app-lazy></app-lazy>
} @placeholder {
<!-- 触发前占位:点击按钮加载 -->
<button>点击加载懒组件</button>
} @loading {
<!-- 加载中状态 -->
<div>加载中...</div>
} @error {
<!-- 懒加载错误捕获:$error 是内置错误对象 -->
<div>❌ 懒加载失败:{{ $error.message }}</div>
}
可监听的错误类型
- 懒加载资源下载错误
| 类型 | 触发示例 |
|---|---|
| JS/CSS chunk 404 | 打包后的懒加载 chunk 路径错误、CDN 缓存失效、文件名哈希变更 |
| 网络层错误 | 下载 chunk 时断网、网络超时、DNS 解析失败 |
| 跨域 / 安全限制 | chunk 资源违反 CORS/CSP 策略,浏览器拦截下载 |
| 混合内容错误 | HTTPS 页面加载 HTTP 协议的懒加载 JS chunk(浏览器阻止) |
| 资源大小超限 | chunk 体积超过服务器 / 浏览器限制(如 nginx client_max_body_size 限制) |
- 懒加载组件编译 / 解析错误
| 类型 | 触发示例 |
|---|---|
| 组件代码语法错误 | 懒加载组件的 TS 代码有语法错误(如少分号、变量未定义) |
| 模板语法错误 | 懒加载组件的模板有语法错误(如 {{ data..name }} 双点、指令拼写错误) |
| 组件元数据错误 | 懒加载组件的 @Component 元数据错误(如 selector 重复、imports 漏写) |
| 二进制 chunk 损坏 | 下载的 JS chunk 二进制数据不完整(如网络中断导致下载一半) |
- 懒加载组件初始化错误
| 类型 | 触发示例 |
|---|---|
| 生命周期抛错 | 懒加载组件 ngOnInit 中调用接口抛错、ngAfterViewInit 操作 DOM 抛错 |
| 组件状态初始化错误 | 懒加载组件的 Signal / 变量初始化抛错(如 count = signal(1/0)) |
| 模板渲染初始化错误 | 组件首次渲染时,同步模板表达式错误(如 {{ undefined.name }}) |
- 懒加载组件依赖注入错误
| 类型 | 触发示例 |
|---|---|
| 服务未提供 | 懒加载组件依赖 UserService,但未在 providers/ 根注入器中配置 |
| 依赖循环引用 | 懒加载组件依赖的服务与其他服务形成循环引用,导致注入失败 |
| 管道 / 指令未导入 | 懒加载组件模板使用 myPipe,但组件 imports 未导入该管道 |
2.4.7 优缺点分析
| 优点 | 缺点 |
|---|---|
| 1. 首屏加载提速:减少首屏 JS/CSS 体积(非关键组件延迟加载) | 1. 触发时机需谨慎:滥用可能导致用户交互时卡顿(如点击后才加载大组件) |
2. 简化状态管理:内置 @loading/@error,无需手动维护 loading/error 变量 |
2. 调试成本:需通过 Angular DevTools 查看延迟加载状态,无法直接断点调试 |
| 3. 预加载优化:prefetch 可平衡加载时机与用户体验,减少等待时间 | 3. 兼容性限制:仅 Angular17+ 支持,无法降级到旧版本 |
| 4. 语法直观:无需手动写 ngIf 控制显隐,模板逻辑更简洁 | 4. 复杂场景适配难:动态组件加载(如 ComponentFactoryResolver)需额外处理 |
2.5 指令组合 API
2.5.1 核心定义
组件通过hostDirectives配置,直接 "继承" 其他指令的逻辑(属性、方法、生命周期)
2.5.2 解决痛点
旧版用 mixin(混入)复用逻辑,代码冗余且类型不安全;模板中重复绑定指令
2.5.3 实战代码
ts
// 1. 定义独立指令(封装通用逻辑)
@Directive({ selector: '[appAuth]', standalone: true })
export class AuthDirective {
@Input() appAuth!: string; // 接收权限标识
ngOnInit() {
console.log('校验权限:', this.appAuth); // 通用权限校验逻辑
}
}
// 2. 组件集成指令(无需模板绑定)
@Component({
selector: 'app-admin-panel',
standalone: true,
hostDirectives: [
{ directive: AuthDirective, inputs: ['appAuth'] } // 集成指令,映射输入
],
template: `<div>管理员面板</div>`
})
export class AdminPanelComponent { }
// 3. 使用组件(直接传递指令输入)
<app-admin-panel appAuth="admin"></app-admin-panel>
2.6 NgOptimizedImage
2.6.1 核心定义
Angular 15 + 稳定的图片优化指令,替代原生img,一站式解决图片加载性能问题
2.6.2 解决痛点
原生图片易导致布局偏移(CLS)、加载慢、格式不优化、缺乏优先级控制
2.6.3 核心能力
强制宽高比(防 CLS)、自动懒加载、自动格式转换(WebP/AVIF)、优先级控制
2.6.4 实战代码
html
<!-- 1. 首屏核心图片(优先加载,禁用懒加载) -->
<img
ngSrc="home-banner.jpg"
width="1200"
height="400"
priority <!-- 核心:首屏优先加载 -->
alt="首页Banner"
>
<!-- 2. 非首屏图片(自动懒加载,优化格式) -->
<img
ngSrc="user-avatar.jpg"
width="80"
height="80"
loading="lazy" <!-- 默认懒加载,可省略 -->
alt="用户头像"
>
<!-- 3. 响应式图片(适配不同设备) -->
<img
ngSrc="product-{{size}}.jpg"
[width]="size === 'sm' ? 300 : 600"
[height]="size === 'sm' ? 200 : 400"
[size]="'(max-width: 640px) 300px, 600px'"
alt="商品图片"
>
2.7 canMatch 路由守卫
2.7.1 核心定义
Angular 15 + 稳定的路由守卫,在 "路由匹配阶段" 筛选路由,决定是否将路由纳入候选
2.7.2 解决痛点
旧版canActivate在路由匹配后执行,失败则拒绝访问,无法实现 "同路径多组件" 动态匹配(如多租户)
2.7.3 核心差异(vs canActivate)
- canActivate:匹配后准入控制 → 失败 = 拒绝访问
- canMatch:匹配中筛选 → 失败 = 跳过当前路由,继续匹配下一个
2.7.4 执行顺序
- matcher:匹配 URL 规则;
- canMatch:校验是否允许匹配该路由;
- canLoad:校验是否允许加载懒加载模块;
- 加载模块(若 canLoad 通过);
- canActivate:校验是否允许激活路由;
- 激活路由,渲染组件。
2.7.5 实战代码(多租户场景)
ts
// 1. 定义canMatch守卫
export const tenantMatchGuard: CanMatchFn = (route) => {
const tenantService = inject(TenantService);
return tenantService.getCurrentTenant() === route.data.tenantId;
};
// 2. 路由配置(同路径匹配不同租户组件)
const routes: Routes = [
{
path: 'dashboard',
canMatch: [tenantMatchGuard],
data: { tenantId: 'tenant1' },
component: Tenant1DashboardComponent
},
{
path: 'dashboard',
canMatch: [tenantMatchGuard],
data: { tenantId: 'tenant2' },
component: Tenant2DashboardComponent
},
{ path: 'dashboard', component: FallbackDashboardComponent } // 兜底
];
2.8 依赖注入(DI)增强:更灵活的注入方式
2.8.1 核心定义
Angular 16 + 优化的 DI 系统,支持inject函数在构造函数外使用,增强环境区分能力
2.8.2 解决痛点
旧版inject仅能在构造函数 / 工厂函数中使用,静态方法、第三方库回调中无法注入
2.8.3 实战代码(常用场景)
场景 1:类内任意位置注入
ts
@Injectable({ providedIn: 'root' })
export class ConfigService {
private http = inject(HttpClient); // 类内直接注入,无需构造函数
loadConfig() {
return this.http.get('/api/config');
}
}
场景 2:静态方法中注入
ts
@Injectable({ providedIn: 'root' })
export class TenantService {
static getCurrentTenant() {
const configService = inject(ConfigService); // 静态方法注入
return configService.loadConfig().then(res => res.tenantId);
}
}
2.9 Required Inputs:编译时校验的必填输入
2.9.1 核心定义
Angular 16 + 稳定的输入属性校验特性,通过@Input({ required: true })标记必填,编译时 + 运行时双校验
2.9.2 解决痛点
旧版需手动在ngOnInit中判空,易遗漏导致空值 Bug;错误反馈滞后(运行时才报错)
2.9.3 实战代码
ts
// 1. 标记必填输入
@Component({
selector: 'app-user-card',
standalone: true,
template: `<div>{{user.name}}</div>`
})
export class UserCardComponent {
@Input({ required: true }) user!: { name: string; id: string }; // 核心:required: true
}
// 2. 使用组件(未传user时编译报错)
<app-user-card [user]="userData"></app-user-card> <!-- 正确 -->
<app-user-card></app-user-card> <!-- 错误:编译时报错 NG8007 -->
三、优秀实践------懒加载
学完上述 9 个实用特性,我们不妨把它们串联起来落地实践。这一节,我们聚焦性能优化的高频需求 ------ 懒加载,详细讲讲如何通过 独立组件与 defer 延迟加载语法的结合,实现高效的按需加载方案。
3.1 必要条件
相信不少开发者刚接触 @defer 延迟加载语法时,都会遇到一个共性问题 ------ 明明写了语法,懒加载却始终不生效。其实这背后,是因为我们忽略了几个关键的必要条件。
假设组件 A 是与 @defer 指令处于同一层级 的组件,而组件 B 是被 @defer 指令包裹、需要实现懒加载的目标组件。
必要条件:
- 组件A必须是独立组件:
standalone: true; - 组件B必须是独立组件:
standalone: true; - 组件A需显式导入组件B:
imports:[RepoEchartComponent];
3.2 实战代码
3.2.1 场景一:评论区懒加载(用户滚动到区域才加载)
html
<section>
<h2>商品详情</h2>
<!-- 其他内容 -->
@defer (on viewport; prefetch on idle) {
<app-comments></app-comments>
} @loading {
<div>评论加载中...</div>
}
</section>
3.2.1 场景二:结合 @switch 实现 Tab 懒加载
html
@switch (activeTab) {
@case ('profile') {
@defer (on viewport) {
<app-profile></app-profile>
}
}
@case ('settings') {
@defer (on interaction) {
<app-settings></app-settings>
}
}
}
3.3 注意事项
-
@defer不能嵌套(截至 Angular 18) -
不要过度使用:每个 defer 块会生成独立 chunk,过多可能导致 HTTP 请求爆炸
-
prefetch为实验性功能,需启用deferBlockPrefetching -
已在其他场景引用或注册的组件无法被懒加载
-
继承的父组件类无法被懒加载,需改造成非组件类 错误示例:
ts@Component({ templateUrl: './basic.component.html', styleUrls: ['./basic.component.scss'], standalone: true, imports: BasicModules, }) //这是一个组件基类 export class BasicComponent{} //这是期望懒加载的组件Demo export class DemoComponent extends BasicComponent{}修改方案
ts//将组件基类改造为非组件类 @Injectable({ providedIn: 'any' }) export class BasicComponent{}
以上就是懒加载的实现方案和踩坑经验啦,希望能帮到正在折腾 @defer 的小伙伴~
四、总结
Angular的新特性围绕 "性能优化 ""开发效率 ""场景适配" 三大核心:延迟加载模版(@defer) 降低首屏加载成本,独立组件与新控制流语法简化代码结构,信号(Signals)完善响应式能力。通过熟练掌握这些新语法,可显著提升项目性能与可维护性。
若想了解Angular新特性底层原理,请参考掘金文章 juejin.cn/post/733968...
五、 加入我们
MateChat 正在快速发展,我们欢迎更多开发者加入:
- 💬 DevUI微信小助手:devui-official(添加时请备注MateChat)
- 🌟 代码仓库地址:gitcode.com/DevCloudFE/...
广纳贤士:AI赋能各行各业,MateChat期待更多感兴趣的小伙伴加入我们~