组件输入、输出与内容投影
组件边界由 inputs、outputs 和 content projection 组成。输入是上游数据,输出是用户动作;子组件不应直接修改父对象或调用父服务的私有方法。
ts
readonly value = input.required<string>();
readonly changed = output<string>();
commit(next: string) { this.changed.emit(next); }实验:父组件传入值,子组件提交变更;验证单向流和销毁后不再发事件。
练习:为可复用确认按钮定义输入、输出、loading 和 disabled 契约。
核心机制与实现
input.required<T>() 是只读输入信号,子组件不能把它当作父组件的可变引用;output<T>() 只发送事件值,父组件决定是否写回。内容投影则由 <ng-content> 接收,投影节点仍由父组件创建,不能在子组件中当作自己的模板销毁。
ts
@Component({
selector: 'confirm-button',
standalone: true,
template: `<button [disabled]="disabled()" (click)="changed.emit(true)"><ng-content /></button>`,
})
export class ConfirmButton {
readonly disabled = input(false)
readonly changed = output<boolean>()
}实验时让父组件切换 disabled 并监听 changed:禁用时不能发事件,启用后一次点击只发一次 true。销毁父组件后再触发旧 DOM 引用不应产生新的输出;这能区分输入/输出契约与直接修改父对象。
本节专属实验:把输入追到视图
在 Angular 组件中只保留本节主题的一条数据流:输入值、状态变更、模板读取和清理日志分别打印。先运行成功路径,再删除本节涉及的绑定或 provider,记录是模板编译、依赖注入还是运行时错误。
ts
const before = signal('idle')
const after = computed(() => before() === 'idle' ? 'ready' : 'busy')
effect(() => console.log('state', before(), after()))预期:before 改变时只重新计算依赖它的 computed;组件销毁后 effect 不再产生输出。
可运行实验
- 在组件、注入服务和路由切换之间记录实例创建与销毁。
- 对模板读取、signal 更新、Observable 订阅和 HTTP 请求分别加日志。
- 用 Angular 测试工具验证可访问性、错误状态、取消请求和 SSR/hydration 边界。
实验记录至少包含:环境版本、输入序列、预期结果、实际结果、失败截图或日志,以及修复后再次运行的结果。
练习
- 用自己的话画出本篇概念的依赖图,并标出一个可能产生重复执行或竞态的节点。
- 写一个最小测试覆盖成功、失败、取消/卸载三条路径;测试应断言可观察结果,而不是内部实现调用次数。
- 给出一个选择当前方案而不是替代方案的理由,并写下替代方案何时更合适。
下一步
完成本节练习后阅读本章答案,再继续本章下一节。