はじめに
S3上にreact-routerを使ってルーティングしたReactアプリをデプロイするのにめちゃくちゃ苦労したので、どのように実現したかを記録します。
なお、使用していたはクラウドのサービスはAWSのS3、そしてCloudFrontです。
アプリはviteを使って構築し、React、TypeScriptで構成。react-routerを使ってルーティングをしています。
やりたかったこと
作っていたのは以下のようにTopページと、各機能のページに遷移できるボタンがあるような簡易的なWebアプリです。
一部省略していますが、コードのイメージとしては以下のような形でした。
// ルーティングの設定
const MainRoutes = [
{ path: "/", element: <Top />},
{ path: `/pageA`, element: <PageA />},
{ path: `/pageB`, element: <PageB />},
{ path: `/pageC`, element: <PageC />}
];
// ルーティング用のコンポーネント生成
const Routes = () => {
return useRoutes(MainRoutes);
};
// 画面描画
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<Routes />
</BrowserRouter>
</React.StrictMode>
);
ボタンを押下したときに、それぞれのurlのページに飛ばし、react-routerをつかってそれぞれのパスに応じたコンポーネントを表示させる、というような作りです。
ローカルの環境ではこれでも問題なく動いていました。
しかし、これをS3の環境に上げたときまったく想定通りの挙動にならず、調査に途方もない時間がかかったので、一つずつ紐解いていきます。
S3上でのhtmlファイルの扱いへの理解不足
ルートパスの理解
AWSのS3では「バケット」と呼ばれる入れ物にファイルをアップロードしていきます。
そして、そのファイルへのアクセスを許可することで、簡単な静的ウェブサイトならそのまま公開することができます。
ただ、あくまでもただのファイルを公開しているだけなので、ファイルパスがそのままURLになります。
例えば通常のウェブサイトであれば
https://hogehoge.jp/react-app
というURLにアクセスすることで、用意しているindex.html
が表示されるようになっています。
ところが、S3の場合にはindex.html
そのものへのアクセスになるため、
https://hogehoge.jp/react-app/index.html
というURLの形式になります。
※S3のバケットのURLをhttps://hogehoge.jp
というドメインに結びつけ、react-app
というフォルダ形式のオブジェクトを作成し、その下にファイルを配置しているものとします。
このとき、ルートパスは/
ではなく/index.html
になるのです。
そのため、TOPページのコンポーネントのルーティング対象のパスは/
ではなく、/index.html
が正しいことになります。
よって、コードは以下の通りになりました。
// ルーティングの設定
const MainRoutes = [
{ path: "/", element: <Top />},
+ { path: "/index.html", element: <Top />},
{ path: `/pageA`, element: <PageA />},
{ path: `/pageB`, element: <PageB />},
{ path: `/pageC`, element: <PageC />}
];
/
はローカル環境で動かす際に必要なので残しています。
今回はこのようにして解決しましたが、実はこの問題は「静的ウェブサイトホスティング」を利用することで解消することができます。
しかし、私のやっていたプロジェクトではこの対応をなぜか採用していなかったため、静的ウェブサイトホスティングを利用せず、ルートパスが/index.html
になるものとして対応をしました。
ルーティングの理解
この部分に関しては、現時点でも完全に理解が及んでいないので不正確な部分があるかもしれません。
react-routerはフロント側のみでルーティング処理を行ってくれるモジュールです。
冒頭で示したアプリのようにページAボタンを押下して、https://hogehoge.jp/react-app/pageA
のURLに飛ぶことを考えます。
バックエンドで処理をする場合、pageA.html
を探して、その中身を表示します。
react-routerの場合はindex.html
を表示したまま、設定したパスに紐づくコンポーネントをJavaScriptの処理で表示させます。
これが、フロント側のみでルーティングができる仕組みの一端です。
しかし、S3上では話が変わってきます。
Aボタン押下でhttps://hogehoge.jp/react-app/pageA
のURLを呼ぶと、S3上でreact-app
オブジェクトの下にあるpageA
オブジェクトを探しに行きます。
つまり、index.html
から離れてしまうのです。
これでは、困るのでどんな場合でもindex.html
を返すようにする必要があります。
ここで登場するのがCloudFrontFunctionsです。
CloudFrontはAWSでコンテンツを配信するためのサービスで、そこに紐づけて様々な処理を実施できるのが、CloudFrontFunctionsになります。
冗長になるのでCloudFrontFunctionsの詳細な設定方法は省略しますが、以下のような処理を作成しました。
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri = '/index.html';
}
return request;
}
端的に言えば、URLの末尾にindex.html
を付与するような処理です。
これにより、https://hogehoge.jp/react-app/pageA
を呼び出した際、pageAのオブジェクトを探しに行くのではなく、常にindex.html
を返すようにできます。
そうすることで、react-routerの処理も正常に働き、PageAのコンポーネントを表示することができます。
ベースURLの理解不足
react-routerを使うにあたって、ベースとなるURLを理解しておく必要があります。
遷移先のパスの指定時に/
や/pageA
と書きますが、このパスより前に指定されるパスが何なのか、という話です。
今回index.html
を配置したのはhttps://hogehoge.jp/react-app
です。
basenameを指定しなかった場合、直前のパスで想定されているのはhttps://hogehoge.jp
になります。
よって、/pageA
として遷移対象になるのはhttps://hogehoge.jp/pageA
が来たときになってしまいます。
これでは困るので、そのときに指定するのがreact-routerのbasename
です。
// 画面描画
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
- <BrowserRouter>
+ <BrowserRouter basename="/react-app">
<Routes />
</BrowserRouter>
</React.StrictMode>
);
今回はS3のメインのバケットではなくそのひとつ下のオブジェクトであるreact-app
の下に配置しています。
このようにサブディレクトリがある場合は、それをbasename
に指定します。
すると、https://hogehoge.jp/react-app/
までが補完されることになり、正しく遷移することができるようになります。
まとめ
いろいろ調べながらやって、苦しみ抜いた末に、なんとかS3上に配置したアプリを正しく閲覧できるようになりました。
ただ、本当にいろいろ試した結果できるようになったので、もしかするとやったけど書ききれていない設定があるかもしれません。
業務でデッドラインが決められている中、知見がある人もおらず、手探りで作業をするのはかなり苦しく、辛いものがありますが、やり遂げられたときの達成感はなんとも言えません。
ただ、反省点として作業記録を細かくつけられなかったので、今後に活かせるようにするためにも、焦らずやったことを逐一まとめていきたいと思いました。