SPA を開発する際に必須のタスクの一つとして、History APIのフォールバック(以下、単に「フォールバック」と記述する)がある。
この事象について掻い摘んで説明すると、SPA においては URL と HTML ファイルが一対一になっていないので、それに伴う対応を行うこと。
SPA ではその名の通りページは一枚しかなく、URL の管理や表示するコンテンツの切り替えは、ルーティングライブラリが行っている。
例えば、唯一のページを返す URL がhttp://example.com/である場合、その URL が返す HTML ファイルが JS ファイルを読み込み、その JS ファイルがルーティングライブラリによるプログラムを実行することで、http://example.com/fooやhttp://example.com/barといった URL が有効になる。
問題となるのが、いきなりhttp://example.com/fooなどの URL にアクセスされた場合。他のサイトからのリンクであったり、ページのリロードなどで、発生する。
この場合、http://example.com/fooに対応する HTML ファイルは存在しないため、404となってしまう。
そのため、http://example.com/fooやhttp://example.com/barへのアクセスがあった場合は、http://example.com/が対応している HTML ファイルを返すようにしないといけない。
そうすることで、必要な HTML ファイルと JS ファイルが読み込まれ、アクセスのあった URL に対応するコンテンツを表示させることが出来る。
開発環境でよく使われているwebpack-dev-serverでは、historyApiFallbackオプションを使うことで、対応できる。
本番環境ではサーバーの設定が必要。
Vueと組み合わせて使うルーティングライブラリVue Routerのドキュメントで、いくつかの例が簡単に紹介されている。
HTML5 History モード | Vue Router
この記事では、SPA の配信にCloudFrontを使う場合にどうやってフォールバックをするのか、書いていく。
Error Pages を使う?
CloudFrontにはError Pagesという設定項目があり、403や404といったステータスコード毎に、レスポンスするページを設定できる。
これを使ってフォールバックを設定することが出来る。
先程の例なら、404が発生したときは/index.htmlを返すように設定すればよい。
だがこの方法だと、起点となるページが複数あるケースに対応できない。
SPA の規模によっては、起点となるページが一つだとは限らない。
そのようなときは、レスポンスしたいページは複数になる。
例えば、http://example.com/user/*にアクセスがあったときはhttp://example.com/user/index.htmlを、http://example.com/product/*にアクセスがあったときはhttp://example.com/product/index.htmlを返したいとする。
Error Pagesでは、このようなニーズに応えることは出来ない。一つのステータスコードに対して、一つのページしか設定できないから。
複数のフォールバックを設定するには、Lambda@Edgeを使う。
Lambda@Edge で URL をリライトする
Lambda@Edgeは、CloudFrontのエッジロケーションでLambdaを実行するサービス。
これを利用して、リクエストのあった URL に応じてリライトすることで、複数のフォールバックを設定できる。
IAMの設定
Lambdaを利用するにはIAMロールが必要なので、作成する。
まず、以下の内容のIAMポリシーを作成する。
CloudWatch Logsにログを出力することになるので、その権限が必要。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*" } ] }
新しくロールを作り、上記のポリシーを付与する。
そして、「信頼されたエンティティ」としてlambda.amazonaws.comとedgelambda.amazonaws.comを指定する。
これでIAMの設定は完了。
関数の作成
Lambdaで関数を作成する。
Lambda@Edgeを使うには、リージョンを「バージニア北部」に、ランタイムをNode.js 8.10かNode.js 6.10に設定しておく必要がある。
ロールは、先程作成したものを使う。
コードを書く
先程の例だと、以下のように書けばいい。
そんなに難しい内容ではないのだが、ポイントとしては以下。
- 第一引数の
eventの中から、リクエストされた URI を取得する - その URI に応じて処理を変えたり、URI を書き換えたりする
console.logを実行すると、その内容がCloudWatchに保存される
exports.handler = (event, context, callback) => { const {request} = event.Records[0].cf; const currentUri = request.uri; // ドットを含むURIは、アセットへのアクセスとみなし、リライトしない if (currentUri.indexOf('.') !== -1) { console.log(`Don't rewrite. Uri is ${currentUri}`); return callback(null, request); } let newUri = currentUri; switch (true) { case /^\/user/.test(currentUri): newUri = '/user/index.html'; break; case /^\/product/.test(currentUri): newUri = '/product/index.html'; break; default: } console.log(`Old URI: ${currentUri}`); console.log(`New URI: ${newUri}`); request.uri = newUri; return callback(null, request); };
デプロイ
コードを保存したら、デプロイする。
ページ上部の「アクション」ボタンから「Lambda@Edge へのデプロイ」を選択すればよい。
今回のケースでは「CloudFront イベント」は「オリジンリクエスト」を選ぶ。
Lambda@Edge の解除
設定したLambda@Edgeの解除は、Lambdaの画面からは出来ない。
CloudFrontのディストリビューションの設定画面からBehaviorsを選び、Lambda Function Associationsを編集することで解除できる。
そうすると、Lambdaの画面から、関数を削除することも出来るようになる。