翻译文章--Angular 14 新特性介绍
Hi 大家好! 我是 Sergei Sarkisian,在InterSystems 做Angular 前端7年。Angular是非常流行的框架,我们的开发人员、客户和合作伙伴经常选择它来开发他们的应用程序。
我会写一系列的文章,涵盖Angular的不同方面:概念、方法、最佳实践、高级主题等等。这个系列的文章将针对那些已经熟悉Angular的人,不会涉及基本概念。由于我正在构建文章的路线图,我想从突出最近的Angular版本中的一些重要功能开始。
严格类型化表单
这可能是近几年来Angular最受欢迎的功能。有了Angular 14,开发者现在可以在Angular Reactive Forms中使用TypeScript的所有严格类型检查功能。
表单控制Formcontrol 类现在是通用的,并接受它所持有的值的类型。
/* Before Angular 14 */
const untypedControl = new FormControl(true);
untypedControl.setValue(100); // value is set, no errors
// Now
const strictlyTypedControl = new FormControl<boolean>(true);
strictlyTypedControl.setValue(100); // you will receive the type checking error message here
// Also in Angular 14
const strictlyTypedControl = new FormControl(true);
strictlyTypedControl.setValue(100); // you will receive the type checking error message here
正如你所见,第一个和最后一个例子几乎是一样的,但有不同的结果。这是因为在Angular 14中,新的FormControl类从开发者提供的初始值中推断出类型。因此,如果提供了true
的值,Angular就为这个FormControl设置boolean | null
的类型。.reset()
方法需要可置空的值,如果没有提供值,就会置空这些值。
一个旧的、没有定义类型的FormControl类被转换为UntypedFormControl
(对UntypedFormGroup
、UntypedFormArray
和UntypedFormBuilder
来说也是如此),它实际上是FormControl<any>
的别名。如果你从以前的Angular版本升级,你所有提到的FormControl
类将被Angular CLI替换为UntypedFormControl
类。
Untyped* 类通常用以实现特定目标:
- 保持应用程序的工作方式与从以前的版本过渡之前完全一样(记住,新的FormControl将从初始值推断出类型)
- 确保所有的
FormControl<any>
的使用都是有意的。所以你需要自己将任何UntypedFormControl改为FormControl<any>
。 - 为了给开发者提供更多的灵活性(我们将在下面介绍这个问题)
记住,如果你的初始值是 "null",那么你将需要明确指定FormControl类型。另外,在TypeScript中有一个错误,如果你的初始值是 "false",也需要这样做。
对于表单组,你也可以定义接口,并把这个接口作为表单组的类型传递。在这种情况下,TypeScript将推断出FormGroup中的所有类型。
interface LoginForm {
email: FormControl<string>;
password?: FormControl<string>;
}
const login = new FormGroup<LoginForm>({
email: new FormControl('', {nonNullable: true}),
password: new FormControl('', {nonNullable: true}),
});
FormBuilder的方法.group()
现在有了通用属性,可以接受你预定义的接口,就像上面的例子中我们手动创建了FormGroup。
interface LoginForm {
email: FormControl<string>;
password?: FormControl<string>;
}
const fb = new FormBuilder();
const login = fb.group<LoginForm>({
email: '',
password: '',
});
由于我们的接口只有原始的nonNullable类型,它可以用新的 "nonNullable "表单生成器属性来简化(它包含 "NonNullable FormBuilder表单生成器 "类实例,也可以直接创建):
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
❗ 请注意,如果你使用nonNullable的FormBuilder或者你在FormControl中设置了nonNullable的选项,那么当你调用.reset()
方法时,它将使用初始FormControl值作为重置值。
另外,非常重要的一点是,this.form.value
中的所有属性都将被标记为可选属性。像这样:
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
// login.value
// {
// email?: string;
// password?: string;
// }
发生这种情况是因为当你禁用表单组FormGroup内的任何表单控件FromControl时,这个表单控件的值将从form.value
中删除。
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
login.get('email').disable();
console.log(login.value);
// {
// password: ''
// }
要获得整个表单对象,你应该使用.getRawValue()
方法::
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
login.get('email').disable();
console.log(login.getRawValue());
// {
// email: '',
// password: ''
// }
严格类型化表单的优势:
- 任何返回FormControl / FormGroup值的属性和方法现在都是严格类型的。例如:
value
,getRawValue()
,valueChanges
. - 任何改变表单控件值的方法现在都是类型安全的:
setValue()
,patchValue()
,updateValue()
- 表单控件现在是严格类型化的。它也适用于表单组的
.get()
方法。这也将防止你在编译时发生访问不存在的情况.
新的 FormRecord 类
新的 "表单组 "类的缺点是它失去了它的动态性质。一旦定义了,你将不能在运行中添加或删除表单控件。
为了解决这个问题,Angular提出了新的类--FormRecord'。
FormRecord实际上与
FormGroup`相同,但它是动态的,所有的表单控件都应该有相同的类型。.
folders: new FormRecord({
home: new FormControl(true, { nonNullable: true }),
music: new FormControl(false, { nonNullable: true })
});
// Add new FormContol to the group
this.foldersForm.get('folders').addControl('videos', new FormControl(false, { nonNullable: true }));
// This will throw compilation error as control has different type
this.foldersForm.get('folders').addControl('books', new FormControl('Some string', { nonNullable: true }));
正如你所看到的,这里有另一个限制 - 所有的FormControls必须是相同的类型。如果你真的需要动态和异质的FormGroup,你应该使用UntypedFormGroup
类来定义你的表单
无模块的 (独立standalone) 组件
这个特性仍然被标记为实验性的,但它是一个有趣的功能。它允许你定义组件、指令和管道,而不把它们包含在任何模块中。
这个概念还没有完全准备好,但我们已经能够在没有ngModules的情况下建立一个应用程序。
要定义一个独立的组件,你需要使用Component组件/Pipe管道/Directive Decorator指令装饰器中新的`standalone'属性:
@Component({
selector: 'app-table',
standalone: true,
templateUrl: './table.component.html'
})
export class TableComponent {
}
在这种情况下,这个组件不能在任何NgModule中声明。但它可以在NgModules和其他独立组件中被导入。
每个独立的组件/管道/指令现在都有机制可以直接在Decorator装饰器中导入它的依赖项:
@Component({
standalone: true,
selector: 'photo-gallery',
// an existing module is imported directly into a standalone component
// CommonModule imported directly to use standard Angular directives like *ngIf
// the standalone component declared above also imported directly
imports: [CommonModule, MatButtonModule, TableComponent],
template: `
...
<button mat-button>Next Page</button>
<app-table *ngIf="expression"></app-table>
`,
})
export class PhotoGalleryComponent {
}
正如我上面提到的,你可以在任何现有的ngModule中导入独立的组件。不再需要导入整个共享模块,我们可以只导入我们真正需要的东西。这也是一个开始使用新的独立组件的好策略:
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, TableComponent], // import our standalone TableComponent
bootstrap: [AppComponent]
})
export class AppModule {}
你可以通过输入Angular CLI创建独立的组件:
ng g component --standalone user
Bootstrap 无模块的应用
如果你想摆脱你的应用程序中的所有ngModules,你将需要以不同的方式启动你的应用程序。Angular有新的函数,你需要在main.ts文件中调用这个函数:
bootstrapApplication(AppComponent);
这个函数的第二个参数将允许你定义你在你的应用程序中需要的提供者。由于大多数提供者通常存在于模块中,Angular(目前)需要为它们使用一个新的importProvidersFrom
提取函数:
bootstrapApplication(AppComponent, { providers: [importProvidersFrom(HttpClientModule)] });
懒人加载独立组件的路线:
Angular有新的懒人-加载路由函数loadComponent
,它的存在正是为了加载独立的组件:
{
path: 'home',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
}
loadChildren
现在不仅允许懒人加载ngModule,而且还允许直接从路由文件中加载子路由:
{
path: 'home',
loadChildren: () => import('./home/home.routes').then(c => c.HomeRoutes)
}
关于本文的一些注意事项
- 独立组件的功能仍处于实验阶段。它在未来会变得更好,因为它将移到Vite builder而不是Webpack,更好的工具,更快的构建时间,更强大的应用架构,更容易的测试等等。但现在这些东西都没有了,所以我们没有得到整个包,但至少我们可以开始用新的Angular范式开发我们的应用程序。
- IDE和Angular工具还没有完全准备好静态地分析新的独立实体。因为你需要在每个独立实体中导入所有的依赖关系,万一你漏掉了什么,编译器也会漏掉它,并在运行时让你失败。这一点会随着时间的推移而得到改善,但现在需要开发人员更加关注导入。
- 目前Angular中没有全局导入(例如在Vue中),所以你需要在每个独立实体中完全导入每个依赖。我希望这个问题能在未来的版本中得到解决,因为在我看来,这个功能的主要目标是减少模板,让事情变得更简单。
先写到这,谢谢大家!