一个简洁、高效的 Angular 最佳实践

翻译自:Best practices for a clean and performant Angular application

1) trackBy

当我们在模板中使用 *ngFor 去循环遍历数组对象时,使用 trackBy 函数来为每一个数组对象来标记一个id是一个不错的选择。

为什么

当一个数组对象改变的时候, Angular 会重绘整个 DOM 树, 但是如果你使用 trackBy 函数的话,Angular 会知道哪个节点被更改并且只重绘更改的节点。

有关此内容的详细说明,请参阅 Angular 2 — Improve Performance with trackBy 的这篇文章(也可以查看我翻译的这篇文章 用 trackBy 改善 Angular 性能)。

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

声明变量时,在不打算重新赋值的地方请使用 const 来声明。

为什么

在适当的地方使用 letconst 可以使声明的意图更加明确。当你把一个值错误的赋值给常量的时候,会出现编译时错误,这有助于你提早发现问题,也使你的代码更具可读性。

Before
let car = 'ludicrous car';

let myCar = `My ${car}`;
let yourCar = `Your ${car};

if (iHaveMoreThanOneCar) {
   myCar = `${myCar}s`;
}

if (youHaveMoreThanOneCar) {
   yourCar = `${youCar}s`;
}
After
// the value of car is not reassigned, so we can make it a 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 operators

在使用 RxJs 操作符的时候,请使用管道运算符将他们联结起来。

为什么

Pipeable操作符是可 tree-shakeable,这意味着只有我们需要执行的代码在导入时才会被包含进来。这也使得识别文件中未使用的操作符变得很容易。

注意:这需要Angular5.5或以上版本。

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) Isolate API hacks

有时我们需要在代码中添加一些逻辑来弥补api中的错误。与其在需要的地方使用组件,不如将它们隔离在一个地方(比如服务中),并从组件中使用服务。

为什么

这有助于使补丁“更接近API”,从而使其尽可能靠近发出网络请求的地方。 这样,更少的代码可以处理未经篡改的代码。 同样,这是所有补丁生活的地方,更容易找到它们。 修复API中的错误时,在一个文件中查找它们比查找可能分布在整个代码库中的补丁更容易。

您还可以创建类似于TODO的自定义标签(例如API_FIX),并使用它来标记修订,以便于查找。

5) Subscribe in template

避免从组件订阅可观察对象,而从模板订阅可观察对象。

为什么

async 自动取消订阅,并且无需手动管理订阅,从而使代码更简单。 它还减少了因为忘记取消订阅而使该组件产生内存泄漏的风险。 也可以通过使用 lint 检测未订阅的可观察对象来减轻这种风险。

这还会阻止组件有状态,并在订阅之外引入数据发生突变的bug。

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) Clean up subscriptions

订阅可观察变量时,请始终确保使用诸如 taketakeUntil 等运算符来适当地取消订阅。

为什么

取消订阅可观察对象将导致可观的流泄漏,因为可观察流将保持打开状态,甚至可能在组件已被破坏(用户导航到另一个页面之后也是如此)。

更好的做法是,制定一条检测未取消订阅的可观察对象的规则。

Before
iAmAnObservable
    .pipe(
       map(value => value.item)     
     )
    .subscribe(item => this.textToDisplay = item);
After

当您要监听更改直到另一个可观察到的对象发出一个值时,请使用 takeUntil

private _destroyed$ = new Subject();

public ngOnInit (): void {
    iAmAnObservable
    .pipe(
       map(value => value.item)
      // We want to listen to iAmAnObservable until the component is destroyed,
       takeUntil(this._destroyed$)
     )
    .subscribe(item => this.textToDisplay = item);
}

public ngOnDestroy (): void {
    this._destroyed$.next();
    this._destroyed$.complete();
}

使用这样的私有 subject 是管理取消订阅组件中许多可观察对象的模式。

当只需要可观察对象发出的第一个值时,使用 take

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

注意 takeUntiltake 在这里的用法。这是为了避免订阅在组件被销毁之前没有收到值而导致的内存泄漏。在这里之前没有 takeUntil,订阅仍然会挂起,直到它得到第一个值,但是由于组件已经被销毁,它将永远不会得到导致内存泄漏的值。

7) Use appropriate operators

请参考这篇文章:RxJS: Avoiding switchMap-Related Bugs

8) Lazy load

如果可能的话,尝试在Angular应用程序中延迟加载模块。延迟加载是指仅在使用某个东西时才加载它,例如,仅在看到某个组件时才加载它。

为什么

这将减少要加载的应用程序的大小,并且可以通过不加载未使用的模块来改善应用程序的启动时间。

Before
// app.routing.ts

{ path: 'not-lazy-loaded', component: NotLazyLoadedComponent }
After
// app.routing.ts

{ 
  path: 'lazy-load',
  loadChildren: 'lazy-load.module#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) Avoid having subscriptions inside subscriptions

有时,您可能需要来自多个观察对象的值来执行操作。在这种情况下,避免订阅一个观察对象嵌套在另一个观察对象。相反,使用适当的链接操作符。链接操作符在来自前面操作符的可观察对象上运行。一些链接操作符是: withLatestFrom, combineLatest,等等。

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}`);
});
为什么

代码风格/可读性/复杂性:并未完全使用 RxJs,这表明开发人员对 RxJs API 使用不熟悉。

表现为:如果可观察对象使冷执行的,它将订阅 firstObservable,等待其完成,然后开始第二个可观察对象的工作。 如果这些是网络请求,它将表现为同步。

10) Avoid any; type everything;

不要用 any 来声明变量或常量。

为什么

在没有类型的 Typescript 中声明变量或常量时,变量/常量的类型将由分配给它的值来推导。 这将导致意外的问题。 一个经典的例子是:

const x = 1;
const y = 'a';
const z = x + y;

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

// Output
Value of z is 1a

当您期望y也是一个数字时,这可能会导致意外的问题。可以通过适当地标记类型来避免这些问题。

const x: number = 1;
const y: number = 'a';
const z: number = x + y;

// This will give a compile error saying:

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

const y:number

通过这种方式,我们可以避免由于缺少类型而导致的bug。在应用程序中拥有良好类型的另一个优点是,它使重构更容易、更安全。

思考一下这个例子:

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

// Output
Name: My cool name
Age: My cool age
Location: My cool location

假设,我们想要将 myFlashObject 对象中的属性 loc 重命名 location

如果我们在myFlashObject上没有类型,它会认为myFlashObject上的属性loc是未定义的,而不是一个有效的属性。如果我们有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) Make use of lint rules

tslint 已经内置了各种选项,比如no-anyno-magic-numbersno-console 等等,您可以在 tslint 中配置这些选项。在代码库中执行特定的规则。

为什么

lint 规则放在适当的位置意味着当您在做不应该做的事情时,您将得到一个不错的错误。这将加强应用程序的一致性和可读性。请参考 这里 以获得更多您可以配置的规则。一些 lint 规则甚至附带了解决 lint 错误的补丁。如果您想配置自己的自定义 lint 规则,也可以这样做。请参考 这篇文章 ,了解如何使用 TSQuery 编写自己的自定义 lint 规则。

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

// Output
No errors, prints the below on console window:
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 errors for console.log and console.warn statements and no error for console.error as it is not mentioned in the config

Calls to 'console.log' are not allowed.
Calls to 'console.warn' are not allowed.

12) Small reusable components

提取可在组件中重用的部分,并使其成为新的组件。使组件尽可能地非智能,因为这将使它在更多的场景中工作。使组件成为非智能组件意味着该组件没有任何特殊的逻辑,并且完全根据提供给它的输入和输出进行操作。一般来说,组件树中的最后一个子元素是最非智能的。

为什么

可重用组件减少了代码的重复,因此更易于维护和更改。非智能组件更简单,所以它们不太可能有bug。非智能组件使您更仔细地考虑公共组件API,并帮助嗅探出混合的问题。

13) Components should only deal with display logic

尽可能避免在组件中使用除显示逻辑之外的任何逻辑,并使组件只处理显示逻辑。

为什么

组件是为表示目的而设计的,并控制视图应该做什么,任何业务逻辑都应酌情提取到其自己的方法/服务中,以将业务逻辑与视图逻辑分开。

将业务逻辑提取到服务时,通常更容易进行单元测试,并且可以由需要应用相同业务逻辑的任何其他组件重用。

14) Avoid long methods

过长的方法通常表示它们做的事情太多了。尝试使用单一责任原则。方法本身作为一个整体可能在做一件事,但是在它内部,可能发生一些其他的操作。我们可以将这些方法提取到它们自己的方法中,并让它们各自做一件事,然后使用它们。

为什么

长方法很难阅读、理解和维护。它们还容易出现bug,因为更改一件事情可能会影响该方法中的许多其他事情。它们还使重构(这在任何应用程序中都是一个关键问题)变得困难。

这有时被称为:“糟糕复杂性”, 这里也有一些 TSLint rule 来检查认知复杂性。 您可以在您的项目中使用它来避免bug并检测代码风格和可维护性问题。

15) DRY

不要写重复代码。确保没有将相同的代码复制到代码库的不同位置。提取重复的代码并使用。

为什么

在多个地方使用相同的代码意味着,如果我们想要更改代码中的逻辑,就必须在多个地方执行。这使得它很难维护,也容易出现bug,我们可能会错过在所有情况下更新它。修改逻辑需要更长的时间,测试也是一个漫长的过程。在这些情况下,提取重复的代码并使用它。这意味着只有一个地方需要改变,只有一件事需要测试。将较少的重复代码交付给用户意味着应用程序将更快。

16) Add caching mechanisms

在进行API调用时,其中一些API的响应并不经常更改。在这些情况下,可以添加缓存机制并存储来自API的值。当对相同API发出另一个请求时,检查缓存中是否有它的值,如果有,就使用它。否则,调用API并缓存结果。如果值变化不频繁,您可以引入一个缓存时间,您可以检查上一次缓存的时间并决定是否调用API。

为什么

拥有缓存机制意味着避免不必要的API调用。通过只在需要时进行API调用并避免重复,应用程序的速度得到了提高,因为我们不必等待网络。这也意味着我们不会一次又一次地下载相同的信息。

17) Avoid logic in templates

如果您的模板中有任何类型的逻辑,即使是简单的&&语句,也最好将其提取到其组件中。

为什么

在模板中包含逻辑意味着不可能对其进行单元测试,因此在更改模板代码时更容易出现bug。

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

// component
public ngOnInit (): void {
    this.role = 'developer';
}
After
// template
<p *ngIf="showDeveloperStatus"> Status: Developer </p>

// component
public ngOnInit (): void {
    this.role = 'developer';
    this.showDeveloperStatus = true;
}

18) Strings should be safe

如果您有一个类型为string的变量,它只能有一组值,那么您可以将值声明为类型,而不是将它声明为string类型。

为什么

通过适当地声明变量的类型,我们可以在编译时而不是运行时编写代码时避免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"

重点

状态管理

使用 @ngrx / store 维护应用程序的状态,并使用 @ngrx / effects 作为 store 的副作用模型。 状态更改由操作来描述,而更改由称为 reducers 的纯函数完成。

为什么

@ngrx / store 将所有与状态相关的逻辑隔离在一个地方,并使其在整个应用程序中保持一致。 当访问 store 中的信息以实现更高性能的应用程序时,它也具有备忘录机制。 @ngrx / storeAngular 的更改检测策略相结合,可以加快应用程序的速度。

Immutable state

使用 @ngrx / store 时,请考虑使用 ngrx-store-freeze使状态不变。 ngrx-store-freeze 通过引发异常来防止状态发生变化。 这避免了状态的意外突变导致不必要的后果。

为什么

组件的状态变化导致应用程序的行为不一致,这取决于所加载的组件的顺序。它打破了 redux 模式的思维模式。如果存储状态更改并重新发出,更改可能会被覆盖。关注点分离的组件是视图层,它们不应该知道如何改变状态。

Jest

JestFacebook 的 JavaScript 单元测试框架。 通过跨代码库并行运行测试,可以使单元测试更快。 在其监视模式下,仅运行与所做更改相关的测试,这使得测试的反馈回路更短。 Jest还提供了测试的代码覆盖,并且在 VS CodeWebstorm 上受支持。

您可以使用Jest的 预设置,在项目中设置 Jest 时,它将为您完成大部分繁重的工作。

Karma

KarmaAngularJS 团队开发的测试运行程序。 它需要一个真实的 browser/DOM 来运行测试。 它也可以在不同的浏览器上运行。 Jest 不需要 chrome headless / phantomjs 即可运行测试,并且可以在纯 Node 中运行。

Universal

如果你还没有让你的应用程序成为一个通用的应用程序,现在是时候了。Angular Universal 允许您在服务器上运行 Angular 应用程序,并执行服务器端呈现 (SSR) ,即提供静态的预呈现html页面。这使得应用程序超级快,因为它几乎可以立即在屏幕上显示内容,而不必等待 JS 包加载和解析,或者 Angular 引导。

它对搜索引擎优化也很友好,因为Angular Universal生成静态内容,使web爬虫更容易对应用程序建立索引,并使其在不执行JavaScript的情况下也能被搜索到。

为什么

Universal 可以极大地提高应用程序的性能。我们最近更新了我们的应用做服务器端渲染和网站加载时间从几秒到几十毫秒!!它还允许你的网站在社交媒体预览片段中正确显示。第一个有意义的绘制是非常快的,让用户可以看到内容,没有任何不必要的延迟。

结语

构建应用程序是一个持续的过程,总是有改进的空间。这个优化列表是一个很好的开始,持续地应用这些模式将使您的团队感到愉快。您的用户也会喜欢您的作品,因为您的应用程序的bug更少,性能更好。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容