一些角度提示

自从更新的Angular发布以来已经过去了足够的时间。 目前,许多项目已经完成。 从“入门”开始,许多开发人员已经转向对该框架及其功能的有意义的使用,并学会了如何避免陷阱。 每个开发人员和/或团队都已经形成了自己的样式指南和最佳实践,或者使用了其他形式。 但是同时,您通常必须处理许多Angular代码,这些代码没有使用此框架的许多功能和/或以AngularJS的样式编写。


本文介绍了使用Angular框架的一些功能,根据作者的保守意见,这些功能没有在手册中充分介绍或未被开发人员使用。 本文讨论了“拦截器” HTTP请求的使用,以及如何使用Route Guards限制对用户的访问。 给出了使用RxJS和管理应用程序状态的一些建议。 还提出了一些有关项目代码设计的建议,这可能会使项目代码更简洁,更易理解。 作者希望本文不仅对刚开始熟悉Angular的开发人员有用,对有经验的开发人员也有用。


使用HTTP


任何客户端Web应用程序的构建都是围绕对服务器的HTTP请求完成的。 本部分讨论了用于HTTP请求的Angular框架的某些功能。


使用拦截器


在某些情况下,可能有必要在请求到达服务器之前对其进行修改。 或者您需要更改每个答案。 从Angular 4.3开始,已发布了新的HttpClient。 它增加了使用拦截器拦截请求的功能(是的,它们最终仅在版本4.3中才返回!,这是未迁移到Angular的AngularJs最令人期待的缺失功能之一)。 这是介于http-api和实际请求之间的一种中间件。


一种常见的用例可能是身份验证。 为了获得服务器的响应,您通常需要向请求中添加某种身份验证机制。 使用拦截器完成此任务非常简单:


import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from @angular/common/http"; @Injectable() export class JWTInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { req = req.clone({ setHeaders: { authorization: localStorage.getItem("token") } }); return next.handle(req); } } 

因为一个应用程序可以有多个拦截器,所以它们是按链组织的。 第一个元素由Angular框架本身调用。 随后,我们负责将请求传输到下一个拦截器。 为此,我们一完成就调用链中下一个元素的handle方法。 我们连接拦截器:


 import { BrowserModule } from "@angular/platform-browser"; import { NgModule } from "@angular/core"; import { AppComponent } from "./app.component"; import { HttpClientModule } from "@angular/common/http"; import { HTTP_INTERCEPTORS } from "@angular/common/http"; @NgModule({ declarations: [AppComponent], imports: [BrowserModule, HttpClientModule], providers: [ { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true } ], bootstrap: [AppComponent] }) export class AppModule {} 

如您所见,拦截器的连接和实现非常简单。


进度追踪


HttpClient的功能之一是能够跟踪请求的进度。 例如,如果您需要下载一个大文件,则可能要向用户报告下载进度。 要获得进度,必须将HttpRequest对象的reportProgress属性设置为true 。 实现此方法的服务示例:


 import { Observable } from "rxjs/Observable"; import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { HttpRequest } from "@angular/common/http"; import { Subject } from "rxjs/Subject"; import { HttpEventType } from "@angular/common/http"; import { HttpResponse } from "@angular/common/http"; @Injectable() export class FileUploadService { constructor(private http: HttpClient) {} public post(url: string, file: File): Observable<number> { var subject = new Subject<number>(); const req = new HttpRequest("POST", url, file, { reportProgress: true }); this.httpClient.request(req).subscribe(event => { if (event.type === HttpEventType.UploadProgress) { const percent = Math.round((100 * event.loaded) / event.total); subject.next(percent); } else if (event instanceof HttpResponse) { subject.complete(); } }); return subject.asObservable(); } } 

post方法返回一个Observable ,它表示下载进度。 现在所需要做的只是显示组件中的加载进度。


路由选择 使用Route Guard


路由允许您将应用程序请求映射到应用程序内的特定资源。 通常,有必要解决根据某些条件限制某些组件所在的路径的可见性的问题。 在这些情况下,Angular具有过渡限制机制。 例如,有一项服务将实现路由保护。 假设在应用程序中,用户身份验证是使用JWT实现的。 检查用户是否被授权的服务的简化版本可以表示为:


 @Injectable() export class AuthService { constructor(public jwtHelper: JwtHelperService) {} public isAuthenticated(): boolean { const token = localStorage.getItem("token"); //        return !this.jwtHelper.isTokenExpired(token); } } 

要实现路由保护,必须实现CanActivate接口,该接口由单个canActivate函数组成。


 @Injectable() export class AuthGuardService implements CanActivate { constructor(public auth: AuthService, public router: Router) {} canActivate(): boolean { if (!this.auth.isAuthenticated()) { this.router.navigate(["login"]); return false; } return true; } } 

AuthGuardService实现使用上述的AuthGuardService来验证用户授权。 canActivate方法返回一个布尔值,该值可以在路由激活的条件下使用。


现在,我们可以将创建的Route Guard应用于任何路由或路径。 为此,在声明Routes我们在canActivate部分中指定服务,该服务继承了CanActivate接口:


 export const ROUTES: Routes = [ { path: "", component: HomeComponent }, { path: "profile", component: UserComponent, canActivate: [AuthGuardService] }, { path: "**", redirectTo: "" } ]; 

在这种情况下, /profile路由具有可选的配置值canActivateAuthGuard描述AuthGuard作为参数传递给此canActivate属性。 接下来,每次有人尝试访问/profile路径时,都会调用canActivate方法。 如果用户被授权,则他将获得对/profile路径的访问权限,否则将被重定向到/login路径。


您应该意识到canActivate仍然允许您在此路径上激活组件,但不允许您切换到该组件。 如果您需要保护组件的激活和加载,那么在这种情况下,我们可以使用canLoadCanLoad实现可以类推。


烹饪RxJS


Angular建立在RxJS之上。 RxJS是一个使用可观察序列处理异步和基于事件的数据流的库。 RxJS是ReactiveX API的JavaScript实现。 在大多数情况下,使用此库时发生的错误与其实现基础的肤浅知识有关。


使用异步而不是注册事件


大量刚开始使用Angular框架的开发人员使用Observablesubscribe功能来接收和保存组件中的数据:


 @Component({ selector: "my-component", template: ` <span>{{localData.name}} : {{localData.value}}</span>` }) export class MyComponent { localData; constructor(http: HttpClient) { http.get("api/data").subscribe(data => { this.localData = data; }); } } 

相反,我们可以使用异步管道通过模板进行订阅:


 @Component({ selector: "my-component", template: ` <p>{{data.name | async}} : {{data.value | async}}</p>` }) export class MyComponent { data; constructor(http: HttpClient) { this.data = http.get("api/data"); } } 

通过订阅模板,我们避免了内存泄漏,因为当组件Observable时,Angular会自动从Observable退订。 在这种情况下,对于HTTP请求,使用异步管道实际上没有任何好处,除了一个好处-如果不再需要数据,异步将取消请求,并且不会完成请求的处理。


手动订阅时未使用Observables许多功能。 Observables行为可以通过重复(例如,在http请求中重试),基于计时器的更新或预缓存来扩展。


$表示可观察值


下一段与应用程序源代码的设计有关,并紧跟于上一段。 为了将Observable与简单变量区分开,通常您会听到在变量或字段名称中使用“ $ ”符号的建议。 这个简单的技巧将消除使用异步时变量的混乱。


 import { Component } from "@angular/core"; import { Observable } from "rxjs/Rx"; import { UserClient } from "../services/user.client"; import { User } from "../services/user"; @Component({ selector: "user-list", template: ` <ul class="user_list" *ngIf="(users$ | async).length"> <li class="user" *ngFor="let user of users$ | async"> {{ user.name }} - {{ user.birth_date }} </li> </ul>` }) export class UserList { public users$: Observable<User[]>; constructor(public userClient: UserClient) {} public ngOnInit() { this.users$ = this.client.getUsers(); } } 

何时退订(退订)


开发人员在短暂了解Angular时最常遇到的问题是何时仍需要退订,何时不退订。 要回答此问题,您首先需要确定当前正在使用哪种Observable 。 在Angular中,有两种类型的Observable有限和无限,某些产生有限,另一些分别产生无限数量的值。


Http Observable是紧凑的,并且DOM事件的侦听器是无限的Observable


如果订阅无限的Observable的值Observable手动完成的(不使用异步管道),则必须毫无疑问地做出答复。 如果我们手动订阅有限的Observable,则不必取消订阅,RxJS会处理这一问题。 在紧凑型Observables情况下Observables我们可以取消订阅Observable是否具有比所需时间更长的执行时间,例如,多个HTTP请求。


紧凑型Observables的示例:


 export class SomeComponent { constructor(private http: HttpClient) { } ngOnInit() { Observable.timer(1000).subscribe(...); this.http.get("http://api.com").subscribe(...); } } 

无限可观察物的例子


 export class SomeComponent { constructor(private element : ElementRef) { } interval: Subscription; click: Subscription; ngOnInit() { this.interval = Observable.interval(1000).subscribe(...); this.click = Observable.fromEvent(this.element.nativeElement, "click").subscribe(...); } ngOnDestroy() { this.interval.unsubscribe(); this.click.unsubscribe(); } } 

以下是您需要取消订阅的详细信息


  1. 有必要取消订阅该表格以及您所订阅的各个控件:

 export class SomeComponent { ngOnInit() { this.form = new FormGroup({...}); this.valueChangesSubs = this.form.valueChanges.subscribe(...); this.statusChangesSubs = this.form.statusChanges.subscribe(...); } ngOnDestroy() { this.valueChangesSubs.unsubscribe(); this.statusChangesSubs.unsubscribe(); } } 

  1. 路由器 根据文档,Angular应该退订自己, 但是这不会发生 。 因此,为了避免进一步的问题,我们自己编写:

 export class SomeComponent { constructor(private route: ActivatedRoute, private router: Router) { } ngOnInit() { this.route.params.subscribe(..); this.route.queryParams.subscribe(...); this.route.fragment.subscribe(...); this.route.data.subscribe(...); this.route.url.subscribe(..); this.router.events.subscribe(...); } ngOnDestroy() { //        observables } } 

  1. 无休止的序列。 示例是使用interva()或事件侦听器(fromEvent())创建的序列:

 export class SomeComponent { constructor(private element : ElementRef) { } interval: Subscription; click: Subscription; ngOnInit() { this.intervalSubs = Observable.interval(1000).subscribe(...); this.clickSubs = Observable.fromEvent(this.element.nativeElement, "click").subscribe(...); } ngOnDestroy() { this.intervalSubs.unsubscribe(); this.clickSubs.unsubscribe(); } } 

takeUntil和takeWhile


为了简化RxJS中的无限Observables的工作,有两个方便的函数takeUntiltakeWhile 。 它们执行相同的操作-在某些条件结束时取消订阅Observable ,差异仅在于接受的值。 takeWhile接受一个boolean ,而takeUntil一个Subject
takeWhile例子:


 export class SomeComponent implements OnDestroy, OnInit { public user: User; private alive: boolean = true; public ngOnInit() { this.userService .authenticate(email, password) .takeWhile(() => this.alive) .subscribe(user => { this.user = user; }); } public ngOnDestroy() { this.alive = false; } } 

在这种情况下,当更改alive标志时, Observable将退订。 在此示例中,取消订阅组件被销毁的时间。
takeUntil示例:


 export class SomeComponent implements OnDestroy, OnInit { public user: User; private unsubscribe: Subject<void> = new Subject(void); public ngOnInit() { this.userService.authenticate(email, password) .takeUntil(this.unsubscribe) .subscribe(user => { this.user = user; }); } public ngOnDestroy() { this.unsubscribe.next(); this.unsubscribe.complete(); } } 

在这种情况下,要取消订阅Observable我们报告subject接受下一个值并完成该值。


使用这些功能将避免泄漏,并通过取消订阅数据来简化工作。 使用哪个功能? 该问题的答案应以个人喜好和当前要求为指导。


Angular应用程序中的状态管理,@ ngrx / store


通常,在开发复杂的应用程序时,我们面临着存储状态并响应其更改的需求。 在ReactJs框架上开发了许多针对应用程序的库,这些库可让您控制应用程序的状态并响应其更改-Flux,Redux,Redux-saga等。 对于Angular应用程序,有一个基于Redux启发的基于RxJS的状态容器-@ ngrx / store。 当应用程序进一步扩展时,对应用程序状态的正确管理将使开发人员免于许多问题的困扰。


为什么要Redux
Redux将自身定位为JavaScript应用程序的可预测状态容器。 Redux受到Flux和Elm的启发。


Redux建议将应用程序视为可以通过一系列操作进行修改的初始状态,这可能是构建复杂Web应用程序的好方法。


Redux与任何特定框架都没有关联,尽管它是为React开发的,但可以与Angular或jQuery一起使用。


Redux的主要假设:


  • 一个用于整个应用程序状态的存储库
  • 只读状态
  • 通过“纯”功能进行更改,这些功能必须满足以下要求:
  • 不得通过网络或数据库进行外部呼叫;
  • 返回仅取决于传递的参数的值;
  • 参数是不可变的,即 功能不应更改它们;
  • 用相同的参数调用纯函数总是返回相同的结果;

状态管理功能的示例:


 // counter.ts import { ActionReducer, Action } from "@ngrx/store"; export const INCREMENT = "INCREMENT"; export const DECREMENT = "DECREMENT"; export const RESET = "RESET"; export function counterReducer(state: number = 0, action: Action) { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } } 

Reducer导入到应用程序的主模块中,并使用StoreModule.provideStore(reducers)函数将其用于Angular注射器:


 // app.module.ts import { NgModule } from "@angular/core"; import { StoreModule } from "@ngrx/store"; import { counterReducer } from "./counter"; @NgModule({ imports: [ BrowserModule, StoreModule.provideStore({ counter: counterReducer }) ] }) export class AppModule { } 

接下来,将Store服务引入必要的组件和服务中。 store.select()函数用于选择“切片”状态:


 // app.component.ts ... interface AppState { counter: number; } @Component({ selector: "my-app", template: ` <button (click)="increment()">Increment</button> <div>Current Count: {{ counter | async }}</div> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset Counter</button>` }) class AppComponent { counter: Observable<number>; constructor(private store: Store<AppState>) { this.counter = store.select("counter"); } increment() { this.store.dispatch({ type: INCREMENT }); } decrement() { this.store.dispatch({ type: DECREMENT }); } reset() { this.store.dispatch({ type: RESET }); } } 

@ ngrx /路由器存储


在某些情况下,将应用程序的状态与应用程序的当前路由关联起来很方便。 对于这些情况,存在@ ngrx / router-store模块。 为了使应用程序使用router-store来保存状态,只需连接routerReducer并在应用程序主模块中添加对RouterStoreModule.connectRoute的调用:


 import { StoreModule } from "@ngrx/store"; import { routerReducer, RouterStoreModule } from "@ngrx/router-store"; @NgModule({ imports: [ BrowserModule, StoreModule.provideStore({ router: routerReducer }), RouterStoreModule.connectRouter() ], bootstrap: [AppComponent] }) export class AppModule { } 

现在将RouterState添加到应用程序的主要状态:


 import { RouterState } from "@ngrx/router-store"; export interface AppState { ... router: RouterState; }; 

此外,在声明存储时,我们可以指示应用程序的初始状态:


 StoreModule.provideStore( { router: routerReducer }, { router: { path: window.location.pathname + window.location.search } } ); 

支持的动作:


 import { go, replace, search, show, back, forward } from "@ngrx/router-store"; //      store.dispatch(go(["/path", { routeParam: 1 }], { query: "string" })); //        store.dispatch(replace(["/path"], { query: "string" })); //        store.dispatch(show(["/path"], { query: "string" })); //       store.dispatch(search({ query: "string" })); //   store.dispatch(back()); //   store.dispatch(forward()); 

UPD:评论建议对于新版本https://github.com/ngrx/platform/blob/master/MIGRATION.md#ngrxrouter-store ,新版本@ngrx中将不提供这些操作。


在开发复杂的应用程序时,使用状态容器将消除许多问题。 但是,使状态管理尽可能简单很重要。 通常,必须处理状态嵌套过多的应用程序,这只会使对应用程序的理解复杂化。


代码组织


摆脱import的笨重表达式


许多开发人员意识到import中的表达式相当麻烦的情况。 在有许多可重用库的大型应用程序中,这一点尤其明显。


 import { SomeService } from "../../../core/subpackage1/subpackage2/some.service"; 

此代码还有什么不好的地方? 如果您需要将我们的组件转移到另一个目录,则import的表达式将无效。


在这种情况下,使用别名将使我们摆脱import笨拙表达式,并使我们的代码更整洁。 为了准备使用别名的项目,您需要在tsconfig.json添加tsconfig.json和path属性:


 / tsconfig.json { "compilerOptions": { ... "baseUrl": "src", "paths": { "@app/*": ["app/*"], "@env/*": ["environments/*"] } } } 

进行了这些更改,即可轻松管理插件:


 import { Component, OnInit } from "@angular/core"; import { Observable } from "rxjs/Observable"; /*    */ import { SomeService } from "@app/core"; import { environment } from "@env/environment"; /*      */ import { LocalService } from "./local.service"; @Component({ /* ... */ }) export class ExampleComponent implements OnInit { constructor( private someService: SomeService, private localService: LocalService ) { } } 

在此示例中, SomeService直接从@app/core导入的,而不是笨拙的表达式(例如@app/core/some-package/some.service )。 这要归功于主index.ts文件中的公共组件的重新导出。 建议为每个需要重新导出所有公共模块的软件包创建一个index.ts文件:


 // index.ts export * from "./core.module"; export * from "./auth/auth.service"; export * from "./user/user.service"; export * from "./some-service/some.service"; 

核心,共享和功能模块


为了更灵活地管理应用程序组件,在文献和各种Internet资源中经常建议使用它来扩展其组件的可见性。 在这种情况下,简化了应用程序组件的管理。 以下是最常用的分隔:核心,共享和功能模块。


核心模块


CoreModule的主要目的是描述将在整个应用程序中具有一个实例的服务(即实现单例模式)。 这些通常包括授权服务或用于获取用户信息的服务。 CoreModule示例:


 import { NgModule, Optional, SkipSelf } from "@angular/core"; import { CommonModule } from "@angular/common"; import { HttpClientModule } from "@angular/common/http"; /*  */ import { SomeSingletonService } from "./some-singleton/some-singleton.service"; @NgModule({ imports: [CommonModule, HttpClientModule], declarations: [], providers: [SomeSingletonService] }) export class CoreModule { /*   CoreModule    NgModule the AppModule */ constructor( @Optional() @SkipSelf() parentModule: CoreModule ) { if (parentModule) { throw new Error("CoreModule is already loaded. Import only in AppModule"); } } } 

共享模块


本模块描述简单的组件。 这些组件不会将依赖关系从其他模块导入或注入到其构造函数中。 他们应该通过组件模板中的属性接收所有数据。 SharedModule不依赖于我们其余的应用程序,也是导入和重新导出Angular Material组件或其他UI库的理想场所。


 import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { FormsModule } from "@angular/forms"; import { MdButtonModule } from "@angular/material"; /*  */ import { SomeCustomComponent } from "./some-custom/some-custom.component"; @NgModule({ imports: [CommonModule, FormsModule, MdButtonModule], declarations: [SomeCustomComponent], exports: [ /*  Angular Material*/ CommonModule, FormsModule, MdButtonModule, /*   */ SomeCustomComponent ] }) export class SharedModule { } 

功能模块


在这里,您可以重复Angular样式指南。 为每个独立的应用程序功能创建一个单独的FeatureModule。 FeatureModule应该仅从CoreModule导入服务。 如果某个模块需要从另一个模块导入服务,则可能需要将该服务移至CoreModule


在某些情况下,仅需要某些模块使用该服务,而无需将其导出到CoreModule 。 在这种情况下,您可以创建一个特殊的SharedModule ,它将仅在这些模块中使用。
, — , - , , CoreModule , SharedModule .


, . , . , , .


参考文献


  1. https://github.com/ngrx/store
  2. http://stepansuvorov.com/blog/2017/06/angular-rxjs-unsubscribe-or-not-unsubscribe/
  3. https://medium.com/@tomastrajan/6-best-practices-pro-tips-for-angular-cli-better-developer-experience-7b328bc9db81
  4. https://habr.com/post/336280/
  5. https://angular.io/docs

Source: https://habr.com/ru/post/zh-CN425959/


All Articles