先记住这个答案
模板驱动表单采用指令(如 NgForm)自动创建表单模型,通过双向绑定 (ngModel) 将模板与模型耦合,验证规则写在模板中,控制流隐式,测试依赖 DOM 和异步变化。模型驱动表单则用 FormGroup/FormControl 在组件类中显式定义模型,通过显式绑定 [formGroup] 和 formControlName 连接,验证规则作为函数或对象定义在代码中,控制流清晰,易于同步测试。核心差异:模型驱动将状态和逻辑集中管理,可预测性和可测试性更强,适合复杂表单;模板驱动简洁但控制分散。
- 模板驱动双向绑定,模型驱动显式响应式
- 验证规则位置不同:模板内 vs 代码内
- 模型驱动更易同步单元测试
表单模型创建与管理机制
模板驱动表单依赖 NgForm、NgModel 等指令。当在模板中写入 <form> 和 [(ngModel)] 时,Angular 自动创建表单控件并注册到 FormGroup 内部。开发者并不直接持有这些控件对象,但可通过模板引用变量(如 #myForm="ngForm")在模板中访问。表单状态(如 valid、dirty)由指令实时更新,绑定是双向的:输入框值变化自动更新组件属性,反之组件属性变化也更新输入框。验证规则通过模板属性(如 required、minlength)声明,Angular 内置验证器直接读取 HTML 标记。
模型驱动表单需要在组件类中手动创建 FormGroup 和 FormControl 实例,例如在构造函数中 this.form = new FormGroup({ name: new FormControl('') })。模板通过 [formGroup]="form" 和 formControlName="name" 进行绑定,数据流不是双向绑定语法,而是显式绑定:用户输入会自动同步到控件值,但若要监听值变化可订阅 valueChanges 或使用异步管道,从组件代码更新模型需调用 setValue 或 patchValue。验证逻辑由 Validators.required 等或自定义验证器函数定义,集中管理在 TS 代码中。这种机制使得表单模型是普通类对象,不依赖 DOM 生成,因此可在没有模板的情况下单独实例化。
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-reactive-form',
template: `
<form [formGroup]="form">
<input formControlName="title">
<span *ngIf="form.get('title')?.invalid">Title required</span>
</form>
`
})
export class ReactiveFormComponent implements OnInit {
form!: FormGroup;
ngOnInit() {
this.form = new FormGroup({
title: new FormControl('', [Validators.required, Validators.minLength(3)])
});
}
}FormGroup 在组件类中定义,验证规则作为数组传入 FormControl。模板通过 formGroup 和 formControlName 连接。此代码需在 Angular 项目中运行,需导入 ReactiveFormsModule,模板驱动不支持这种写法。
工程场景:动态添加验证的复杂表单
场景:构建一个用户注册表单,包含用户名、密码和确认密码,且确认密码必须与密码一致,密码强度根据输入动态变化。模板驱动中,交叉验证需自定义指令并注入 NgModel,动态控制错误信息需查询 NgModel 状态,代码分散且难维护。改用模型驱动:在组件类中创建 FormGroup,用户名用 Validators.required,密码用包含自定义正则的 Validators.pattern,确认密码用自定义同步验证器(比较两字段)。模板只需绑定 formControlName,错误信息用 *ngIf 读取 form.controls。决策:采用模型驱动。结果:验证逻辑集中于 TS,单元测试可同步调用验证器,不需渲染 DOM。
为什么这样处理:模型驱动允许将表单模型作为普通对象测试,动态验证可通过 addValidators/clearValidators 编程调整。而模板驱动依赖模板指令,这类逻辑必须通过组件属性间接操作,测试需触发 DOM 事件并等待变更检测循环。另外,模型驱动的同步验证器会在输入变化时由 Angular 自动运行,而模板驱动则需要额外指令。因此复杂控制流优先模型驱动。
export function passwordMatchValidator(form: FormGroup) {
const pwd = form.get('password')?.value;
const confirm = form.get('confirm')?.value;
return pwd === confirm ? null : { mismatch: true };
}
export function validateMinLength(min: number) {
return (control: FormControl) =>
control.value && control.value.length >= min ? null : { minLength: true };
}自定义验证器是纯函数,可独立于 Angular 测试,只需构造 FormControl/FormGroup 实例调用。
适用边界与失效条件
模板驱动适合简单表单(如登录框),因为代码量少、模板直观。但若表单需要动态创建控件(如数组字段),复杂逻辑会使模板驱动指令过多,且深层控件难以访问。模型驱动则天然支持 FormArray,但在大型表单中创建大量 FormControl 代码冗余,需借助 FormBuilder 简化。当仅需要非常简单的双向绑定且无复杂验证时,模型驱动增加样板代码。
易失效条件:模板驱动中动态修改验证规则需要直接操作 NgModel 的 control.validator,但这引用 NgModel 内部,不推荐在模板中写。模型驱动中若忘记在模板绑定 formControlName 或使用双向绑定,可能导致值不更新。另外,模板驱动依赖 NgForm 指令必须引入 FormsModule,模型驱动需 ReactiveFormsModule。若混淆使用,会报错。可测试性:模板驱动需要 ComponentFixture 和 fakeAsync,模型驱动可直接 new FormGroup 测试。
容易答错的地方
- 认为模板驱动是双向数据绑定,模型驱动也是双向
- 实际上模型驱动不使用双向绑定语法,而是通过 formGroup 和 formControlName 将模板连接到显式创建的模型。用户输入会由指令自动同步到控件值,但从组件代码更新模型则需显式调用 setValue 或 patchValue,也可通过订阅 valueChanges 监听变化。而模板驱动使用 ngModel 进行双向绑定,底层也是单向同步并反馈。
- 认为模型驱动无法在模板中使用管道
- 仍可通过 valueChanges 异步管道或 get 获取值,但不如 ngModel 直观。常见错误是试图在模板中直接用
[(ngModel)]绑定到 formControl,但这样会脱离响应式模型。
面试官还会怎么问?
在模型驱动表单中,如何使用 FormArray 动态添加一组输入框?
使用 FormArray 作为控件组,添加时调用数组 push(new FormControl()),模板中通过 *ngFor 遍历 controls,并用 formControlName 索引绑定。必须构建对应的 FormGroup 结构。
为什么模型驱动表单更容易编写单元测试?
因为 FormGroup 和验证器是纯 TypeScript 对象,可直接实例化并验证状态,无需渲染组件和触发 DOM 事件。测试速度更快、稳定。
哪种表单策略适合大型企业级应用?
通常推荐模型驱动,因为其复杂验证、动态控制和可测试性优势明显。但团队若对模板驱动更熟且场景简单,也可用,但长远维护成本较高。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。