自从更新的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");
要实现路由保护,必须实现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
路由具有可选的配置值canActivate
。 AuthGuard
描述AuthGuard
作为参数传递给此canActivate
属性。 接下来,每次有人尝试访问/profile
路径时,都会调用canActivate
方法。 如果用户被授权,则他将获得对/profile
路径的访问权限,否则将被重定向到/login
路径。
您应该意识到canActivate
仍然允许您在此路径上激活组件,但不允许您切换到该组件。 如果您需要保护组件的激活和加载,那么在这种情况下,我们可以使用canLoad
。 CanLoad
实现可以类推。
烹饪RxJS
Angular建立在RxJS之上。 RxJS是一个使用可观察序列处理异步和基于事件的数据流的库。 RxJS是ReactiveX API的JavaScript实现。 在大多数情况下,使用此库时发生的错误与其实现基础的肤浅知识有关。
使用异步而不是注册事件
大量刚开始使用Angular框架的开发人员使用Observable
的subscribe
功能来接收和保存组件中的数据:
@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(); } }
以下是您需要取消订阅的详细信息
- 有必要取消订阅该表格以及您所订阅的各个控件:
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(); } }
- 路由器 根据文档,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() {
- 无休止的序列。 示例是使用
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
的工作,有两个方便的函数takeUntil
和takeWhile
。 它们执行相同的操作-在某些条件结束时取消订阅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 { 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: [ CommonModule, FormsModule, MdButtonModule, SomeCustomComponent ] }) export class SharedModule { }
功能模块
在这里,您可以重复Angular样式指南。 为每个独立的应用程序功能创建一个单独的FeatureModule。 FeatureModule应该仅从CoreModule
导入服务。 如果某个模块需要从另一个模块导入服务,则可能需要将该服务移至CoreModule
。
在某些情况下,仅需要某些模块使用该服务,而无需将其导出到CoreModule
。 在这种情况下,您可以创建一个特殊的SharedModule
,它将仅在这些模块中使用。
, — , - , , CoreModule
, SharedModule
.
, . , . , , .
参考文献
- https://github.com/ngrx/store
- http://stepansuvorov.com/blog/2017/06/angular-rxjs-unsubscribe-or-not-unsubscribe/
- https://medium.com/@tomastrajan/6-best-practices-pro-tips-for-angular-cli-better-developer-experience-7b328bc9db81
- https://habr.com/post/336280/
- https://angular.io/docs