Angular 应用无法正常运行的9个常见错误
(最后一次更新: ) 阅读需要 5 分钟
当 Angular 应用无法正常工作时,会在浏览器控制台上报一些错。当初学者遇到这些报错时,可能会使开发过程陷入困境。
本文中列出了11个常见的错误,几乎每个 Angular 应用开发者都会遇到。文中会解释为什么会出现这个错误,并通过至少一种解决方案引导解决。
1.导入所需的角度模块
可能在开发中遇到最常见的错误是没有导入所需的模块。
1
Can't bind to 'ngModel' since it isn't a known property of 'input'
这个错误表明没有将 Angular Forms Module 导入到当前模块中。
1
Unhandled Promise rejection: No provider for HttpClient!
这个错误表明没有将 Angular HttpClient Module 导入到(根)模块中。
解决方案
要解决类似上面的错误,需要将缺少的模块导入的当前或者根模块中。根模块就是应用目录中的 AppModule
。
src/app/app.module.ts
1
2
3
4
5
6
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, FormsModule, HttpClientModule],
bootstrap: [AppComponent],
})
export class AppModule {}
该建议不仅适用于 Angular 框架模块(@angular/*
),也适用于任何 Angular 模块,包括第三方模块。
以下是一些需求导入的常见模块:
1
2
3
4
5
BrowserModule
FormsModule
HttpClientModule
RouterModule
BrowserAnimationsModule
第三方库最好将模块拆分为尽可能多的模块,以使应用的大小保持较小。在 Angular Material 中必须为所使用的每个组件导入一个模块。
例如:
1
2
3
4
5
6
MatMenuModule
MatSidenavModule
MatCheckboxModule
MatDatepickerModule
MatInputModule
...
2.不要在 DOM 引用存在之前使用 @ViewChild
@ViewChild
装饰器使引用组件的特点子元素(html 元素或者组件)变得非常容易。只需要在模板内部的 html 元素或子组件上添加引用标识符:# 加名称
1
2
3
<div #myDiv></div>
<demo-component #demoComponent></demo-component>
现在,可以从父组件中引用该元素。如果是一个组件,则可以调用其公共方法和访问属性。如果是 HTML 元素,则可以更改其样式,属性或子元素。
如果使用 @ViewChild
装饰器装饰该属性,Angular 会自动将引用分配给组件的属性。确保将参考名称传递给装饰器,例如:@ViewChild('myDiv', {static: true})
。
1
2
3
4
5
6
import { ViewChild } from '@angular/core';
@Component({})
export class ExampleComponent {
@ViewChild('myDiv', {static: true}) divReference;
}
问题
只有在元素实际存在的情况下,才能使用对元素的引用!
所引用的元素无法实际存在的原因有很多。最常见的原因是浏览器尚未创建完成,也就是尚未将其添加到 DOM 中。
如果你在元素添加到 DOM 之前使用元素的引用,就会在导致应用报错无法执行。在 JavaScript 中也有可能会遇到这个问题,因为这个问题并不是特定于 Angular 的。
一个在 DOM 不存在时访问元素的引用的例子,这会导致应用报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ViewChild, OnInit } from '@angular/core';
@Component({})
export class ExampleComponent implements OnInit {
@ViewChild('myDiv', {static: true}) divReference;
constructor() {
let ex = this.divReference.nativeElement; // divReference is undefined
}
ngOnInit() {
let ex = this.divReference.nativeElement; // divReference is undefined
}
}
解决方案
在原生 JavaScript 中有 DOMContentLoaded 事件,JQuery 中有 $(document).ready( ) 回调,Angular 中也有类似的机制,叫做 ngAfterViewInit 生命周期钩子。当所有组件视图和子视图都初始化时会触发这个钩子,可以在这个钩子回调里安全的访问 viewChild
引用。
1
2
3
4
5
6
7
8
9
10
import { ViewChild, AfterViewInit } from '@angular/core';
@Component({})
export class ExampleComponent implements AfterViewInit {
@ViewChild('myDiv', {static: true}) divReference;
ngAfterViewInit() {
let ex = this.divReference.nativeElement; // divReference is NOT undefined
}
}
在正常情况下上面代码会正常运行,但还有一个问题。就像上面说的,只能访问实际存在元素的引用。当元素或者组件上有 *ngIf
指令,并且值为 false
时元素会被从 DOM 中删除。为了防止在这种情况下应用报错,在使用引用之前应先判断引用是否为 null
。
1
2
3
4
5
6
7
8
9
10
11
12
import { ViewChild, AfterViewInit } from '@angular/core';
@Component({})
export class ExampleComponent implements AfterViewInit {
@ViewChild('myDiv', {static: true}) divReference;
ngAfterViewInit() {
if (this.divReference) {
let ex = this.divReference.nativeElement; // divReference is NOT undefined
}
}
}
不要直接操作 DOM
直接用 Angular 操作 DOM 被认为是不好的做法。这也可能导致 Angular 应用无法在浏览器以外的其他平台中运行。比如在 Angular Universal 项目中,Angular 应用可以在服务端渲染。
在服务端渲染中不能正常运行的示例:
1
2
3
4
5
6
7
8
9
10
11
import { ViewChild, AfterViewInit } from '@angular/core';
@Component({})
export class ExampleComponent implements AfterViewInit {
@ViewChild('myDiv', {static: true}) divReference;
ngAfterViewInit() {
let ex = this.divReference.nativeElement;
ex.style.color = 'red'; // does not work on the server
}
}
解决方案
既然不能操作 DOM,那就间接操作。为此,Angular 提供了 (Renderer2)[https://angular.io/api/core/Renderer2] 类形式的渲染 API。
通过使用渲染器,可以确保代码在服务端渲染和浏览器环境中都能正常运行。
使用 Renderer2
的实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { ViewChild, Renderer2, AfterViewInit } from '@angular/core';
@Component({})
export class ExampleComponent implements AfterViewInit {
@ViewChild('myDiv', {static: true}) divReference;
constructor(private renderer: Renderer2) {}
ngAfterViewInit() {
if (this.divReference)
this.renderer.setStyle(this.divReference.nativeElement, 'color', 'red');
}
}
Renderer2
有很多不同的方法更改元素,对于熟悉 JavaScript DOM API 的人来说很容易就能猜到每个 API 的作用。
4.避免重复的提供者互相覆盖
Angular 使用了一种称为依赖注入的概念。借助依赖注入可以在构造函数中获取 service
实例。
为此,必须在 @Component
或 @NgModule
装饰器的 providers
数组中添加 service
或其他 injectable
。最常见的做法是在模块级别提供。
Angular 使用了分层依赖注入系统,在根模块(AppModule)中提供的 service
或 injectable
可以用于根模块的所有组件中。并且由于根模块应包含所有组件和模块,因此,在根模块中提供的 service
在整个应用中都可用。
如果在子模块中提供 service
,则该 service
仅可用于该子模块。这也意味着,如果在两个子模块中都提供 service
,则子模块的组件构造函数中获取的 service
实例与其他组件不同。如果假设 service
是应用中的唯一实例(单例),则可能导致各种错误。
解决方案
在 AppModule 中只提供一次 service
。除非明确的知道自己在做什么,否则应该遵守这个约定,尤其是刚开始接触 Angular 时。
推荐使用以下这种方式注册 service
:
1
2
3
@Injectable({
providedIn: 'root'
})
providedIn: 'root'
告诉 Angular 在根注入器中注册service
,这也是使用 Angular CLI 生成 service
时默认的方式。
这种方式注册,不需要在 @NgModule
装饰器中写 providers
,而且在代码编译打包时,可以执行摇树优化,会移除所有没在应用中使用过的服务。
5.Angular Guard 是不安全的
Angular Guard 是人为地对指定路由限制访问的最佳方式。例如:要在用户访问页面之前检查是否已经登录。以下是一个简单示例:
auth.guard.ts
1
2
3
4
5
6
7
8
9
10
11
12
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()
}
}
app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@NgModule({
imports: [
RouterModule.forRoot([
{
path: '',
component: SomeComponent,
canActivate: [AuthGuard]
])
],
providers: [
AuthGuard,
AuthenticationService
]
})
export class AppModule {}
问题
因为应用程序在客户端可以以任何方式被更改,这意味着,通过注释某些代码可以很轻易的绕开 Guard。这样就可以自由访问仅用路由限制的客户端数据。
解决方案
因此,如果需要保护任何敏感数据,则还需要一个真正安全的,基于服务端的解决方案。例如,带有签名的 JSON Web Token。
6.只声明一次组件
使用组件的前提是必须在对应的模块中声明它们。开发中普遍遇到的一个问题:一个组件只能在一个模块中声明!
很明显在开发中组件要在多个模块中使用,但又不能在多个模块中声明,这种场景中应该怎么处理?。
解决方案
只需将组件封装到模块中即可,使用组件时在当前模块中导入组件所在的模块。需要注意的是不仅要在组件模块中声明组件,还需要导出它们。
1
2
3
4
5
6
@NgModule({
imports: [CommonModule],
declarations: [LoginComponent, RegisterComponent, HelpComponent],
exports: [LoginComponent, RegisterComponent, HelpComponent],
})
export class AuthenticationModule {}
7.用 [hidden]
代替 *ngIf
以提高应用渲染性能
另一个常见的错误就是混淆了 [hidden]
与 *ngIf
。
[hidden]
属性
hidden
属性切换元素的可见性,有时候这就是在应用中想要的效果。当 [hidden]
的值设置为 true,则 css 属性 display
的值会变为 none
,但是这个元素仍然在 DOM 中存在。
使用 hidden
属性有两个问题需要注意。
第一,其他 css 属性会意外的覆盖用 hidden
属性切换的 css 属性。例如,在组件样式中将元素 display
的值设置为 block
,这将覆盖 display: none
,会导致元素始终可见。
第二,如果有大量的(成百上千个)元素需要控制可见性,当这些元素不可见时依然会保留在 DOM 中,那么这些元素会降低浏览器性能。如果不需要这些元素,就把它们删了。
*ngIf
指令
*ngIf
指令的主要区别就是不仅隐藏了元素,还会从 DOM 中完全删除元素。
当使用 *ngIf
指令隐藏元素后,这些元素将很难调试,因为已经从 DOM 中删除的元素无法再使用浏览器的 DOM Explorer 检查。
8.通过封装 Service 避免可维护性问题
将核心业务逻辑抽出来放到 service 中是一种好习惯,这样以后维护会变得更加容易,因为可以在短时间内就可以将其替换为新的实现。
在需要从外部获取数据的时候,这很有用。当服务端在还没有完全提供数据的时候就可以在 service 中 mock 数据,以保证组件能正常运行。当服务端能提供完整数据的时候再把 mock 数据替换为 HTTP 请求,这样可以节省很多时间在修改代码上。
9.记得取消订阅
在处理 RxJ 的 Observables 和 Subscriptions 时,使用不当很容易会泄漏一些内存。组件被销毁了,但是在可观察对象内部注册的功能却未被销毁。这样,不仅会泄漏内存,可能还会遇到一些奇怪的行为。
解决方案
为防止发生这种情况,确保在组件销毁后取消订阅。
推荐下面这种方法:
1
2
3
4
5
6
7
8
9
10
11
12
...
private unsubscribe$ = new Subject<void>();
ngOnInit() {
something.pipe(takeUntil(this.unsubscribe$)).subscribe();
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
还有一种方式是使用第三方库实现,比如:ngx-take-until-destroy。