2022/05/03

AngularのSPAサイトで 表示ページに応じて ページのタイトルを更新する

経緯

Webサイトを作る際、現在表示しているページのタイトルを、ブラウザのタイトルバーに表示するのがアクセシビリティの面から望まれます。 Single Page Application(以下SPAと表記)では、URI的には複数のページが、実体としては1つにということになります。SPAでは表示しているURIに応じてタイトルバーの表示を変更する必要が出てきます。

SPAサイトをAngularで実装していたのですが、単にページへのリンクを押して遷移するわけではなく、スクリプトが特定の条件を満たした場合に遷移するので、Routerを使ってページ遷移を実現することになります。このパターンの実装方法はネットで探せばまあまあ出てくるのですが、実装してみると動かないことが多々ありました。特定のAngularバージョンでしか動作しないか、記載の間違いと思われます。思いのほか難航してしまったので、解決できた実装内容などを残しておきます。


実装方法 : Angular 13まで

こちらの記事「Setting Page Titles Natively With The Angular Router」で紹介されていました。著者はGDE(Google Developers Experts)の方なので確実な実装だと思います。この記事は前半が現在の最新版であるAngular 13までの実装方法です。

StackOverflowの記事「How to change page title with routing in Angular application?」の、Ankurさんの回答も多少の違いはありますが、ほぼ同じです。


まずは各ページのデータを用意します。ここで任意のデータを渡せる dataプロパティを使ってページタイトルを含めます。

app.module.ts
@NgModule({ imports: [ RouterModule.forRoot([ { path: 'page-path', component: PageComponent, data:{ title: 'タイトル' }, }, ]), ... ], ... })


ページ表示時にタイトルを反映させる処理がこちら。ループで最もチェインの先端のRouteを取得していますが、ページが入れ子になっているケースに対応するためです。

app.components.ts
import { Title } from '@angular/platform-browser'; import { Router, ActivatedRoute, NavigationEnd } from '@angular/router'; import { filter, map } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', providers: [ Title ], }) export class AppComponent { constructor( private router: Router, private pageTitle: Title, ) { } ngOnInit() { this.router.events .pipe( filter(event => event instanceof NavigationEnd), // NavigationEndのタイミングで map(() => { let route: ActivatedRoute = this.router.routerState.root; while (route.firstChild) { route = route.firstChild; // 最も下位のRouteを取得して } if (route.snapshot.data['title']) { return route.snapshot.data['title']; // そのRouteのdataからタイトルを取得 } return null; }) ) .subscribe((title: string) => { if (title) { this.title.setTitle(title); // 取得したタイトルを反映 } }); } }


実装方法 : Angular 14から

上記の記事の後半「Using the built-in TitleStrategy」からは、Angular 14での実装について書かれています(Angular 14はまだプレリリース版のようです)。TitleStrategyクラスを使用することで、より簡単に実装できるようです。

以下のスニペットは記事をもとに書いてみただけです。まだAngular 14の開発環境を用意していないので、動作確認していません。こんな感じになるくらいに認識してください。


各ページのタイトルのために、Routeクラスに専用のプロパティtitleが追加されています。もうdataプロパティは使わなくても構いません。

app.module.ts
@NgModule({ imports: [ RouterModule.forRoot([ { path: 'page-path', component: PageComponent, title: 'タイトル' }, ]), ... ], ... })


ページ表示時にタイトルを反映させる処理は、AppComponentではなくTitleStrategyクラスから派生したサブクラスでupdateTitle()メソッドをオーバーライドして実装します。下記のbuildTitle()メソッドはTitleStrategyクラスのものなのか、サブクラスに自分で実装するのかは、説明がないので不明です。

app.components.ts
@Injectable() export class TemplatePageTitleStrategy extends TitleStrategy { constructor(private readonly title: Title) { super(); } override updateTitle(routerState: RouterStateSnapshot) { const title = this.buildTitle(routerState); if (title) { this.title.setTitle(title); } } }


ソースファイルは増えますが、この実装の方がより直感的で魅力的になったように感じます。


公式痛恨(?)のフライング...

Angularの日本語サイトを彷徨っていたら「Setting the page title」を見つけました。参照したのはAngular 13のstable(v13.3.3)なのですが、この章の記載内容は上記のAngular 14そのものです。フライングで公開してしまったようです。

2022/04/23

GAE/Java8でSPA対応に苦戦

GAEの第1世代VMであるJava8とGoで動かしているアプリを、Single Page Application(以下 略称のSPAを使用します)に一部作り直そうと検討しているのですが、意外と苦戦しています。


問題

SPAでは、複数のパスを実際には1つのHTML/JavaScriptで表示します。例として、以下のパスの3ページを用意したとします。

/         ←3つとも/index/htmlが対応

/aaa    ←〃

/bbb    ←

見た目には3ページでも、実際にはJavaScriptでリクエストされたパスを見て表示内容を切り替えているだけで、コンテンツとしてはこれら3ページを1つのHTML/JavaScriptが表示します。


で、これらのページをいつものようにGAEの静的ファイルとしてホストしようと思っていました。しかしGAEの静的ファイルは基本的に1パス=1ファイルに対応します。つまり"/"に対応するHTML/JavaScriptで3ページすべてを表示するつくりにすると、残り2ページをリクエストしてもGAEの静的ファイルサーバが「404 Not found」を返してしまい、HTML/JavaScriptが得られません。

Java8以外だとapp.yamlの記述(参照:ハンドラ要素)で、"/aaa", "/bbb"が指定された場合でも"/"と同じコンテンツを返すことが可能です。しかしJava8では、そもそもapp.yamlが使用できません。

サーバサイドのアプリはすでに作成済みのものを使いまわすつもりなので、今更ほかの言語で書き換える気などありません。このアプリではMemcacheやSearch APIなど第1世代VMでしか提供されない機能を利用していますので、使えるのは第1世代VMに縛られます。


解決案

いろいろ調査・検討してみました。ここから先はアイデアだけで試したわけではありません。


A案. Firebase Hostingへの一部移行

静的ファイルのホストをFirebase Hostingに変えます。複数パスが1つのHTML/JavaScriptに対応する問題は、firebase.jsonのリライト設定(参照:リライトを構成する)で解決できそうです。

Firebase Hostingは独自ドメインにも対応しています。GAEで実装したAPIはFirebase Hostingとは別ドメインになりますので、Cross Origin Resource Sharing(以下CORS)ポリシーに触れますが、どちらも自前で実装するので適切にレスポンスを返すだけです。


しかし実際のアプリには前記の3つのパス以外に、SPAに入らない全く独立したページも存在します。以下のような感じです。

/         ←3つとも/index/htmlが対応

/aaa    ←

/bbb    ←

/ccc/xxx    ←独立した動的ページ

この独立ページ"/ccc/xxx"は静的ファイルではありませんので、Firebase Hostingで対応できません。現状は静的ファイルと一緒にGAEで対応していますが、Firebase HostingとGAEでドメインが異なるのは問題です。アドレスはエンドユーザにも見えますので、同じアプリの一部であること、あやしいページに遷移したわけではないことがエンドユーザにもわかるように同じドメインへの配置が必要です。


B案. 動的ページは空コンテンツを用意

前記の独立した動的ページ"/ccc"にはhtml/body要素のみの空ページを用意しておき、"/xxx"の部分は前記のリライトで"/ccc"に置き換えます。中身はajaxで"/xxx"を判断して埋めます。

これならエンドユーザに見えるドメインはすべて同じにできます。ajaxのリスエストエンドポイントはGAEなので別ドメインですが、エンドユーザには見えないので問題にならないはずです。

ただしこのページは他のページとデザインなども異なるので、「空ページを埋める」でどこまで対応できるのか不安が残ります。


C案. API Gatewayで振り分け

一般提供になったばかり(だったと思う)API Gatewayで同じドメインの別パスとして、Firebase HostingとGAEの振り分けを追加する案です。B案の動的ページの不安も解決できる可能性があります。

しかしドキュメントを読んだだけでも以下の懸念が...

API GatewayがFirebase HostingやCloud Storageの静的コンテンツも対象に出来れば面白いと思うのですが。1つのWebアプリとして単一のドメインの中で静的コンテンツやajaxのAPIだけでなく、公開APIも対応できます。

API Gatewayのドキュメントには静的コンテンツに関する記述はなさそうです。名前のとおりWeb APIのみを対象としているのかもしれません。


(2022/4/25追記)

Firebase Hostingの動的コンテンツ配信の機能(参照:Firebase Hosting を使用した動的コンテンツの配信とマイクロサービスのホスティング)がGAEにも対応していれば問題ないのですが、この機能はCloud Functions for FirebaseとCloud Runにしか対応していません。

仮にアプリを書き直すにしてもCloud Functions for FirebaseはJavaScript/TypeScriptにしか対応していないようなのでパフォーマンスとランニングコストに疑問がありますし、Cloud Runでもランニングコストが課題になります。

なかなか、かゆいところに手が届いてくれないですね。

2022/04/19

WindowsでTypeScriptのソースを入れたフォルダの表示について

問題

 Windowsがフォルダの内容物によって表示の仕方を変えるようになったのは、Windows7あたりだったと思います (Windows Vistaは使ったことがないので不明)。クライアントサイドのソースをTypeScriptで書くようになって、拡張子.tsのファイルが増えてきました。

Windowsで拡張子.tsのファイルが多勢となっているフォルダを開くと、困ったことに勝手にアイコン表示になってしまいます。tsファイルを選択するとツールバーに「ビデオツール」のメニューが表示されることからも、TypeScriptのソースがビデオファイルとして認識されていることがわかります。しかもやたらファイルアクセスを繰り返して異常に遅くなったり、散々です。


原因

Windowsが拡張子".ts"のファイルを拡張子だけで、MPEG2のTransportStreamのファイルと認識してしまうことが原因です。遅くなるのはアイコンとしてビデオのサムネイルを表示しようとして、ファイの内部を解析していることが原因です。

動画と音声を1つのストリームにまとめるために、MPEG2では以下のストリームが規定されています。

  • 放送メディア向けのTransportStream
  • 蓄積メディア向けのProgramStream

動画と音声の両方が1つのファイルに入っているなら、上記のどちらかになります。しかし実際に流通しているMPEG2ファイルの拡張子は ".mpg" や ".mpeg" が多く、ごくまれに ".mpeg2" を見るくらいです。音声付きの動画なのに拡張子".ts"/".ps"なんて、見た記憶がありません。

事実上存在していないに等しいファイルへの対応をデフォルトにするのは止めてほしいものですね。


解決策

根本的にはレジストリをいじるしかないようですが、それは危険なので避けたいところです。

ドライブ単位ですが、以下の手順でも解決しました。

  1. エクスプローラを開いて".ts"ファイルを格納しているドライブを表示。
  2. ファイルアイコンのない場所を右クリックして「プロパティ」を選択。
  3. ドライブのプロパティダイアログが表示されるので、「カスタマイズ」タブを選択。
  4. 「このフォルダーを次の目的に最適化」で「全般」を選択し、その下の「このテンプレートをすべてのサブ フォルダーに適用する」をチェック。
  5. 「OK」を押してダイアログを閉じる。