Angular アプリを GitHub Pages に公開する際、ルーティングによる 404 を回避する、具体的な実装方法

以前書いた以下の記事の詳説。

目次

事象のおさらい

Angular アプリを GitHub Pages で公開すると、そのままではトップページの URL しか有効にならない。

例えば拙作の Angular Utilities でいうと、トップページの URL は https://neos21.github.io/angular-utilities/ だが、ある画面に遷移すると、https://neos21.github.io/angular-utilities/text-converter/case-converter という URL がブラウザのアドレスバーに表示される。

そこでこの URL をコピーして、別のタブにでも貼り付けて遷移しようとしてみると、/text-converter/case-converter/ やその配下に index.html というファイルはないので、404 エラーになってしまう。GitHub Pages は Angular のルーティング URL を考慮してはくれないのだ。

そこでコレを自前でなんとかしよう、というのが今回の記事。

アプリのビルド時は Base Href を変更する

GitHub Pages にデプロイする Angular アプリは、--base-href オプションをデフォルトの / ではなく ./ という相対パスに変更してやらないと、上手くルーティングが機能しない。よって、ビルド時のコマンドは以下のようになるだろう。

$ ng build --base-href ./ --prod

このまま package.json に npm-scripts として記載しておいて良いだろう。

404.html からリダイレクトする

前回の記事でも書いたが、GitHub Pages は、404.html というファイルがあると、それを 404 ページとして利用してくれる機能がある。コレを利用して、まずは 404 になる URL で遷移してきた時に、その URL を記録しつつ、Angular アプリのトップページに遷移させる。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script>
      sessionStorage.redirect = location.href;
    </script>
    <meta http-equiv="refresh" content="0;URL=/angular-utilities/">
    <title>Angular Utilities</title>
  </head>
  <body>
    <a href="/angular-utilities/">Angular Utilities</a>
  </body>
</html>

どうやら IE だけ、404 ページのサイズが 512 バイト以下でないと表示してくれないようなので、必要最小限の内容にしておく。ちなみに上述のコードで、インデントなど諸々込みで 343 バイト。必要に応じて改行やインデントを削り調整しよう。

このファイルでやっているのは、meta 要素によるリダイレクトと、SessionStorage に叩かれた URL を記録するという処理。このままリダイレクトすると、Angular アプリの起動時に SessionStorage から URL 情報が拾える、というワケ。

AppComponent でリダイレクト処理を行う

というワケで、リダイレクトが行われて、Angular アプリが起動する時に、ngOnInit() にてリダイレクト処理を行ってあげる。

export class AppComponent implements OnInit {
  constructor(private router: Router) { }
  
  public ngOnInit(): void {
    // SessionStorage より 404.html からの遷移元 URL 情報を取得する
    const redirectUrl = sessionStorage.redirect;
    // SessionStorage の情報は削除しておく
    delete sessionStorage.redirect;
    
    // 遷移元 URL が取得できていた場合 (もしトップページの URL が取得できた場合は移動する必要ないので無視)
    if(redirectUrl && redirectUrl !== location.href) {
      // ハッシュやパラメータ「#」「?」「;」を除去する : ココはアプリの要件に応じて、ハッシュやパラメータを別途再現するために抽出して処理分けする
      const pureUrl = redirectUrl.split(/#|\?|;/)[0];
      // アドレスバーの URL (履歴) を修正する
      window.history.replaceState(null, null, pureUrl);
      // ドメイン・アプリルート部分 ('https://neos21.github.io/angular-utilities/' の部分) を削除する → 配下のパス文字列だけが残る
      const navigateUrl = pureUrl.replace(/http.*:\/\/.*\/angular-utilities/, '');
      // パス文字列を渡して遷移する
      this.router.navigate([navigateUrl])
        .catch(() => {
          // 遷移エラー時 (受け取った URL が不正だった場合など) は仕方ないのでトップページに遷移する
          this.router.navigate(['']);
        });
    }
  }
}

SessionStorage に保存させた location.href はフルパスなので、Router#navigate() に渡すための加工をしている。パラメータを渡したい場合は、別途上手くちぎって、Router#navigate() の第2引数に渡してやったりする必要がある。

もしも整形した URL に移動できない場合は、Router#navigate() が例外を吐くので、.catch() で処理してやる。こうすれば、ルーティングモジュールで '**' でのリダイレクトを宣言していなくても上手くいく。

コレで完了。

そもそも HashLocationStrategy を使う?

ところで、そもそもユーザに見せる URL が、実は直接遷移しているワケではなく、このような奇妙なリダイレクトをしている、というのは、気持ち悪さがある。

そこで、別の方法として、Angular デフォルトのルーティングの仕組みである PathLocationStrategy ではなく、ハッシュを利用した HashLocationStrategy を利用できる。

HashLocationStrategy は、一昔前の Ajax ページで流行った、#! で始まる Shebang (シバン) と同じ要領で、

といった URL ではなく、

といった URL を利用する。

全てのページは /angular-utilities/ というルート URL のハッシュとして扱われるので、この URL をコピペして遷移されても、トップページが踏まれて、Angular によって上手いことリダイレクト的な処理をやってもらえる、というワケだ。

HashLocationStrategy を利用するのは簡単で、AppRoutingModule にて、useHash というフラグを与えてやるだけで良い。

@NgModule({
  imports: [RouterModule.forRoot(routes, { useHash: true })],  // ← 第2引数で指定
  exports: [RouterModule]
})
export class AppRoutingModule { }

コレなら手軽ではあるが、URL に余計な #/ が挟まるのが嫌かも…?

以上。