更新されたAngularのリリースから十分な時間が経過しました。 現在、多くのプロジェクトが完了しています。 「はじめに」から、多くの開発者がすでにこのフレームワークとその機能の有意義な使用に移行し、落とし穴を回避する方法を学びました。 各開発者および/またはチームは、独自のスタイルガイドとベストプラクティスをすでに作成しているか、または他のものを使用しています。 しかし同時に、このフレームワークの多くの機能を使用せず、かつ/またはAngularJSのスタイルで記述されていない多くのAngularコードを扱う必要があります。
この記事では、Angularフレームワークを使用する機能と機能の一部を紹介します。著者の控えめな意見によると、これらはマニュアルで十分にカバーされていないか、開発者によって使用されていません。 この記事では、「インターセプター」HTTP要求の使用、ユーザーへのアクセスを制限するためのルートガードの使用について説明します。 RxJSの使用とアプリケーション状態の管理に関するいくつかの推奨事項が示されています。 また、プロジェクトコードの設計に関する推奨事項も示します。これにより、おそらくプロジェクトコードがよりわかりやすく、より理解しやすくなります。 著者は、この記事が、Angularに精通し始めたばかりの開発者だけでなく、経験豊富な開発者にも役立つことを望んでいます。
HTTPを使用する
クライアントWebアプリケーションの構築は、サーバーへのHTTP要求を中心に行われます。 このパートでは、HTTPリクエストを処理するためのAngularフレームワークの機能のいくつかについて説明します。
インターセプターの使用
場合によっては、リクエストがサーバーに到達する前に変更する必要がある場合があります。 または、各回答を変更する必要があります。 Angular 4.3以降、新しいHttpClientがリリースされました。 インターセプターを使用してリクエストをインターセプトする機能を追加しました(はい、それらは最終的にバージョン4.3でのみ返されました!これは、AngularJの最も期待されていなかった機能の1つで、Angularに移行しませんでした)。 これは、http-apiと実際のリクエストの間の一種のミドルウェアです。
一般的な使用例の1つに認証があります。 サーバーから応答を取得するには、リクエストに何らかの認証メカニズムを追加する必要があります。 インターセプターを使用するこのタスクは、非常に簡単に解決されます。
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
の機能の1つは、要求の進行状況を追跡する機能です。 たとえば、大きなファイルをダウンロードする必要がある場合、おそらくダウンロードの進行状況をユーザーに報告する必要があります。 進行状況を取得するには、 HttpRequest
オブジェクトのreportProgress
プロパティをtrue
設定する必要があり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
を返します。 ここで必要なのは、コンポーネントのダウンロードの進行状況を表示することだけです。
ルーティング ルートガードの使用
ルーティングを使用すると、アプリケーション要求をアプリケーション内の特定のリソースにマップできます。 状況によっては、特定のコンポーネントが配置されているパスの可視性を制限する問題を解決する必要がある場合がよくあります。 これらの場合、Angularには遷移制限メカニズムがあります。 例として、ルートガードを実装するサービスがあります。 アプリケーションで、ユーザー認証がJWTを使用して実装されているとします。 ユーザーが承認されているかどうかを確認するサービスの簡易バージョンは、次のように表すことができます
@Injectable() export class AuthService { constructor(public jwtHelper: JwtHelperService) {} public isAuthenticated(): boolean { const token = localStorage.getItem("token"); // return !this.jwtHelper.isTokenExpired(token); } }
ルートガードを実装するには、1つの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
メソッドは、ルートのアクティブ化条件で使用できるブール値を返します。
これで、作成したルートガードを任意のルートまたはパスに適用できます。 これを行うには、 Routes
を宣言するときに、 canActivate
セクションでCanActivate
インターフェイスを継承するサービスを指定します。
export const ROUTES: Routes = [ { path: "", component: HomeComponent }, { path: "profile", component: UserComponent, canActivate: [AuthGuardService] }, { path: "**", redirectTo: "" } ];
この場合、 /profile
ルートにはオプションの構成値canActivate
ます。 AuthGuard
は、このcanActivate
プロパティへの引数として渡されます。 次に、誰かが/profile
パスにアクセスしようとするたびにcanActivate
メソッドが呼び出されます。 ユーザーが許可されている場合、 /profile
パスにアクセス/login
ます。そうでない場合、 /login
パスにリダイレクトされます。
canActivate
では、このパスでコンポーネントをアクティブ化することはできますが、切り替えることはできません。 コンポーネントのアクティベーションとロードを保護する必要がある場合、この場合はcanLoad
を使用できます。 CanLoad
実装は、類推によって行うことができます。
クッキングRxJS
AngularはRxJSの上に構築されています。 RxJSは、監視可能なシーケンスを使用して、非同期およびイベントベースのデータストリームを操作するためのライブラリです。 RxJSは、ReactiveX APIのJavaScript実装です。 ほとんどの場合、このライブラリを操作するときに発生するエラーは、その実装の基本に関する表面的な知識に関連しています。
イベントにサインアップする代わりに非同期を使用する
Angularフレームワークを使用するようになったばかりの多数の開発subscribe
は、 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"); } }
テンプレートを介してサブスクライブすることにより、Angularはコンポーネントが破損したときにObservable
から自動的にサブスクライブ解除されるため、メモリリークを回避します。 この場合、HTTPリクエストの場合、非同期パイプを使用しても実際には利点はありませんが、1つを除いて-データが不要になった場合、非同期はリクエストをキャンセルし、リクエストの処理を完了しません。
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には、2種類のObservable
があります-有限と無限、いくつかはそれぞれ有限を生成し、他はそれぞれ無限の値を生成します。
Http
Observable
はコンパクトで、DOMイベントのリスナー/リスナーは無限のObservable
です。
無限のObservable
値へのサブスクライブが手動で(非同期パイプを使用せずにObservable
行われた場合、必ず応答する必要があります。 有限のObservableを手動でサブスクライブする場合、サブスクライブを解除する必要はありません。RxJSがこれを処理します。 コンパクトなObservables
の場合、 Observables
実行時間が必要以上に長い場合(たとえば、複数の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() { // observables } }
- 無限のシーケンス。 例は、
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
での作業を簡素化するために、2つの便利な関数takeUntil
とtakeWhile
ます。 それらは同じアクションを実行します-ある条件の終わりにObservable
からサブスクライブを解除します。違いは受け入れられた値のみです。 takeWhile
はboolean
受け入れ、 takeUntil
Subject
takeUntil
。
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
はサブスク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
からサブスクObservable
を解除する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の主な仮定:
- アプリケーションの状態全体のための1つのストア
- 読み取り専用状態
- 変更は「純粋な」機能によって行われ、次の要件に従います。
- ネットワークまたはデータベースを介して外部呼び出しを行ってはなりません。
- 渡されたパラメーターのみに依存する値を返します。
- 引数は不変、つまり 関数はそれらを変更すべきではありません。
- 同じ引数で純粋な関数を呼び出すと、常に同じ結果が返されます。
状態管理機能の例:
// 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
かなりかさばる状況を認識しています。 これは、再利用可能なライブラリが多数ある大規模なアプリケーションで特に顕著です。
import { SomeService } from "../../../core/subpackage1/subpackage2/some.service";
このコードで他に何が悪いですか? コンポーネントを別のディレクトリに転送する必要がある場合、 import
の式は無効になります。
この場合、エイリアスを使用すると、 import
かさばる式をimport
して、コードをよりきれいにすることができます。 エイリアスを使用できるようにプロジェクトを準備するには、tsconfig.jsonにbaseUrlプロパティと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";
コア、共有、および機能モジュール
アプリケーションコンポーネントをより柔軟に管理するために、文献やさまざまなインターネットリソースで、コンポーネントの可視性を広めることをお勧めします。 この場合、アプリケーションのコンポーネントの管理が簡素化されます。 次の分離が最も一般的に使用されます:コア、共有、および機能モジュール。
コアモジュール
CoreModuleの主な目的は、アプリケーション全体に対して1つのインスタンスを持つサービスを記述することです(つまり、シングルトンパターンを実装します)。 多くの場合、これらには認証サービスまたはユーザー情報を取得するためのサービスが含まれます。 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
に移動する必要があります。
場合によっては、一部のモジュールでのみサービスを使用する必要があり、 CoreModule
にエクスポートする必要はありません。 この場合、これらのモジュールでのみ使用される特別なSharedModule
作成できます。
モジュールを作成するときに使用される基本的なルールは、他のモジュールに依存せず、 CoreModule
提供するサービスとSharedModule
提供するコンポーネントのみに依存するモジュールを作成することSharedModule
。
これにより、開発されたアプリケーションのコードがよりクリーンになり、保守および拡張が容易になります。 また、リファクタリングに必要な労力も削減します。 このルールに従えば、1つのモジュールを変更しても、アプリケーションの残りの部分に影響を与えたり、破壊したりすることはありません。