系列写到这里,主干知识已经铺完。最后一篇实战向的文章不设单一主题,而是把散落在日常开发里的”边角利器”与经验汇总成册:内容投影的进阶技巧、宿主元素样式、Angular Elements、DevTools 与 AI 工具链、性能清单、报错速查。当作工具书用,遇到问题回来翻。
ng-content 的 select 技巧
内容投影不只是”把内容搬进来”,select 属性支持按元素、属性、 class 精准分拣:
<!-- 卡片组件 -->
<div class="card">
<header><ng-content select="[card-header]" /></header>
<main><ng-content /></main>
<footer><ng-content select="[card-footer], .footer" /></footer>
</div>
<app-card>
<span card-header>标题</span>
<p>正文默认进入无 select 的插槽</p>
<div class="footer">底部</div>
</app-card>
常用套路:
select="[slot]"属性选择器最稳,不受用户改 class 影响- 投影组件的样式隔离:投影内容声明在父组件作用域,子组件样式作用不到它们——需要跨边界时用
::ng-deep(已废弃但仍在)或全局样式 - 默认内容:
@if包住<ng-content>无法判断”是否投影了内容”,改用两段式——有内容投影时ng-content优先,空态用兄弟节点兜底,或通过@ContentChild检测 - 多插槽必须全部有 select:没有 select 的插槽只能有一个,它是”其余内容”的收容所
对比 ng-template + NgTemplateOutlet 的”显式插槽”方案(第 10 篇):ng-content 是声明式、宿主写什么就渲染什么;ng-template 是宿主把”怎么渲染”的模板交给组件,组件决定渲染时机与次数。表单控件 label/错误提示这类强结构场景用后者。
host 类与 :host 上下文
给组件宿主元素本身加样式与状态类:
@Component({
selector: 'app-tile',
host: {
class: 'tile',
'[class.expanded]': 'expanded()',
'[attr.aria-expanded]': 'expanded()',
},
})
export class TileComponent {
readonly expanded = signal(false);
}
样式侧的配套选择器:
:host {
display: block; // 宿主默认 inline,自定义元素几乎总是要 block/flex
}
:host(.expanded) {
border-color: var(--accent);
}
// 祖先链上的上下文:父组件带 .dark 时才生效
:host-context(.dark) {
background: #1e1e1e;
}
host 对象替代了老的 @HostBinding/@HostListener 装饰器(v22 中已不建议),事件绑定同样支持:'(click)': 'toggle()'。优先用 host 而不是在模板里包一层 div——少一层 DOM,Shadow DOM 仿真下也没有样式穿透问题。
@angular/elements:组件打包成 Web Component
把 Angular 组件注册成自定义元素,供非 Angular 页面(CMS、微前端壳、老技术栈)使用:
import { createCustomElement } from '@angular/elements';
import { NgElement, WithProperties } from '@angular/elements';
@NgModule({ imports: [RatingComponent] })
export class RatingElementModule implements DoBootstrap {
private readonly injector = inject(Injector);
ngDoBootstrap(): void {
// inputs 自动映射为 attributes/properties
const element = createCustomElement(RatingComponent, { injector: this.injector });
customElements.define('app-rating', element);
}
}
// 宿主页面(可以是纯 HTML)
const rating = document.querySelector('app-rating') as NgElement & WithProperties<RatingComponent>;
rating.value = 4; // 组件的输入
| 适合 | 不适合 |
|---|---|
| 向外输出嵌入式小组件(评分、地图、播放器) | 整页应用——直接用 Angular |
| 与老技术栈共存的部分增量 UI | 高频跨边界通信的复杂交互 |
成本注意:每个元素化组件都要带一份注入器上下文;信号输入映射为 property,attribute 只支持字符串。细节见第 16 篇的对比分析。
DevTools:信号图与 DI 图
Angular DevTools 的浏览器扩展在 v22 迎来两大杀手锏:
- 信号图:可视化 signal/computed/effect 的生产者-消费者关系,定位”谁更新了它""为什么没更新”——信号应用的调试从猜谜变成看图
- DI 图:可视化依赖注入关系与 provider 覆盖链,排查”为什么注入了两个实例”一目了然
配合 chrome-devtools-mcp,可以让 AI 代理直接读取 DevTools 状态(组件树、信号图)做诊断。本地排障三部曲:Profiler 录制找慢组件 → 信号图找多余依赖 → DI 图找实例异常。
AI 工具链一览
v22 生态的 AI 辅助已工具化:
- 官方 skills:
npx skills add https://github.com/angular/skills把 Angular 官方最佳实践安装进代理的技能库,生成代码遵循当前版本惯例 - WebMCP(实验):通过
declareExperimentalWebMcpTool把网页上下文暴露给 MCP 客户端,AI 能”看见”运行中的应用状态,做 E2E 级诊断 - chrome-devtools-mcp:AI 读取性能轨迹与 DOM,配合 Angular DevTools 数据定位渲染问题
用法心态:AI 工具链加速的是”查证与定位”,架构决策与版本事实仍以 angular.dev 文档为准。
性能优化清单
按投入产出排序的检查项:
- 长列表用虚拟滚动(第 43 篇),
@for全员带 track - 大计算用
computed缓存,重活进 Web Worker - 图片懒加载:
loading="lazy"+@defer包裹首屏外组件(第 8 篇) - 包体:按路由懒加载 standalone 组件,
@defer拆首屏;检查ng build的 chunk 分析 - 请求:
httpResource声明式缓存与去重(第 23 篇),防抖搜索用debounced信号 - 变更检测:确认无
Eager策略组件;事件处理器里只写信号,不手动改 DOM - 内存:退订 RxJS(
takeUntilDestroyed)、清理 ResizeObserver/Interval(第 27 篇) - SSR:增量 hydration 默认开启,验证首屏指标(LCP/INP)与水合错误
- 字体与图标:图标按需(第 39 篇),避免整包字体下载
常见报错速查
| 报错 | 原因 | 解法 |
|---|---|---|
NG0203: must be used in an injection context | 在普通函数/方法里调 inject() | 移到构造函数/字段初始化器,或改 @Inject 参数注入 |
NG0602: ... enter the view tree 之类 Portal 错误 | TemplatePortal 缺 ViewContainerRef | attach 前补上第二个参数(第 41 篇) |
NG05105(模板表达式类) | 信号没调用 | 模板写 count() 而非 count |
ExpressionChangedAfterItHasBeenChecked | 生命周期里同步改了已检查的绑定 | 改信号驱动或移到 effect |
NG0200: Circular dependency in DI | 服务互相注入 | 抽第三个服务或 inject(Injector) 延迟取 |
Cannot bind to 'x' since it isn't a known property | standalone 组件没 import 指令所在模块 | imports 数组补齐 |
NG0303: No provider for Xxx | 未注册 provider | @Service() 确认 providedIn 或组件级 providers |
Maximum call stack size exceeded 伴随循环更新 | effect 里写信号形成环 | 拆分状态或用事件驱动(第 21 篇) |
报错编号(NGxxxx)在 angular.dev 的 errors 手册都有专页,搜索编号永远比搜报错全文可靠。