阅读 2351

【译】Angular最佳实践

DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)

引言

Angular 框架作为前端三大框架之一,有着其独到优点,可用于创建高效、复杂、精致的单页面应用。

本文介绍了Angular开发过程中推荐的十八个最佳实践及示例,用于开发过程中参考运用。

1. trackBy

What

当使用*ngFor指令在html中对数组进行陈列时,添加trackBy()函数,目的是为每个item指定一个独立的id

Why

一般情况下,当数组内有变更时,Angular将会对整个DOM树加以重新渲染。如果加上trackBy方法,Angular将会知道具体的变更元素,并针对性地对此特定元素进行DOM刷新,提升页面渲染性能

详细内容 -> NetanelBasal

Example

【Before】

<li *ngFor="let item of items;">{{ item }}</li>复制代码

【After】

// in the template
<li *ngFor="let item of items; trackBy: trackByFn">{{ item }}</li>

// in the component
trackByFn(index, item) {
  return item.id; // unique id corresponding to the item
}复制代码

2. const vs let

What

声明常量时,使用const而不是let

Why

a. 使赋值意图更加明确

b. 若常量被重赋值,编译将直接报错,避免潜在风险

c. 增加代码可读性

Example

【Before】

let car = 'ludicrous car';
let myCar = `My ${car}`;
let yourCar = `Your ${car};
if (iHaveMoreThanOneCar) {
  myCar = `${myCar}s`;
}
if (youHaveMoreThanOneCar) {
  yourCar = `${youCar}s`;
}复制代码

【After】

// 变量car不会被重赋值,所以用const声明
const car = 'ludicrous car';
let myCar = `My ${car}`;
let yourCar = `Your ${car};
if (iHaveMoreThanOneCar) {
  myCar = `${myCar}s`;
}
if (youHaveMoreThanOneCar) {
  yourCar = `${youCar}s`;
}复制代码

3. pipeable 操作符

What

使用RxJs算子时,使用pipeable操作符号 -> 拓展阅读

Why

a. 可被摇树优化: import的代码中,只有需要被执行的才会被引入

b. 容易定位到代码中未使用的算子

注: 需要RxJs版本在5.5及以上

Example

【Before】

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';

iAmAnObservable
.map(value => value.item)
.take(1)复制代码

【After】

import { map, take } from 'rxjs/operators';

iAmAnObservable
.pipe(
  map(value => value.item),
  take(1)
)复制代码

4. 隔离API攻击

What

不是所有的API都是安全的 -> 很多情况下需要添加额外的代码逻辑去为API打补丁

相较于将这些逻辑放在component中,更好的做法是封装到一个独立的地方:比如封装到service中,再在其他地方引用

Why

a. 隔离攻击,使得攻击更靠近于原有的请求所在地

b. 减少用于处理攻击打补丁的代码

c. 将这些攻击封装在同一个地方,更容易发现

d. 当要解决bug的时候, 只需要到同一个文件内去搜寻,更容易定位

注: 也可以打个自有标签,比如API_FIX,类似于TODO标签,用于标记API修复

5. 模板的订阅

What

最好在html订阅变化,而不是在ts中

Why

a. async管道能自动取消订阅:通过减少手动订阅管理能够简化代码

b. 减少在ts中忘记取消订阅,造成内存泄露的风险(这种风险也可以通过lint规则检测来避免)

c. 减少由于在订阅之外数据发生变更,进而引入bug的情况

Example

【Before】

// template
<p>{{ textToDisplay }}</p>

// component
iAmAnObservable
.pipe(
  map(value => value.item),
  takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item复制代码

【After】

// template
<p>{{ textToDisplay$ | async }}</p>

// component
this.textToDisplay$ = iAmAnObservable
.pipe(
  map(value => value.item)
)复制代码

6. 订阅清理

What

如果订阅了observable,记得通过take, takeUntil等操作符妥善取消订阅

Why

a. 如果不取消订阅,可能导致哪怕组件被销毁或者用户去到了其他页面了,但观察observable流始终保持进而造成内存泄露

b. 更好的做法是:通过lint规则检测来避免

Example

【Before】

iAmAnObservable
.pipe(
  map(value => value.item)
)
.subscribe(item => this.textToDisplay = item);复制代码

【After】

private _destroyed$ = new Subject();

public ngOnInit (): void {
  iAmAnObservable
  .pipe(
    map(value => value.item)
    // 被销毁前希望一直监听
    takeUntil(this._destroyed$)
  )
  .subscribe(item => this.textToDisplay = item);
}

public ngOnDestroy (): void {
  this._destroyed$.next();
  this._destroyed$.complete();
}复制代码

如果你只想要第一个值,那么就使用一个take(1)

iAmAnObservable
.pipe(
map(value => value.item),
take(1),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);复制代码

注: 此处takeUntil与take在此处同时被使用,目的是防止在组件被销毁前一直没有收到值,导致内存泄露。

(如果没有takeUntil,那么在获取到第一个值之前,这个订阅将持续存在,

而组件在被销毁后,由于不可能接收到第一个值,就会造成内存泄露)

7. 使用合适的操作符

What

选取合适的合并操作符

switchMap: 当你想要用新接收的值替换前面的旧值


mergeMap: 当你希望同时所有接收到的值进行操作


concatMap: 当你希望对接收到的值轮番处理


exhaustMap: 当还在处理前一个接收到的值时,取消处理后来值



Why

a. 相较于链式使用多个操作符,使用一个合适的操作符实现相同的目的有助于有效减少代码量

b. 不恰当地使用操作符可能导致预料外的行为,因为不同的操作符所实现的效果是不同的

8. 懒加载

What

如果条件允许的话,尝试在angular应用中懒加载模块。

懒加载是指仅在需要的情况下才加载模块内容

Why

a. 有效减少需要加载的应用体积

b. 通过避免加载不需要的模块,能够有效提升启动性能

Example

【Before】

{ path: 'not-lazy-loaded', component: NotLazyLoadedComponent }

【After】

// app.routing.ts

{ 
  path: 'lazy-load',
  loadChildren:  () => import(lazy-load.module).then(m => m.LazyLoadModule)
}

// lazy-load.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { LazyLoadComponent }   from './lazy-load.component';

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild([
         { 
             path: '',
             component: LazyLoadComponent 
         }
    ])
  ],
  declarations: [
    LazyLoadComponent
  ]
})
export class LazyModule {}复制代码

9. 避免嵌套订阅

What

一定情况下,可能需要从多个observable中获取数据去达到特定目的。

在这种情况下,避免在订阅块内嵌套订阅。

更好的方法是使用合适的链式操作符。

比如: withLatestFromcombineLatest

Why

代码异味/可读性/复杂度: 没有完全使用RxJs,表明开发者对RxJs的API浅层使用不熟悉

代码表现: 如果是冷observable,将会持续订阅第一个observable直到其complete,然后才是启动第二个observable的工作。

假如其中有网络请求,那么表现就会为瀑布流式的

Example

【Before】

firstObservable$.pipe(

take(1)

) .

subscribe(firstValue => {

secondObservable$.pipe(

take(1)

)

.subscribe(secondValue => {

console.log(`Combined values are: ${firstValue} &

${secondValue}`);

});

});

【After】

firstObservable$.pipe(

withLatestFrom(secondObservable$),

first()

)

.subscribe(([firstValue, secondValue]) => {

console.log(`Combined values are: ${firstValue} & ${secondValue}`);

});

10. 避免使用any,明确定义类型

What

声明变量或常量时,为其指定具体类型而不是简单使用any

Why

a. 当在TS中声明未指定类型的变量或者厂里,其类型将会由赋予的值推论得出,这容易引起意料之外的问题

一个经典的例子如下:

Example

【Before】

const x = 1;

const y = 'a';

const z = x + y;

console.log(`Value of z is: ${z}`

// 输出

Value of z is 1a

如果原来的预期输入是y也是个数字类型,那么就会导致意料之外的问题。

【After】

这些问题可以通过为声明变量指定一个恰当的类型来避免:

const x: number = 1;

const y: number = 'a';

const z: number = x + y;

// 这个输入将会导致编译报错抛出

Type '"a"' is not assignable to type 'number'.

const y:number

通过上述方法,可以避免由于类型缺失导致的bug

b. 指定类型的另一个好处是可以使得重构更简单,更安全

Example

【Before】

public ngOnInit (): void {

let myFlashObject = {

name: 'My cool name',

age: 'My cool age',

loc: 'My cool location'

}

this.processObject(myFlashObject);

}

public processObject(myObject: any): void {

console.log(`Name: ${myObject.name}`);

console.log(`Age: ${myObject.age}`);

console.log(`Location: ${myObject.loc}`);

}

// 输出

Name: My cool name

Age: My cool age

Location: My cool location

假如希望重命名myFlashObject中的loc属性名为location

public ngOnInit (): void {

let myFlashObject = {

name: 'My cool name',

age: 'My cool age',

location: 'My cool location'

}

this.processObject(myFlashObject);

}

public processObject(myObject: any): void {

console.log(`Name: ${myObject.name}`);

console.log(`Age: ${myObject.age}`);

console.log(`Location: ${myObject.loc}`);

}

// 输出

Name: My cool name

Age: My cool age

Location: undefined

当未对myFlashObject指定类型时,看起来方法loc属性在myFlashObject中不存在而不是属性取值错误导致的上述结果

【After】

当对myFlashObject增加了类型定义,我们将获取到一个更加清晰的编译报错问题如下

type FlashObject = {

name: string,

age: string,

location: string

}

public ngOnInit (): void {

let myFlashObject: FlashObject = {

name: 'My cool name',

age: 'My cool age',

// Compilation error

Type '{ name: string; age: string; loc: string; }' is not

assignable to type 'FlashObjectType'.

Object literal may only specify known properties, and 'loc'

does not exist in type 'FlashObjectType'.

loc: 'My cool location'

}

this.processObject(myFlashObject);

}

public processObject(myObject: FlashObject): void {

console.log(`Name: ${myObject.name}`);

console.log(`Age: ${myObject.age}`)

// Compilation error

Property 'loc' does not exist on type 'FlashObjectType'.

console.log(`Location: ${myObject.loc}`);

}

如果你正在开启一个全新的工程,推荐在tsconfig.json文件中设定strict:true方式,将严格模式打开,开启所有的严格类型检查选项

11. 使用lint规则

What

lint规则由多个预置的选项比如no-any, no-magic-numbers, no-consle 等,你可以在你的tslint.json文件中去开启特定的校验规则

Why

使用lint规则意味着,在某个地方有不应当产生发生的行为出现时,你将会得到较为清晰的报错

这将会提高你应用代码的一致性以及可读性

一些lint规则甚至有特定的fix解法用于解决此lint任务

如果你希望去定义自己的lint规则,你也可以去撰写

使用TSQuery去编写自己的lint规则的教程链接

一个经典的例子如下:

Example

【Before】

public ngOnInit (): void {

console.log('I am a naughty console log message');

console.warn('I am a naughty console warning message');

console.error('I am a naughty console error message');

}

// 输出

并不会报错,而是在控制台中打印如下信息:

I am a naughty console message

I am a naughty console warning message

I am a naughty console error message

【After】

// tslint.json

{

"rules": {

.......

"no-console": [

true,

"log", // no console.log allowed

"warn" // no console.warn allowed

]

}

}

// ..component.ts

public ngOnInit (): void {

console.log('I am a naughty console log message');

console.warn('I am a naughty console warning message');

console.error('I am a naughty console error message');

}

// Output

lint在console.log及log.warn语句处报错,console.error并不会报错,因为lint规则中未配置

Calls to 'console.log' are not allowed.

Calls to 'console.warn' are not allowed.

12. 精简,可重用的组件

What

将组件中可重用的代码片段抽取出来成为一个新的组件

让组件尽可能地“dumb”,从而能够在更多的场景中复用

编写“dumb”组件的意思是,其中没有隐含特别的逻辑,操作只是简单地依赖于提供给它的输入输出

作为一个通用的规则,在组件树中的最子节点的组件将会是其中最“dumb”的一个

Why

可重用的组件将会降低代码重复率,进而使其更易于维护及变更

dumb组件更加简单,因此存在bug的可能性也更低。dumb组件使得你去仔细思考如何抽取通用组件API,并且帮助你识别出混杂的问题

13. 组件只处理展示逻辑

What

避免将除了展示逻辑外的业务逻辑封装进组件,确保组件只用于处理展示逻辑

Why

a. 组件是为控制视图及展示目的而设计的,任何业务逻辑都应封装到自己合适的方法或者service内部,业务逻辑应与组件逻辑分离

b. 业务逻辑如果被抽取到一个service内部,通常更适用于使用单元测试,而且可以被其他需要相同的业务逻辑的组件重用

14. 避免长方法

What

长方法通常说明他们已经包含了太多的任务,尝试使用单一职责原则

一个方法应该作为整体去完成一件事情,如果其中有多个操作,那么我们可以抽取这些方法,形成独立的函数,使得他们独自负责各自职责,再去调用他们

Why

a. 长方法难以阅读、理解以及维护。他们容易产生bug,因为改变其中一部分很可能影响方法内的其他逻辑。这也使得代码重构更加难以进行

b. 方法可以用圈复杂度衡量,有一些TSLint方法用于检测圈复杂度,你可以在你的项目中去使用,避免bug以及检测代码可用性

15. Dry

What

Dry = Do not Repeat Yourself

保证在代码仓库中没有重复拷贝的代码,抽取重复代码,并且在需要使用的地方引用即可

Why

a. 在多个地方用重复代码意味着,如果我们想要改变代码逻辑,我们需要在多个地方修改,降低了代码的可维护性

使得对代码逻辑进行变更变得很困难而且测试过程很漫长

b. 抽取重复代码到一个地方,意味着只需要修改一处代码以及单次测试

c. 同时更少的代码意味着更快的速度

16. 增加缓存

What

发起API请求得到的响应通常并没有经常变化,在这类场景里,可以通过增加缓存机制并且储存获取的值

当同样的API请求再发起的时候,确认cache中是否已经有值,若有,则可以直接使用,否则发起请求并缓存。

如果这些值会变化但变化不频繁,那么可以引入一个缓存时间,用于决策是否使用缓存或者去重新调用

Why

具有缓存机制意味着可以避免不必要的API调用,通过避免重复调用有助于提高应用响应速度,不再需要等待网络返回,而且我们不需要重复地下载同样的信息

17. 避免模板中的逻辑

What

如果在HTML中需要增加任何逻辑,哪怕只是简单的&&,最好都将其抽取到组件内

Why

模板中的逻辑难以单元测试,当切换模板代码的时候容易导致代码问题

【Before】

// template

<p *ngIf="role==='developer'"> Status: Developer </p>

// component

public ngOnInit (): void {

this.role = 'developer';

}

【After】

<p *ngIf="showDeveloperStatus"> Status: Developer </p>

// component

public ngOnInit (): void {

this.role = 'developer';

this.showDeveloperStatus = true;

}

18. 安全地声明string类型

What

如果有一些string变量只有一些特定的值,相比于声明为string类型,更好的方式是将其声明为一个可能的值集合类型

Why

通过为变量提供恰当的声明有助于避免bug:当编写代码超出预期时可以在编译阶段被发现,而不是等运行了才发现

【Before】

private myStringValue: string;

if (itShouldHaveFirstValue) {
   myStringValue = 'First';
} else {
   myStringValue = 'Second'
}复制代码

【After】

private myStringValue: 'First' | 'Second';

if (itShouldHaveFirstValue) {
   myStringValue = 'First';
} else {
   myStringValue = 'Other'
}

// This will give the below error
Type '"Other"' is not assignable to type '"First" | "Second"'
(property) AppComponent.myValue: "First" | "Second"复制代码


加入我们

我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。


本文版权归原作者所有,仅用于学习与交流;
如需转载译文,烦请按下方注明出处信息,谢谢!
原文链接: Best practices for a clean and performant Angular application
作者: Vamsi Vempati
译者: DevUI 弘一