10个你不知道的Angular实用特性
1.应用初始化器 APP_INITIALIZER
有时您希望在应用程序启动后立即运行某些代码,例如加载一些设置以确定应用程序的各个部分的显示方式。假设这个设置是异步加载的,这可能会有问题,因为在异步请求还没有响应回来的时候,应用程序继续引导过程。
Angular为我们提供了一个简单的解决方案。我们可以访问一个APP_INITIALIZER
令牌,我们可以用它来添加作为应用初始化过程的一部分调用的函数。这些函数可以返回一个promise,因此我们也可以将它们用于异步事件。
export function onInit(settingsService: SettingsService) {
return () => settingsService.getSettings();
}
@NgModule({
providers: [
SettingsService,
{ provide: APP_INITIALIZER, useFactory: onInit, deps: [SettingsService], multi: true }
]
})
export class AppModule { }
2.手势识别
Angular预先构建了所有浏览器事件监听器,主要针对PC用户和传统的鼠标和键盘事件,以及一些基本的触摸识别事件。然而,有许多触摸手势可以用于更好的用户体验,例如平移,缩放,滑动和旋转等等。Angular没有标配这些事件监听器,但是它为HammerJS提供了很好的支持,HammerJS是一个专为处理此类事件而设计的库。
<div (tap)="onTap($event)"
(press)="onPress($event)"
(pressup)="onPressUp($event)"
(pinch)="onPinch($event)"
(pinchstart)="onPinchStart($event)"
(pinchmove)="onPinchMove($event)"
(pinchend)="onPinchEnd($event)"
(pinchcancel)="onPinchCancel($event)"
(pinchin)="onPinchIn($event)"
(pinchout)="onPinchOut($event)"
(swipe)="onSwipe($event)"
(swipeleft)="onSwipeLeft($event)"
(swiperight)="onSwipeRight($event)"
(swipeup)="onSwipeUp($event)"
(swipedown)="onSwipeDown($event)"
(rotate)="onRotate($event)"
(rotatestart)="onRotateStart($event)"
(rotatemove)="onRotateMove($event)"
(rotateend)="onRotateEnd($event)"
(rotatecancel)="onRotateCancel($event)">
</div>
3.模板类型检查(以及如何绕开)
在TypeScript中,我们可能没有足够的类型定义,例如,我们可能正在使用没有提供它们的库,并且我们没有可用的时间或资源来创建它们。TypeScript允许我们在这种情况下使用一种特殊类型,any它只是告诉编译器“保持安静,我知道我在做什么”......
作为AOT编译过程的一部分,不仅会对TypeScript代码执行类型检查,还会键入检查模板。这对于检测错误很有用,例如拼写错误或访问不存在的属性。然而,与TypeScript一样,有时您可以“更好地了解”,并且正在访问某些确实存在的属性,即使编译器不知道它也是如此。幸运的是,通过使用$any类型转换功能,我们有一个简单的解决方案,例如:
<p>{{ $any(user).name }}</p>
4.Provider 的作用域
一般在创建服务Service
时,要么将它添加 到NgModule
中的providers
中:
@NgModule({
providers: [ApiService]
})
要么在Angular6+中:
@Injectable({
providedIn: 'root'
})
export class ApiService {}
由于这种方式是单例模式,所有使用它的地方都会创建一个对象,但是都指向同一个引用(在 lazy loaded模块中使用的services 是个例外)。有时它可以在父子组件间方便地通信,这个时候services 中的数据改变不会丢失。但是,有时候如果有不同的组件,需要不同的数据 需要重新初始化service中的共享区时,就有问题了:services 无法区分不同的组件。
5.装饰器: Host, Self, SkipSelf & Optional
Angular 提供了大量的装饰器,我们天天在使用的就有:@NgModule, @Component, @Directive and @Injectable
。Angular有一些额外的装饰器,允许我们为依赖注入添加额外的约束以及解决依赖。
首先,知道Injector
的原理很重要。一个组件Component
要查找一个service
来依赖注入,会先检查自己是不是已经注册了Injectables
。再查父组件是否注册了。直到找到了,否则会报错:there was no provider found。
@Self
用来限制寻找Injectable对象时,向上寻找的Injector 树的层级。它明确了查当前说好的Component,再多一点都不查~
@Host
跟@Self
很像,也是限制向上查找的层级。它通过一些特殊情况来确认这个Component本身的作用域:
特殊情况一:如果directive
正在请求一个injectable
时,就要看使用这个directive
的Component
了;
特殊情况二:如果使用的是 <ng-content>
来插入的这个Component
, 那么这个<ng-content>
元素也会被检查.
@SkipSelf
正好和@Self
相反 , 不检查自己,而检查父节点的injectable
对象.
@Optional
在我们找不到Injectable
对象时, @Optional
很有用. 如果找injectable对象没找到时, 保护不抛异常.你只需要保证没有injectable
对象时,你的代码依然能正常运行.
6.Http Interceptors
如果有鉴权的时候,使用它会非常有用.比如要给每个请求设置一个header
时.
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.get('token');
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(request);
}
}
然后在我们的NgModule
:
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
]
})
export class AppModule {}
7.Route Guards
当需要用户权限和安全性时,路由器附带了一个名为Guards的强大功能,它允许我们检查用户是否被允许访问应用程序中的特定页面并阻止他们导航到它必要。
但是,不要想着在前端做安全,因为它可以很轻松地就被伪造,最好还是要做服务端的鉴权 。
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.authService.isLoggedIn();
}
}
// 这样去使用
const routes: Routes = [
{
path: 'profile',
component: ProfileComponent,
canActivate: [AuthGuard]
}
];
8.RxJS订阅(Subscriptions)
当我们有非常多的可观察对象(observables)时,我们经常这样写:
export class AppComponent implements OnDestroy {
private _mouseDownSubscription: Subscription;
private _mouseUpSubscription: Subscription;
private _mouseMoveSubscription: Subscription;
constructor(@Inject(DOCUMENT) document: any) {
this._mouseDownSubscription = fromEvent(document, 'mousedown')
.subscribe(() => { /*...*/ });
this._mouseUpSubscription = fromEvent(document, 'mouseup')
.subscribe(() => { /*...*/ });
this._mouseMoveSubscription = fromEvent(document, 'mousemove')
.subscribe(() => { /*...*/ });
}
ngOnDestroy(): void {
this._mouseDownSubscription.unsubscribe();
this._mouseUpSubscription.unsubscribe();
this._mouseMoveSubscription.unsubscribe();
}
}
这样写,就开始变得混乱和不可维护,建议用这些更好的方法:
(1)使用Subscription
的add
方法
export class AppComponent implements OnDestroy {
private _subscription = new Subscription();
constructor(@Inject(DOCUMENT) document: any) {
this._subscription.add(fromEvent(document, 'mousedown')
.subscribe(() => { /*...*/ }));
this._subscription.add(fromEvent(document, 'mouseup')
.subscribe(() => { /*...*/ }));
this._subscription.add(fromEvent(document, 'mousemove')
.subscribe(() => { /*...*/ }));
}
ngOnDestroy(): void {
this._subscription.unsubscribe();
}
}
(2)使用
个人非常推荐这种方式,效果也很好。在RxJS中,我们有许多操作可以自动取消订阅。例如,在某些情况下,我们实际上只关心第一个值,或者直到满足某个条件,在这些情况下拥有将自动取消订阅的first和takeWhile运算符。另一个有用的是takeUntil,当另一个emits时 我们可以使用它来取消订阅所有的observable,可以像这样使用:
export class AppComponent implements OnDestroy {
private _onDestroy = new Subject<void>();
constructor(@Inject(DOCUMENT) document: any) {
fromEvent(document, 'mousedown').pipe(takeUntil(this._onDestroy))
.subscribe(() => { /*...*/ }));
fromEvent(document, 'mouseup').pipe(takeUntil(this._onDestroy))
.subscribe(() => { /*...*/ }));
fromEvent(document, 'mousemove').pipe(takeUntil(this._onDestroy))
.subscribe(() => { /*...*/ }));
}
ngOnDestroy(): void {
this._onDestroy.next();
this._onDestroy.complete();
}
}
9.预加载 懒模块
我们可以在路由中方便地设置懒模块,从而减少总的打包JS大小,进而提升初次打开页面的速度。懒加载的模块是在访问某路由时,才加载相应的JS。而这里要说的是提前加载它们:
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
另外,这个加载策略也可以很方便地自定义,比如只预加载常用的模块,而不预加载所有的。
https://angular.io/api/router/PreloadAllModules
10.不可变的只读类型
虽然这不是直接Angular特定的,但在使用Angular功能(如OnPush更改检测和NGRX等不可变状态管理)时可以提供很大帮助。
让我们从检查OnPush变化检测开始。这是Angular通过完全避免组件来优化变更检测的一种方法,除非它知道@Input已经改变了。
这是一个很好的优化,可以显着提高应用程序的性能,但它确实存在一些限制,最明显的是,如何检查输入的变化。
举个例子,Angular使用引用相等性检查来确定输入是否已更改,如下所示:
if (oldValue !== newValue) {
// value has changes
}
当输入值是基本类型(如数字,布尔值或字符串)时,没问题。但是在使用数组或对象时我们可能会遇到一些麻烦。JavaScript不执行任何类型的深度检查,作为等式检查的一部分,它只是检查引用,例子:
let obj1 = { name: 'Michael Scott' };
let obj2 = obj1;
// Change the name property in obj2
obj2.name = 'Dwight Schrute';
// Perform an equality check
if (obj1 === obj2) {
// this will be true!!
}
所以即使我们更改了obj2
引用上的属性仍然是相同的,所以就JavaScript而言,这两个对象仍然是相等的!
对于数组也是如此,例如,将新值推送到数组也将在相等测试中返回true,即使数组中有新项也是如此!
在这种情况下,更新对象属性或将项添加到数组不会在启用了OnPush更改检测的组件中触发更改检测。
这就是不可变对象和数组的概念所在。不是更改对象中的属性或将项目推送到数组,而是根据原始对象创建新对象或数组,但需要进行更改。通过进行此更改,我们更改了对象引用,允许我们的相等性检查以了解发生了更改。
使用新的JavaScript Spread运算符,我们可以使这很简单,例如:
let obj1 = { name: 'Michael Scott', company: 'Dunder Mifflin' };
let obj2 = { ...obj1, name: 'Dwight Schrute' };
if (obj1 === obj2) {
// Objects are now not equal
}
我们还可以使用spread运算符将项添加到数组并创建新引用:
let employees = ['Pam Beasley'];
employees = [...employees, 'Jim Halpert'];
有时候很难确保我们总是创建一个新的对象或数组,特别是在大量的数组函数可用的情况下,其中一些可以执行,有些则不可用。看看这里疯狂:https://doesitmutate.xyz/。
这就是TypeScript真正有用的地方。我们可以告诉TypeScript在对象或数组上强制实现不变性,这意味着每当我们对其进行更改而不更改其引用时,我们都会遇到构建错误。这样做非常简单!只需开始使用Readonly
和ReadonlyArray
类型!
// Before:
const employee: Employee = { name: 'Michael Scott' };
// This will compile fine
employee.name = 'Oscar Martinez';
// After:
const employee: Readonly<Employee> = { name: 'Michael Scott' };
// This will produce an error
employee.name = 'Kevin Malone';
我们还可以为数组,映射和集使用只读类型:
const employees: ReadonlyArray<Employee>;
const employees: ReadonlySet<Employee>;
const employees: ReadonlyMap<string, Employee>;
您不需要包含像Immutable.js这样的库来使您的代码不可变。让TypeScript在构建时为您完成!