【荐】Angular 最佳实践 - CSDN博客

692 阅读5分钟
原文链接: blog.csdn.net

推荐文章

Armen VardanyanAngular: Best Practices

推荐理由

作者根据自身的项目实践,总结出了一些Angular的最佳实践。主要包涵了TypeScript类型的最佳使用,组件的合理使用,通用服务的封装,模版的合理定义。

文章概要

首先作者推荐在阅读原文之间先阅读官方的Angular风格指南,里面包涵了一些常见的设计模式和实用的实践。而文中提到的建议是在《风格指南》中找不到的。最佳实践建议如下:

利用好TypeScript类型

  1. 利用好类型的并集/交集
interface User {
  fullname: string;
  age: number;
  createdDate: string | Date;
}

此处的createdDate即可以是string类型,也可以是Date类型。

2.限制类型

interface Order {
  status: 'pending' | 'approved' | 'rejected';
}

可以指定status的数值只能是上述三者之一。

当然也可以通过枚举类型来代替这种方式:

enum Statuses {
  Pending = 1,
  Approved = 2,
  Rejected = 3
}

interface Order {
  status: Statuses;
}

3.设置“noImplicitAny”: true

在项目的tsconfig.json文件中,建议设置“noImplicitAny”: true。这样,所有未明确声明类型的变量都会抛出错误。

组件最佳实践

合理地利用组件,可以有效地减少代码冗余。

  1. 设置基础组件类
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
}

如果有很多组件都需要Statuses这个枚举接口的话,每次都需要重复声明。把它抽象成基础类,就不需要啦。

enum Statuses {
  Unread = 0,
  Read = 1
}

abstract class AbstractBaseComponent {
  statuses = Statuses;
  someOtherEnum = SomeOtherEnum;
  ... // 其他可复用的
}

@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}
  ];
}

再进一步,对于Angular表单,不同组件通常有共同的方法。一个常规的表单类如下:

@Component({
  selector: 'component-with-form',
  template: `...omitted for the sake of brevity`
})
class ComponentWithForm extends AbstractBaseComponent {
  form: FormGroup;
  submitted: boolean = false; // 标记用户是否尝试提交表单

  resetForm() {
    this.form.reset();
  }

  onSubmit() {
    this.submitted = true;
    if (!this.form.valid) {
      return;
    }
    // 执行具体的提交逻辑
  }
}

如果有很多地方用到表单,表单类中就会重复很多次上述的代码。 是不是把这些基础的方法抽象一下会更好呢?

abstract class AbstractFormComponent extends AbstractBaseComponent {
  form: FormGroup;
  submitted: boolean = false; 

  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();
    // 继续执行具体的提交逻辑
  }

}

2.善用容器组件

这点作者觉得可能有点争议,关键在于你要找到合适你的场景。具体是指,建议把顶级组件处理成容器组件,然后再定义一个接受数据的展示组件。这样的好处是,将获取输入数据的逻辑和组件内部业务操作的逻辑分开了,也有利于展示组件的复用。

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);
    /* 获取传递到真正的view组件的数据 */
  }

}

@Component({
  selector: 'app-user-component',
  template: `...displays the user info and some controls maybe`
})
class UserComponent {
  @Input() user;
}

3.组件化循环模板

在使用*ngFor指令时,建议将待循环的模板处理成组件:

<-- 不推荐 -->
<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>

<-- 推荐 -->

<user-detail-component *ngFor="let user of users" [user]="user"></user-detail-component>

这样做的好处在于减少父组件的代码,同时也将代码可能存在的业务逻辑移到子组件中。

封装通用的服务

提供合理结构的服务很重要,因为服务可以访问数据,处理数据,或者封装其他重复的逻辑。作者推荐的实践有以下几点:

  1. 统一封装API的基础服务

将基础的HTTP服务封装成一个基础的服务类:

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()}`);
  }

}

2.封装通用工具服务

项目中总有一些通用的方法,跟展示无关,跟业务逻辑无关,这些方法建议封装成一个通用的工具服务。

3.统一定义API的url

相对于直接在函数中写死,统一定义的方法更加利于处理:

enum UserApiUrls {
  getAllUsers = 'users/getAll',
  getActiveUsers = 'users/getActive',
  deleteUser = 'users/delete'
}

4.尽可能缓存请求结果

有些请求结果你可能只需要请求一次,比如地址库,某些字段的枚举值等。这时Rx.js的可订阅对象就发挥作用了。

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;
  }

}

模板

将复杂一点的逻辑移至类中,不推荐直接写在模版中。

比如表单中有个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]
    });
  }


}

但这里的判断逻辑很复杂,如果有多个控件的话,你就需要重复多次这个冗长的判断。建议处理成:


@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);
  }


}

大概就是这些啦。作者说他还没总结关于指令和管道部分的一些实践经验,他想专门再写一篇文章来说清楚Angular的DOM。我们拭目以待吧!

本文首发知乎野草。如有不当之处,欢迎指正。