您的位置:首页 >聚焦 >

微头条丨Angular: 最佳实践

2022-08-08 15:42:10    来源:程序员客栈

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: `    
{{ notification.text }}
`})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: `    
{{ notification.text }}
`})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: ``})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 -->

{{user.name}}

<-- write this: -->

这在父组件中写更少的代码,让后允许委托任何重复逻辑到子组件。

服务 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 {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 {return this.get(this.relativeUrl);  }public getUserById(id: number): Observable {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 = 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 Subscriberspublic getCountries(): Observable {return this.countries;  }}

所以现在,不管什么时候你订阅这个国家列表,结果都会被缓存,以后你不再需要发起另一个 HTTP请求了。

模版 Templates

Angular是使用 html模版(当然,还有组件、指令和管道)去渲染你应用程序中的视图,所以编写模版是不可避免的事情,并且要保持模版的整洁和易于理解是很重要的。

从模版到组件方法的委托比原始的逻辑更难。请注意,这里我用了比原始更难的词语,而不是复杂这个词。这是因为除了检查直接的条件语句之外,任何逻辑都应该写在组件的类方法中,而不是写在模版中。在模版中写 *ngIf=”someVariable === 1”是可以的,其他很长的判断条件就不应该出现在模版中。

比如,你想在模版中为未正确填写表单控件添加 has-error类(也就是说并非所有的校验都通过)。你可以这样做:

@Component({  selector: "component-with-form",  template: `        
`})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: `        
`})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 和解决方案「持续更新... 」

如果读者觉得文章还可以,不防一键三连:关注➕点赞➕收藏

关键词: 应用程序 上传文件 生命周期

相关阅读