[S3-E440]Angular 开发者常犯的错误

来源:互联网 发布:淘宝一件代发下单 编辑:程序博客网 时间:2024/06/16 12:13


Angular vs Angular 2 vs Angular 4

Angular 1.x 版本统称为 AngularJS,Angular 2+ (4/5) 统称为 Angular。


第三方库的命名也有一定的规则。假设早期版本的命名以 ng- 作为前缀,当 Angular 2 发布后,该库名称会使用 ng2- 作为前缀。但当 Angular 4 发布以后,新的命名规则就随之出现了。新的术语是使用 ngx- 作为前缀,因为 Angular 使用语义版本,每六个月会发布一个新版本。因此,举个例子,当我们把 ng2-bootstrap 更名为 ngx-bootstrap 后,今后就不需要再频繁更换库的名称了。


ngOnChanges vs ngDoCheck

AngularJS 使用 watcher 和 listener 的概念。watcher 是一个函数,返回被监测的值。通常情况下,这些值是对象模型的属性值。但也不总是数据模型的属性 - 我们可以跟踪组件的状态、计算新值等。如果该函数返回的值与前一次的值不一样,Angular 就会调用 listener,通常它用来更新 UI 状态。


Angular 移除了 watch 和 scope,现在我们将使用组件的输入属性。除此之外,Angular 为我们提供了 ngOnChanges 生命周期钩子。为了提高变化检测的性能,对于对象比较,Angular 内部直接使用 === 运算符进行值比较。因此当输入属性是引用类型,当改变对象内部属性时,是不会调用 ngOnChanges 生命周期钩子的。


// JS has NaN !== NaN

export function looseIdentical(a: any, b: any): boolean {

  return a === b || typeof a === 'number' && typeof b === 'number'

    && isNaN(a) && isNaN(b);

}

许多开发人员不知道这一点,陷入这个陷阱。为了解决这个问题,有各种解决方案:

1、使用 ngDoCheck 生命周期钩子

2、使用不可变的数据结构

3、把输入对象拆分为多个输入 (即不是直接传递引用对象,而是把内部属性抽离成独立的字段)

4、使用 subscriptions


使用 ngDoCheck 生命周期挂钩是解决此问题的常用方法。当变化检测运行时会自动调用此钩子。在使用此生命周期钩子时,你要小心控制该钩子的内部逻辑,因为通常每分钟会触发多次变化检测 (可以参考下面的源码)。

ngStyle 指令内部也实现了 DoCheck 接口,然后利用 KeyValueDiffer 对象来检测对象的变化 (如内部属性的新增、修改、移除操作)。

// packages/core/src/view/provider.ts

// 变化检测: 

// checkAndUpdateView -> Services.updateDirectives(view, CheckType.CheckAndUpdate)

function checkAndUpdateDirectiveInline(

    view: ViewData, 

    def: NodeDef, 

    v0: any, v1: any, v2: any,

    v3: any, v4: any, v5: any, 

    v6: any, v7: any, v8: any, 

    v9: any): boolean {

  const providerData = asProviderData(view, def.index);

  const directive = providerData.instance;

  let changed = false;

  let changes: SimpleChanges = undefined !;

  const bindLen = def.bindings.length;

  // 判断输入属性值是否改变,若发生改变则更新changes对象相应的属性。 

  if (bindLen > 0 && checkBinding(view, def, 0, v0)) {

    changed = true;

    changes = updateProp(view, providerData, def, 0, v0, changes);

  }

  // ...

  if (bindLen > 9 && checkBinding(view, def, 9, v9)) {

    changed = true;

    changes = updateProp(view, providerData, def, 9, v9, changes);

  }

  // 若输入属性发生变化才会调用ngOnChanges生命周期钩子

  if (changes) {

    directive.ngOnChanges(changes);

  }

  // 若首次执行变化检测及实现OnInit生命周期钩子,则调用ngOnInit生命周期钩子 

  if ((view.state & ViewState.FirstCheck) && (def.flags & NodeFlags.OnInit)) {

    directive.ngOnInit();

  }

  // 若实现DoCheck接口,则调用ngDoCheck生命周期钩子

  if (def.flags & NodeFlags.DoCheck) {

    directive.ngDoCheck();

  }

  return changed; // 返回SimpleChanges对象

}


未及时释放资源

你可能知道当你订阅 Observable 对象或设置事件监听时,在某个时间点,你需要执行取消订阅操作,进而释放操作系统的内存。否则,你的应用程序可能会出现内存泄露。


@Component({ ... })

export class HeroComponent implements OnInit, OnDestroy {

  heroForm: FormGroup;

  valueChanges$: Observable;


  ngOnInit() {

    this.valueChanges$ = this.heroForm.valueChanges.subscribe(...);

  }


  ngOnDestroy() {

    this.valueChanges$.unsubscribe();

  }

}

大多数情况下,当你在组件类中执行订阅操作,你可以在 ngOnDestroy 生命周期钩子中,执行取消订阅的操作。


额外取消订阅操作

上面介绍了在某些场景下需要手动执行取消订阅操作,进而释放相应的资源。但有些场景下,无需我们开发者手动执行额外的取消订阅操作。因为在这些场景下,Angular 内部会自动执行取消订阅操作。比如,使用 async 的场景:


@Component({

  selector: 'heroes-garden',

  template: `<hero [hero]="heroes$ | async"></todos>`

})

export class HeroesGardenComponent implements OnInit, OnDestroy {

  heroesChanged$: Observable;


  ngOnInit() {

    this.heroesChanged$ = this.store.select('heroes');

  }


  ngOnDestroy() {

    this.heroesChanged$.unsubscribe();

  }

}


除了使用 async 的场景外,还有以下场景会自动取消订阅:

1、Observer.timer(1000).subscribe(...)

2、http.get('https://segmentfault.com/u/').subscribe(...)

3、RxJS 中的 take()、takeWhile()、first() 等操作符

若想进一步了解手动释放资源和自动释放资源的场景,可以参考专栏 Angular 中何时取消订阅 这篇文章。


@Component.providers vs @NgModule.providers

分层依赖注入作为 Angular 的新机制的一部分,让我们可以灵活地控制依赖注入。在 AngularJS 中,服务都是单例的,而 Angular 2.x 以上的版本,我们可以多次实例化一个服务。


假设我们已经定义了一个 HeroesService 服务,用来获取英雄信息:


@Injectable()

export class HeroesService {

  heroes: Hero[] = [];

  

  constructor(private http: Http) {

    this.http.get('http://give-me-heroes.com')

      .map(res => res.json())

      .subscribe((heroes: Hero[]) => {

         this.heroes = heroes;

    });

  }


  getHeroes() {

    return this.heroes;

  }

}

正如你所见,我们在构造函数中获取英雄的数据,此外我们定义了 getHeroes() 方法,用来获取英雄信息。


现在我们来使用刚创建的 HeroesService 服务:


*在组件中声明服务

@Component({

  selector: 'hero',

  template: '...',

  providers: [HeroesService]

})

export class HeroComponent {

  constructor(private heroesService: HeroesService) {}

}


@NgModule({

  declarations: [HeroComponent]

}

export class HeroesModule { ... }


在 HeroComponent 中,我们在 @Component.providers 数组中声明 HeroesService 服务,然后在 HeroComponent 组件类的构造函数中注入该服务。使用这种方式会有问题,每当实例化新的 HeroComponent 组件时,都会创建一个新的 HeroService 实例,这会导致发送多次 Http 请求。


解决上述问题的一种方案是在 @NgModule.providers 中声明服务。


*在模块中声明服务

@Component({

  selector: 'hero',

  template: '...'

})

export class HeroComponent {

  constructor(private heroesService: HeroesService) {}

}


@NgModule({

  declarations: [HeroComponent],

  providers: [HeroesService]

}

export class HeroesModule { ... }


采用这种方式的话,对于多个 HeroComponent 组件,HeroesService 服务只会被实例化一次。因为,当在模块中声明 provider ,它所相关的依赖对象,将是单例的,其它的模块都能够使用它。我们不需要通过 @NgModule.exports 数组来导出对应的 provider,它会被自动导出。


直接操作 DOM

Angular 不再是简单的 Web 框架,Angular 是一个平台,它的一个优点是允许我们将应用程序代码与渲染器分离,从而编写可以在浏览器、服务器上运行的应用程序,甚至可以编写原生应用。


此外解耦后,也为我们提供更多的能力,如使用 AOT (Ahead of time) 或 Web Worker。AOT 意味着在构建阶段进行模板编译,AOT 编译模式的开发流程

1、使用 TypeScript 开发 Angular 应用

2、运行 ngc 编译应用程序

*使用 Angular Compiler 编译模板,一般输出 TypeScript 代码

*运行 tsc 编译 TypeScript 代码

3、使用 Webpack 或 Gulp 等其他工具构建项目,如代码压缩、合并等

4、部署应用


除此之外 AOT 还有以下优点

1、在客户端我们不需要导入体积庞大的 angular 编译器,这样可以减少我们 JavaScript 脚本库的大小

2、使用 AOT 编译后的应用,不再包含任何 HTML 片段,取而代之的是编译生成的 TypeScript 代码,这样的话 TypeScript 编译器就能提前发现错误。总而言之,采用 AOT 编译模式,我们的模板是类型安全的。


如果我们现在或将来要使用这种功能,我们需要遵守一定的规则。其中一个规则是不能使用 jQuery,document 对象或 ElementRef.nativeElement 来直接操作 DOM。具体示例如下:


@Component({ ... })

export class HeroComponent {

  constructor(private _elementRef: ElementRef) {}


  doBadThings() {

    $('.bad-with-jquery').click();

    this._elementRef.nativeElement.xyz = 'bad with native element';

    document.getElementById('bad-with-document');

  }

}


如你所见,doBadThings() 方法中有三行代码,这三行代码演示了直接操作 DOM 的三种方式。在 Angular 中我们推荐通过 Renderer2 服务执行 DOM 操作 (Angular 2 中使用 Renderer)。


@Component({ ... })

export class HeroComponent {

  constructor(

    private _renderer2: Renderer2,

    private _elementRef: ElementRef) {}


  doGoodThings() {

    this._renderer2.setElementProperty(this._elementRef,

      'some-property', true);

  }

}

上面代码中,我们通过依赖注入方式注入 Renderer2 和 ElementRef 实例,然后在 doGoodThings() 方法中调用 Renderer2 实例提供的 setElementProperty() 方法来设置元素的属性。 此外,为了方便开发者获取视图中的元素,Angular 为我们提供了 @ViewChild、@ViewChildren、@ContentChild 和 @ContentChildren 等装饰器。


渲染器是视图层的封装。当我们在浏览器中时,将使用默认渲染器。当应用程序在不同平台 (如 WebWorker ) 上运行时,渲染器将被替换为平台对应的渲染器。此渲染器需要实现 Renderer2 抽象类,并利用 DI (依赖注入) 机制作为默认的 Renderer 对象注入到组件或服务中。


若想深入了解 Angular 渲染器,可以参考专栏 Angular Renderer (渲染器) 这篇文章。


多次声明同一个组件

组件是 Angular 应用程序中的常见构建块。每个组件都需要在 @NgModule.declarations 数组中声明,才能够使用。


在 Angular 中是不允许在多个模块中声明同一个组件,如果一个组件在多个模块中声明的话,那么 Angular 编译器将会抛出异常。例如:


@Component({

  selector: 'hero',

  template: '...',

})

export class HeroComponent { ... }


@NgModule({

  declarations: [HeroComponent]

}

export class HeroesModule { ... }


@NgModule({

  declarations: [HeroComponent]

}

export class AnotherModule { ... }


如你所见,HeroComponent 组件在 HeroesModule 以及 AnotherModule 中进行声明。在多个模块中使用同一个组件是允许的。但当这种情况发生时,我们应该考虑模块之间的关系是什么。如果一个模块作为另一个模块的子模块,那么针对上面的场景解决方案将是:

1、在子模块的 @NgModule.declaration 中声明 HeroComponent 组件

2、通过子模块的 @NgModule.exports 数组中导出该组件

3、在父模块的 @NgModule.imports 数组中导入子模块


而对于其它情况,我们可以创建一个新的模块,如 SharedModule 模块。具体步骤如下:

1、在 SharedModule 中声明和导出 HeroComponent

2、在需要使用 HeroComponent 的模块中导入 SharedModule


NgModule({

  declarations: [HeroComponent],

  exports: [HeroComponent]

}

export class SharedModule { ... }


NgModule({

  imports: [SharedModule]

}

export class HeroesModule { ... }


@NgModule({

  imports: [SharedModule]

}

export class AnotherModule { ... }


参考资源

top-common-mistakes-of-angular-developers


转自:https://segmentfault.com/a/1190000010438679

作者:semlinker


>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

更多文章点击 目录 查看(或在导航菜单中查看

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

==========阅读原文==========