Angularのページ遷移に応じて何かを行う
Angularを使用してSPAを開発すると、ページの再ローディング無しで画面遷移を行うことができます。ルーティングイベントを使用して、Angular内での画面遷移に応じて何か処理を差し込む方法を紹介します。
前提条件
この記事は以下のAngularバージョンで作成されています。またこの記事ではAngularでルーティングする方法についての紹介は行いません。
Angular CLI: 6.1.5 Node: 8.9.2 OS: darwin x64 Angular: 6.1.6 ... animations, common, compiler, compiler-cli, core, forms ... http, language-service, platform-browser ... platform-browser-dynamic, router Package Version ----------------------------------------------------------- @angular-devkit/architect 0.7.5 @angular-devkit/build-angular 0.7.5 @angular-devkit/build-optimizer 0.7.5 @angular-devkit/build-webpack 0.7.5 @angular-devkit/core 0.7.5 @angular-devkit/schematics 0.7.5 @angular/cli 6.1.5 @ngtools/webpack 6.1.5 @schematics/angular 0.7.5 @schematics/update 0.7.5 rxjs 6.3.0 typescript 2.7.2 webpack 4.9.2
ルーティングイベントの種類
AngularのRouterはページ遷移の状態に応じて各種イベントを発火させています。イベントの種類はと飛んでくる順番はAPIリファレンスにも記載があります。そのうち私がよく使うものをここで紹介します。
種別 | 発火タイミング |
---|---|
NavigationStart | ナビゲーションが開始された時。 |
ActivationStart | ナビゲーション先のコンポーネントが決まった時(GuardやResolveの前)。 |
ActivationEnd | ナビゲーション先のコンポーネントのインスタンス化終了時(GuardやResolveが終わって、遷移先コンポーネントのインスタンスが作られた後)。 |
NavigationEnd | ナビゲーションが終了した時(正常に終了した場合)。 |
NavigationCancel | ナビゲーションが終了した時(ナビゲーション処理の途中でキャンセルされた場合)。 |
NavigationError | ナビゲーションが終了した時(ナビゲーション先が存在しないなど、エラーが発生した場合)。 |
NavigationEnd
や NavigationCancel
、 NavigationError
はページナビゲーションの最後に呼ばれるため、finallyブロックの感覚でよく使っています。また ActivationStart
は遷移先のコンポーネント情報が取れるため、あまり頻度は多くありませんが、使用するときがあります。
実装サンプル
ページ遷移でページタイトルを初期化したい
ページ遷移時にコンテンツに応じてページタイトル(HTMLのtitleタグ)の中身を書き換えることをしますが、開発時など、ページタイトルが決まっていないものが混在している時はとても面倒です。そんな時はルーティングイベントを拾って、毎回ページ遷移時にページタイトルを初期化してしまうのが楽で便利です。
これを実現するにはページタイトルを管理する PageTitlteService
を作成して、ページタイトルをこのサービス経由で設定します。
import { Injectable } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { NavigationEnd, Router } from '@angular/router'; import { filter } from 'rxjs/operators'; /** * ページタイトルを設定するサービス */ @Injectable({ providedIn: 'root' }) export class PageTitleService { private _pageTitle: string = ''; constructor( private readonly _router: Router, private readonly _title: Title ) { // ナビゲーション終了時にタイトルをリセットする this._router.events .pipe(filter(e => e instanceof NavigationEnd)) .subscribe(() => this.setTitle('')); } setTitle(title: string): void { this._pageTitle = title; title = title ? `${title} - SampleSite` : 'SampleSite'; this._title.setTitle(title); } getTitle(): string { return this._pageTitle; } }
この PageTitleService
はコンストラクタで Router#events
から NavigationEnd
のイベントだけを絞ってSubscribeし、ナビゲーションが正常に終了した時のみページタイトルをデフォルト値に設定します。 NavigationEnd
のイベントは遷移先のコンポーネントのコンストラクタが実行されてからコンポーネントの OnInit
が実装される間に発火されるイベントです。遷移先コンポーネントの OnInit
でページタイトルを設定すると、ページ遷移に応じてページタイトルのリセットと設定が実装できます。
ApplicationInsightsにPageViewを送信する
SPAではページロードが初回以外発生しないため、ApplicationInsights等の解析ツールにPageViewイベントを送信するときはPageViewに該当するイベントを手動で送信せねばなりません。ググればAngularにApplicationInsightsを組み込むライブラリくらい見つかりそうですが、今回はナビゲーションイベントから手動でPageViewイベントを送信します。
import { Injectable } from '@angular/core'; import { ActivationStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { Observable } from 'rxjs'; import { filter, map, withLatestFrom } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class PageTrackingService { constructor(private readonly _router: Router) { // NavigationStartから最後に発生したActivationStartイベントの取得 const start$: Observable<ActivationStart | null> = this._router.events.pipe( filter(e => e instanceof NavigationStart || e instanceof ActivationStart), map(e => e instanceof ActivationStart ? e as ActivationStart : null) ); // NavigationEnd/Cancel/Errorと最後の発生したActivationStartを組み合わせる this._router.events .pipe( filter(e => e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError), withLatestFrom(start$) ) .subscribe(([end, start]) => { // コンポーネント名の取得 const component: any = !start ? null : start.snapshot.component; const name = !component ? '' : (typeof component === 'string' ? component : component.name); // ナビゲーション正常終了時 if (end instanceof NavigationEnd) { appInsights.trackPageView(name, end.url); return; } // ナビゲーションキャンセル時(Guardで拒否された場合など) if (end instanceof NavigationCancel) { appInsights.trackEvent('PageNavigation cancelled', { url: end.url, component: name, reason: end.reason }); return; } // ナビゲーションエラー if (end instanceof NavigationError) { appInsights.trackException(end.error, 'Navigation', { url: end.url }); return; } }); } }
結局のところナビゲーション終了系のイベントと ActivationStart
を組み合わせているだけなのですが、入れ子のルーティング設定にしていると ActivationStart
は入れ子の階層分一気に飛んでくる可能性があります。 combineLatest
や zip
を使うとそんな時に対応できないため、 withLatestFrom
で確実に最後の ActivationStart
をセットで処理するように気を付けます。