Angular应用常见错误

1.导入所需的角度模块

Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’

此错误表明,尚未将 angular Forms Module导入到您的模块中。

Unhandled Promise rejection: No provider for HttpClient!

此错误告诉您尚未将angular HttpClient 模块导入到您的(根)模块中。

解决方案

要解决此问题,您需要将缺少的模块导入模块中。在大多数情况下,该模块将是您应用目录中的AppModule

src/app/app.module.ts

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule, HttpClientModule],
  bootstrap: [AppComponent],
})
export class AppModule {}

只导入您真正需要的模块!不必要地导入模块会大大增加应用程序的大小。
该建议不仅适用于框架Angular模块。它也适用于您可能要使用的任何Angular模块,包括第三方模块。

BrowserModule 
FormsModule //需要使用ngModel指令
HttpClientModule // //以前是HttpModule 
RouterModule 
BrowserAnimationsModule / NoopAnimationsModule

第三方库最好将模块拆分为尽可能多的模块,以使应用程序的大小保持较小。例如,对于Angular Material,您必须为所使用的每个组件导入一个模块。

MatMenuModule 
MatSidenavModule 
MatCheckboxModule 
MatDatepickerModule 
MatInputModule 
...

2.不要使用DOM引用在它们存在之前(@ViewChild)

在@ViewChild装饰器的帮助下,angular使引用组件的特定子元素(html节点或组件)变得非常容易。您需要做的就是向模板内部的节点或组件添加引用标识符。只需在节点属性旁边添加一个#后跟一个名称。

<div #myDiv></div>

现在,我们可以从组件中引用该元素。如果它是一个组件,则可以调用其公共方法并访问其属性。如果它是纯HTML元素,则可以更改其样式,属性或子元素。

如果我们使用@ViewChild()装饰器装饰该属性,Angular会自动将引用分配给该组件的属性。确保将参考名称传递给装饰器。例如@ViewChild(‘myDiv’)。

import { ViewChild } from '@angular/core'

@Component({})
export class ExampleComponent {
  @ViewChild('myDiv') divReference
}

问题

@ViewChild()指令非常有用。但是您必须记住这一点:

如果元素确实存在,则只能使用对该元素的引用!

为什么不应该这样做?有许多原因导致您所引用的元素实际上不存在。

最常见的原因是,浏览器尚未完成创建并且尚未将其添加到DOM。如果您尝试在添加之前使用它,它将无法正常工作并使您的应用程序崩溃。如果您通常对JavaScript熟悉,那么您可能偶然发现了该问题,因为它不是特定于angular的。

在不存在DOM时访问DOM的一个示例是在组件的构造函数中。另一个将在ngOnInit生命周期回调中。

这以下代码是不起作用的:

import { ViewChild, OnInit } from '@angular/core'

@Component({})
export class ExampleComponent implements OnInit {
  @ViewChild('myDiv') divReference

  constructor() {
    let ex = this.divReference.nativeElement // divReference是未定义的
  }

  ngOnInit() {
    let ex = this.divReference.nativeElement // divReference是未定义的
  }
}

解决方案

就像DOMContentLoaded事件或JQuery中的 $(document).ready() 回调一样,angular也具有类似的机制来通知您所有HTML元素均已创建。它称为ngAfterViewInit生命周期挂钩。这是您应该实现的回调,当所有组件视图和子视图都初始化时被触发。这样,您几乎可以安全地在该回调内部访问viewChild引用。

import { ViewChild, AfterViewInit } from '@angular/core'

@Component({})
export class ExampleComponent implements AfterViewInit {
  @ViewChild('myDiv') divReference

  ngAfterViewInit() {
    let ex = this.divReference.nativeElement // divReference 不是未定义的
  }
}

还有一个陷阱要注意。

如前所述,您只能访问实际存在的元素。正如我们将在下一章中讨论的那样,具有错误* ngIf指令的元素已从DOM中完全删除。因此,在这种情况下,我们将无法访问它们。

为防止应用程序崩溃,在使用引用之前,应检查引用是否为null。顺便说一句,该建议不仅适用于组件或角度,而且还适用于所有编程语言。

import { ViewChild, AfterViewInit } from '@angular/core'

@Component({})
export class ExampleComponent implements AfterViewInit {
  @ViewChild('myDiv') divReference

  ngAfterViewInit() {
    let ex
    if (this.divReference) {
      ex = this.divReference.nativeElement // divReference 不是未定义的
    }
  }
}

3.不要直接操作DOM-Angular Universal

在Angular中直接操作DOM不仅被认为是不好的做法。
这也可能导致您的角度应用程序无法在浏览器以外的其他环境中运行。最受欢迎的示例是所谓的Angular Universal项目,该项目使您的angular应用程序可以在服务器端呈现。阅读有关Angular通用和服务器端渲染的所有信息。

此示例不起作用:

import { ViewChild, AfterViewInit } from '@angular/core'

@Component({})
export class ExampleComponent implements AfterViewInit {
  @ViewChild('myDiv') divReference

  ngAfterViewInit() {
    let ex = this.divReference.nativeElement
    ex.style.color = 'red' // 在服务器上不起作用
  }
}

解决方案

而不是直接更改元素,而应间接操作它们。为此,angular以Renderer2类的形式提供了一个渲染API 。

借助该渲染器,我们仍然可以处理使用DOM时所习惯的一切。但是通过使用渲染器,我们可以确保我们的代码在服务器上以及在客户端上都能正常工作。

那我们应该如何做:

1、通过构造函数中的依赖注入请求来获取Renderer2的实例

import { ViewChild, Renderer2 } from '@angular/core'

@Component({})
export class ExampleComponent {
  @ViewChild('myDiv') divReference

  constructor(private renderer: Renderer2) {}
}

2、使用渲染器间接操作DOM。再次,确保对元素的引用确实存在。

import { ViewChild, Renderer2, AfterViewInit } from '@angular/core'

@Component({})
export class ExampleComponent implements AfterViewInit {
  @ViewChild('myDiv') divReference

  constructor(private renderer: Renderer2) {}

  ngAfterViewInit() {
    if (this.divReference)
      this.renderer.setStyle(this.divReference.nativeElement, 'color', 'red')
  }
}

Renderer2有许多不同的方法来更改元素。您可以在官方文档中找到解决方法。

4.避免重复的提供者(providers)互相覆盖

您可能已经听说过angular使用了一种称为依赖注入的概念。借助依赖注入,您可以在构造函数中请求服务实例。

为此,必须在组件或模块装饰器的提供者部分中注册服务或更广泛的注射剂。最常见的方法是在模块级别提供它。

补图
这里的问题是,角度使用了分层依赖注入系统。这意味着,根模块(AppModule)中提供的服务/注射剂可用于该模块中的所有组件。并且由于该模块应包含所有其他组件和模块,因此此处提供的服务在整个应用程序中都可用。

补图
如果您为子模块提供服务,则该服务仅可用于该子模块。这也意味着,如果您在两个模块中都提供服务,则子模块中的组件将获得与任何其他组件不同的服务实例。如果您假定服务是应用程序中的唯一实例(单例),则可能导致各种错误。

解决方案很简单。只提供一次服务。通常在AppModule中。除非您知道自己在做什么,否则就应该坚持下去,尤其是刚开始的时候。在99%的情况下,您应该可以这样做。

5. Angular Guard不是安全功能

Angular Guard是人为地限制进入某些路线的好方法。例如,在实际向用户显示页面之前检查用户是否已登录。
这是一个警卫的简单例子:

import { Injectable } from '@angular/core'
import { AuthenticationService } from './authentication.service'
import { CanActivate } from '@angular/router'

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthenticationService) {}

  canActivate() {
    return this.authService.isAuthenticated()
  }
}

当然,由于防护罩是可观察的,因此也必须提供防护罩。

src/app/app.module.ts

@NgModule({
  providers: [AuthGuard, AuthenticationService],
})
export class AppModule {}

最后,我们必须告诉它要保护的路由:

@NgModule({
  imports: [
    RouterModule.forRoot([
    {
        path: '',
        component: SomeComponent,
        canActivate: [AuthGuard]
    ])
  ],
  providers: [
    AuthGuard,
    AuthenticationService
  ]
})
export class AppModule {}

问题

那为什么警卫有问题呢?事实是,事实并非如此!

但是,许多人似乎对该主题感到困惑。如果人们因他们而对安全有错误的认识,它们将成为一个问题。事实是,您在客户端上所做的任何事情都是“安全的”。因为您将完整的源代码交付给了潜在的有害用户,所以可以以任何方式更改应用程序。这意味着,通过注释一些行可以很容易地绕开我们的警卫。

不,特别是对于AOT编译,这并非易事,但要有足够的犯罪能量,绝对可以在几个小时内完成。

这样,无需太多工作即可访问仅由路由保护客户端保护的数据。您绝对不希望那样!

解决方案

因此,如果您需要保护任何敏感数据,则还需要一个真正安全的,基于服务器的解决方案。例如,带有签名的JavaScript Web令牌。

6.仅声明一次组件

为了使组件能够按角度工作,必须在模块中声明它们。因此,只要我们只有一个模块(AppModule)并在其中注册组件,就没有问题。

但是,当我们开始将应用程序捆绑到模块中时,顺便说一句,您肯定应该遇到一个普遍的问题。

一个组件只能在一个模块中声明!

这几乎就是它的全部。但是,如果我们想在多个模块中使用我们的组件,该怎么办?

解决方案很简单。只需将您的组件包装到模块中即可。也许每个组件的模块太多了,那么为什么不创建一个组件模块呢?然后可以将该模块导入到其他模块中,然后可以在其中使用组件。这样做时,请确保您不仅要在该components模块中声明您的组件,还要导出它们。否则,只能从模块本身内部访问它们。

@NgModule({
  imports: [CommonModule],
  declarations: [LoginComponent, RegisterComponent, HelpComponent],
  exports: [LoginComponent, RegisterComponent, HelpComponent],
})
export class AuthenticationModule {}

7.通过使用* ngIf而不是[hidden]属性来加速您的应用程序

另一个常见的错误就是混淆* ngIf和[hidden] 。在其中选择正确的一项可以在性能上有很大的不同。因此,让我们仔细看看这两种技术。

[hidden]属性

hidden属性切换元素的可见性。就像我们期望的那样。正确的?这意味着,如果我们将[hidden]设置为true,则css属性“ display”将设置为“ none”。之后,该元素不再可见,但仍存在于DOM上。

<div [hidden]="isHidden"></div>

使用hidden属性的一个问题是,切换的CSS属性很容易被其他CSS属性意外覆盖。例如,如果您在样式表中将元素“ display”属性定义为“ block”,它将始终覆盖display:none属性。这将导致您的元素始终可见。

另一个尽管很理论的问题是,所有这些元素都保留在DOM上,尽管它们是不可见的。如果我们谈论数百或数千个元素,则这些元素会大大降低浏览器的速度。那么,如果我们不需要它们,为什么不删除它们呢?

Angular * ngIf指令

ngIf指令的主要区别就是这一点。它不仅隐藏了标记的元素,还从DOM中完全删除了它们。除了可能的性能改进外,此解决方案对我来说也看起来更干净。但这是我的看法。这似乎是对我隐藏事物的标准方式。因此,我几乎只使用ngIf。

<div *ngIf="!isHidden"></div>

当涉及到* ngIf指令的缺点时,有时很难调试这些部分,因为已删除的元素无法再使用浏览器DOM Explorer进行检查。

8.通过包装服务避免可维护性问题

您可能已经注意到,我们从严重错误过渡到了有关如何改善应用程序的建议。最后一点是正确的:使您的应用程序更快,更小,更好地维护。

作为一般建议,将核心业务逻辑提取到服务中始终是一种好的做法。这样一来,维护变得更加容易,因为它可以在短短几秒钟之内被换出并替换为新的实现。测试也是如此。通常,您需要获取外部数据的服务,才能在测试环境中伪造结果。如果您在服务中获取数据,那很容易。如果不是这样,那么祝您好运,更改所有需要更改的行。

当使用有角度的HttpClient时,该建议当然是正确的。应始终将其包装在集中式服务中。这样,它不仅可测试,而且易于更改。想象一下,您的后端要求在最近更新之后,每个请求都将传递一个新的标头。如果没有集中式服务,您现在必须查找整个应用程序中受影响的所有行。不用说,这远非最佳。

相反,您应该始终将HTTP请求包装到服务中。在最坏的情况下,这仍然不会伤害您。在最佳情况下,它可以为您(和您的团队)节省最简单的任务的时间。

9.通过在生产中使用AOT获得性能和缩小尺寸

通过启动角度cli应用程序

ng serve
ng build

导致您的应用程序以常规模式生成。这意味着您的应用程序将按原样提供给浏览器。然后,浏览器必须执行有角度的编译器,以将您的组件和模板转换为可执行的JavaScript代码。该过程不仅需要花费大量时间。它还要求整个角度编译器随应用程序一起提供。在最新版本的angular中,编译器的大小约为1 MB(压缩后为167kb)。太好了!

您可以使用称为webpack-bundle-analyzer的工具来分析角度束本身。您需要做的就是使用stats-json参数创建捆绑包。

ng build --stats-json

然后,像这样启动bundle分析器:

webpack-bundle-analyzer dist / stats.json

该工具将自动打开浏览器窗口,为您显示与上述类似的结果。

解决方案

解决方案是使用所谓的AOT(Ahead of Time)编译。使用AOT模式时,Angular应用程序将在构建时进行编译。这样,浏览器就不必执行此工作。相反,我们一劳永逸。这样可以使您的应用程序启动更快。

更重要的是,每个用户必须下载的包大小会急剧减少,因为不再需要将角度编译器包含在包中。

在angular-cli的旧版本中,必须在生产版本中手动启用AOT。幸运的是,一段时间以来,AOT一直是生产版本的默认版本。您所要做的就是将生产标记添加到构建命令中。这不仅为您提供了AOT编译功能,还为您提供了减少捆绑包大小的其他好处,例如排除的源映射。

较旧的版本

ng build --prod --aot

较新的版本

ng build --prod

这是生产捆绑包的外观。供应商捆绑包的压缩大小现在约为55 KB。从330 KB到55 KB。这就是我所说的进步!您还将注意到,不再包含编译器

10.通过仅导入所需内容,使应用程序大小保持较小

下一点与上一点直接相关。同样,我们将看一下捆绑包的大小。

这次,我要为您提供的建议是要小心导入。

您使用的每个import语句都会增加包的大小。有道理吧?我们将添加更多代码,因此大小会增加。

这里的问题是,某些库非常庞大。当使用错误的import语句时,您可能会在应用程序中得到整个库。这是一个非常普遍的错误,它将整个RxJs库导入您的应用程序。

import 'rxjs'

这个微小的语句几乎使我们的应用程序大小增加了一倍。

补图

这里的要点是,这种额外的大小是完全没有必要的。如果将此捆绑包与以前的捆绑包进行比较,您会注意到,这些捆绑包也包括RxJ。区别在于,先前的捆绑包仅包含实际需要的模块。使用此导入语句,我们仅导入了所有内容。

解决方案

嗯,对此有多种解决方案。最通用的解决方案是评估添加到项目中的每个库。您是否真的需要一个闪亮的按钮,但要额外花费100 KB?该库是否提供子模块,因此您只能导入所需的内容?如果没有,那可能不值得。

如果您的库提供了子模块,请确保仅导入所需的内容。使用bundle-analyzer定期检查生成的bundle。

那么,如何只导入所需的东西呢?让我们看一下RxJs示例。RxJs已将几乎所有内容拆分为自己的模块。这就要求您最终编写大量导入文件,但是它可以帮助您减小应用程序的大小。

例如,您需要导入要使用的每个运算符:

import 'rxjs/add/observable/of'
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/switchMap'

不要使用

import 'rxjs'

不幸的是,并不是所有的库在拆分代码方面都做得很好。他们的做法各不相同。因此,仔细查看您的库对于小型和快速的应用程序至关重要。

11.不要泄漏内存-取消订阅

当处理RxJ的Observables和Subscriptions时,很容易发生,您会泄漏一些内存。那是因为您的组件被破坏了,但是您在可观察对象内部注册的功能却没有被破坏。这样,您不仅会泄漏内存,而且可能还会遇到一些奇怪的行为。

解决方案

为避免这种情况,请确保在组件被销毁后取消订阅。这样做的一个好地方是ngOnDestroy生命周期挂钩。这是一个例子:

@Component({})
export class ExampleComponent implements OnDestroy {
  private subscriptions = []

  constructor() {
    this.subscriptions.push(this.anyObservable.subscribe())
  }

  ngOnDestroy() {
    for (let subscription of this.subscriptions) {
      subscriptions.unsubscribe()
    }
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值