Angular: 最佳实践
共 9644字,需浏览 20分钟
·
2022-08-08 14:46
Note: 本文中,我将尽量避免官方在 Angular Style Guide 提及的模式和有用的实践,而是专注我自己的经验得出的东西,我将用例子来说明。如果你还没读过官网指引,我建议你在阅读本文之前读一下。因为官网涵盖了本文很多没介绍的东西。
本文将分为几个章节来讲解,这些章节根据应用核心需求和生命周期来拆分。现在,我们开始吧!
类型规范 Typing
我们主要是用 TypeScript
去编写 Angular
(也许你只是用 JavaScript 或者谷歌的 Dart 语言去写),Angular
被称为 TYPEScript
也是有原因的。我们应该为我们数据添加类型限定,下面有些有用的知识点:
使用类型联合和交集。官网解释了如何使用 TS
编译器组合类型以轻松工作。这在处理来自 RESTful API
数据的时非常有用。如下例子:
interface User {
fullname: string,
age: number,
createDate: string | Date
}
上面 createdDate
字段的类型不是 JS Date
就是字符串。这很有用,因为当服务端提供一个 User
实例数据给你,它只能返回字符串类型的时间给你,但是你可能有一个 datepicker
控件,它将日期作为有效的 JS Date
对象返回,并且为了避免数据被误解,我们需要在 interface
里面可选指明。
限制你的类型。在 TypeScript
中,你可以限制字段的值或者变量的值,比如:
interface Order {
status: 'pending' | 'approved' | 'rejected'
}
这实际上变成了一个标志。如果我们有一个 Order
类型的变量,我们只能将这三个字符串中的一个分配给 status
字段,分配其他的类型 TS
编辑器都会跑出错误。
enum Statuses {
Pending = 1,
Approved = 2,
Rejected = 3
}
interface Order {
status: Statuses;
}
**考虑设置 noImplicitAny: true
**。在应用程序的 tsconfig.json
文件中,我们可以设置这个标志,告诉编辑器在未明确类型时候抛出错误。否则,编辑器坚定它无法推断变量的类型,而认为是 any
类型。实际情况并非如此,尽管将该标志设置为 true
会导致发生意想不到的复杂情况,当会让你的代码管理得很好。
严格类型的代码不容易出错,而 TS
刚好提供了类型限制,那么我们得好好使用它。
组件 Component
组件是 Angular
的核心特性,如果你设法让它们被组织得井井有条,你可以认为你工作已经完成了一半。
考虑拥有一个或者几个基本组件类。如果你有很多重复使用的内容,这将很好用,我们可不想讲相同的代码编写多次吧。假设有这么一个场景:我们有几个页面,都要展示系统通知。每个通知都有已读/未读两种状态,当然,我们已经枚举了这两种状态。并且在模版中的每个地方都会显示通知,你可以使用 ngClass
设置未通知的样式。现在,我们想将通知的状态与枚举值进行比较,我们必须将枚举导入组件。
enum Statuses {
Unread = 0,
Read = 1
}
@Component({
selector: 'component-with-enum',
template: `
<div *ngFor="notification in notifications"
[ngClass]="{'unread': notification.status == statuses.Unread}">
{{ notification.text }}
</div>
`
})
class NotificationComponent {
notifications = [
{text: 'Hello!', status: Statuses.Unread},
{text: 'Angular is awesome!', status: Statuses.Read}
];
statuses = Statuses
}
这里,我们为每个包含未读通知的 HTML
元素添加了 unread
类。注意我们是怎么在组件类上创建一个 statuses
字段,以便我们可以在模版中使用这个枚举。但是假如我们在多个组件中使用这个枚举呢?或者假如我们要在不同的组件使用其他枚举呢?我们需要不停创建这些字段?这似乎很多重复代码。我们看看下面例子:
enum Statuses {
Unread = 0,
Read = 1
}
abstract class AbstractBaseComponent {
statuses = Statuses;
someOtherEnum = SomeOtherEnum;
... // lots of other reused stuff
}
@Component({
selector: 'component-with-enum',
template: `
<div *ngFor="notification in notifications"
[ngClass]="{'unread': notification.status == statuses.Unread}">
{{ notification.text }}
</div>
`
})
class NotificationComponent extends AbstractBaseComponent {
notifications = [
{text: 'Hello!', status: Statuses.Unread},
{text: 'Angular is awesome!', status: Statuses.Read}
];
}
所以,现在我们有一个基本组件(实际上就是一个容器),我们的组件可以从中派生以重用应用程序的全局值和方法。
另一种情况经常在 forms
表单中被发现。如果在你的 Angular
组件中有个表单,你可能有像这样的字段或者方法:
@Component({
selector: 'component-with-form',
template: `...omitted for the sake of brevity`
})
class ComponentWithForm extends AbstractBaseComponent {
form: FormGroup;
submitted: boolean = false; // a flag to be used in template to indicate whether the user tried to submit the form
resetForm() {
this.form.reset();
}
onSubmit() {
this.submitted = true;
if (!this.form.valid) {
return;
}
// perform the actual submit logic
}
}
当然,如果你正在大量组件中使用 Angular
表单,那么将这些逻辑移动到一个基础类会更友好...但是你不需要继承 AbstractBaseComponent
,因为不是每个组件都有 form
表单。像下面这样做比较好:
abstract class AbstractFormComponent extends AbstractBaseComponent {
form: FormGroup;
submitted: boolean = false; // a flag to be used in template to indicate whether the user tried to submit the form
resetForm() {
this.form.reset();
}
onSubmit() {
this.submitted = true;
if (!this.form.valid) {
return;
}
}
}
@Component({
selector: 'component-with-form',
template: `...omitted for the sake of brevity`
})
class ComponentWithForm extends AbstractFormComponent {
onSubmit() {
super.onSubmit();
// continue and perform the actual logic
}
}
现在,我们为使用表单的组件创建了一个单独的类(注意:AbstractFormComponent
是如何继承 AbstractBaseComponent
,因此我们不会丢失应用程序的值)。这是一个不错的示范,我们可以在真正需要的地方广泛使用它。
容器组件。 这可能有些争议,但是我们仍然可以考虑它是否适合我们。我们知道一个路由对应一个 Angular
组件,但是我推荐你使用容器组件,它将处理数据(如果有数据需要传递的话)并将数据传递给另外一个组件,该组件将使用输入所包含的真实视图和 UI
逻辑。下面就是一个例子:
const routes: Routes = [
{path: 'user', component: UserContainerComponent}
];
@Component({
selector: 'user-container-component',
template: `<app-user-component [user]="user"></app-user-component>`
})
class UserContainerComponent {
constructor(userService: UserService) {}
ngOnInit(){
this.userService.getUser().subscribe(res => this.user = user);
/* get the user data only to pass it down to the actual view */
}
}
@Component({
selector: 'app-user-component',
template: `...displays the user info and some controls maybe`
})
class UserComponent {
@Input() user;
}
在这里,容器执行数据的检索(它也可能执行一些其他常见的任务)并将实际的工作委托给另外一个组件。当你重复使用同一份 UI
并再次使用现有的数据时,这可能派上用场,并且是关注点分离的一个很好的例子。
小经验:当我们在带有子元素的 HTML
元素上编写 ngFor
指令时,请考虑将该元素分离为单独的组件,就像下面:
<-- instead of this -->
<div *ngFor="let user of users">
<h3 class="user_wrapper">{{user.name}}</h3>
<span class="user_info">{{ user.age }}</span>
<span class="user_info">{{ user.dateOfBirth | date : 'YYYY-MM-DD' }}</span>
</div>
<-- write this: -->
<user-detail-component *ngFor="let user of users" [user]="user"></user-detail-component>
这在父组件中写更少的代码,让后允许委托任何重复逻辑到子组件。
服务 Services
服务是 Angular
中业务逻辑存放和数据处理的方案。拥有提供数据访问、数据操作和其他可重用逻辑的结构良好的服务非常重要。所以,下面有几条规则需要考虑下:
有一个 API 调用的基础服务类。将简单的 HTTP 服务逻辑放在基类中,并从中派生 API 服务。像下面这样:
abstract class RestService {
protected baseUrl: 'http://your.api.domain';
constructor(private http: Http, private cookieService: CookieService){}
protected get headers(): Headers {
/*
* for example, add an authorization token to each request,
* take it from some CookieService, for example
* */
const token: string = this.cookieService.get('token');
return new Headers({token: token});
}
protected get(relativeUrl: string): Observable<any> {
return this.http.get(this.baseUrl + relativeUrl, new RequestOptions({headers: this.headers}))
.map(res => res.json());
// as you see, the simple toJson mapping logic also delegates here
}
protected post(relativeUrl: string, data: any) {
// and so on for every http method that your API supports
}
}
当然,你可以写得更加复杂,当用法要像下面这么简单:
@Injectable()
class UserService extends RestService {
private relativeUrl: string = '/users/';
public getAllUsers(): Observable<User[]> {
return this.get(this.relativeUrl);
}
public getUserById(id: number): Observable<User> {
return this.get(`${this.relativeUrl}${id.toString()}`);
}
}
现在,你只需要将 API
调用的逻辑抽象到基类中,现在就可以专注于你将接收哪些数据以及如何处理它。
考虑有方法(Utilites)服务。有时候,你会发现你的组件上有一些方法用于处理一些数据,可能会对其进行预处理或者以某种方式进行处理。示例可能很多,比如,你的一个组件中可能具有上传文件的功能,因此你需要将 JS File
对象的 Array
转换为 FormData
实例来执行上传。现在,这些没有涉及到逻辑,不会以任何的方式影响你的视图,并且你的多个组件中都包含上传文件功能,因此,我们要考虑创建 Utilities
方法或者 DataHelper
服务将此类功能移到那里。
使用 TypeScript 字符串枚举规范 API url。你的应用程序可以和不同的 API
端进行交互,因此我们希望将他们移动到字符串枚举中,而不是在硬编码中体现,如下:
enum UserApiUrls {
getAllUsers = 'users/getAll',
getActiveUsers = 'users/getActive',
deleteUser = 'users/delete'
}
这能更好得了解你的 API
是怎么运作的。
尽可能考虑缓存我们的请求。Rx.js
允许你去缓存 HTTP
请求的结果(实际上,任何的 Observable
都可以,但是我们现在说的是 HTTP
这内容),并且有一些示例你可能想要使用它。比如,你的 API
提供了一个接入点,返回一个 Country
对象 JSON
对象,你可以在应用程序使用这列表数据实现选择国家/地区的功能。当然,国家不会每天都会发生变更,所以最好的做法就是拉取该数据并缓存,然后在应用程序的生命周期内使用缓存的版本,而不是每次都去调用 API
请求该数据。Observables
使得这变得很容易:
class CountryService {
constructor(private http: Http) {}
private countries: Observable<Country[]> = this.http.get('/api/countries')
.map(res => res.json())
.publishReplay(1) // this tells Rx to cache the latest emitted value
.refCount(); // and this tells Rx to keep the Observable alive as long as there are any Subscribers
public getCountries(): Observable<Country[]> {
return this.countries;
}
}
所以现在,不管什么时候你订阅这个国家列表,结果都会被缓存,以后你不再需要发起另一个 HTTP
请求了。
模版 Templates
Angular
是使用 html
模版(当然,还有组件、指令和管道)去渲染你应用程序中的视图
,所以编写模版是不可避免的事情,并且要保持模版的整洁和易于理解是很重要的。
从模版到组件方法的委托比原始的逻辑更难。请注意,这里我用了比原始更难
的词语,而不是复杂
这个词。这是因为除了检查直接的条件语句之外,任何逻辑都应该写在组件的类方法中,而不是写在模版中。在模版中写 *ngIf=”someVariable === 1”
是可以的,其他很长的判断条件就不应该出现在模版中。
比如,你想在模版中为未正确填写表单控件添加 has-error
类(也就是说并非所有的校验都通过)。你可以这样做:
@Component({
selector: 'component-with-form',
template: `
<div [formGroup]="form"
[ngClass]="{
'has-error': (form.controls['firstName'].invalid && (submitted || form.controls['firstName'].touched))
}">
<input type="text" formControlName="firstName"/>
</div>
`
})
class SomeComponentWithForm {
form: FormGroup;
submitted: boolean = false;
constructor(private formBuilder: FormBuilder) {
this.form = formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required]
});
}
}
上面 ngClass
声明看起来很丑。如果我们有更多的表单控件,那么它会使得视图更加混乱,并且创建了很多重复的逻辑。但是,我们也可以这样做:
@Component({
selector: 'component-with-form',
template: `
<div [formGroup]="form" [ngClass]="{'has-error': hasFieldError('firstName')}">
<input type="text" formControlName="firstName"/>
</div>
`
})
class SomeComponentWithForm {
form: FormGroup;
submitted: boolean = false;
constructor(private formBuilder: FormBuilder) {
this.form = formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required]
});
}
hasFieldError(fieldName: string): boolean {
return this.form.controls[fieldName].invalid && (this.submitted || this.form.controls[fieldName].touched);
}
}
现在,我们有了个不错的模版,甚至可以轻松地测试我们的验证是否与单元测试一起正常工作,而无需深入查看视图。
读者可能意识到我并没有写关于 Directives
和 Pipes
的相关内容,那是因为我想写篇详细的文章,关于 Angular
中 DOM
是怎么工作的。所以本文着重介绍 Angular
应用中的 TypeScript
的内容。
希望本文能够帮助你编写更干净的代码,帮你更好组织你的应用结构。请记住,无论你做了什么决定,请保持前后一致(别钻牛角尖...)。
本文是译文,采用的是意译的方式,其中加上个人的理解和注释,原文地址是:https://medium.com/codeburst/angular-best-practices-4bed7ae1d0b7
往期精彩推荐
- Dart 知识点 - 数据类型
- Flutter 开发出现的那些 Bugs 和解决方案「持续更新... 」
如果读者觉得文章还可以,不防一键三连:关注➕点赞➕收藏