概要
Angular 6 で作った Web サイト、サーバにデプロイして外部からアクセスし、リロードボタンを押したら「Not Found」と 404 エラーが返ってきた時に、どう対応すればいいかの備忘録。
※ ごく簡単な対応のため。全ての対策が書かれているわけではありません。より良い対策があればそちらを共有ください。
事象
Angular6 でサクッと構築した Web アプリを、GCP の GCE で立てたサーバにデプロイした。
http://独自ドメイン/
でアプリのトップページに飛びページリロードしたら「Not Found」と表示されて画面遷移できなくなった。(もちろん、トップページに遷移し直せば、通常通り使用できる。)
また、リロードだけでなく、URLのルート以降のパスを追加・変更し Enter キーを押した時も「Not Found」になる。
しかし、ローカルで ng serve
で動かしているときは、リロードしても同じ事象は発生しない。。。
原因
結論から言えば、Angular で作った Web アプリは、ルートにしか index.html が存在しないため。
つまり、ルート以外のパスを直接指定されても、そんな HTML ファイルは存在しないので、存在しない Web ページをリクエストされたら、当然 404 (Not Found) となる。
Angular は、トップページの時点で、ルート以降に何かしらのパスが存在しているはずである(/home
など)。
この状態でページリロードしたときは、 前述の理由通り、HTML がそのパスには実在しないので、「Not Found」となる。URLを直指定した場合も同様の理由である。
そもそも Angular はルートに index.html しかないのに、なぜURLが変わるのか?
Angular がデフォルトで HTML5 の「History API」という機能をしようしているためである。
そもそも、History API とは、以下の様な機能を持った API である。
- 画面を遷移せず、履歴に新たなURLを発行・追加する。
- 現在のページの履歴を変更する。
- ブラウザの戻る・進むボタンをクリックしたときにイベントを検知する。
Angular は、表示するコンポーネントを切り替えながら、この History API を使ってURLを変更しているのだけなので、実際に HTML を移動しているわけではない。
ちなみに、構成によるが、通常の構成であれば、React や Vue でも同様の事象は発生する。
対処法
対策1: URL にハッシュ(#)を導入する(アプリ側での対応)
ハッシュ付きURLを有効にする。
有効化すると、アドレスが http://独自ドメイン/#/home
→ http://独自ドメイン/#/home
の様に変化する。
Angular の場合、Angular Router の Use Hash オプションを有効にするだけ。
@NgModule({
imports: [RouterModule.forRoot(routes, {
useHash: true, // 追加
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
対策2: Web サーバのルート修正で対応する(Web サーバ側での対応)
単純な catch-all フォールバックのためのルートをサーバー側で追加し、もし URL がどの静的なアセットにもマッチしなかった時は、アプリケーションが動作しているのと同じ index.html ページで受け付けるように設定あを行うだけ。
ng serve
でリロードしても大丈夫なのは、ng serve
実行時にこの方式が採用されているから。
各 Web サービスの設定変更内容は以下のとおり。
Apache 設定
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Nginx 設定
location / {
try_files $uri $uri/ /index.html;
}
Native Node.js 設定
const http = require('http')
const fs = require('fs')
const httpPort = 80
http.createServer((req, res) => {
fs.readFile('index.htm', 'utf-8', (err, content) => {
if (err) {
console.log('We cannot open "index.htm" file.')
}
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8'
})
res.end(content)
})
}).listen(httpPort, () => {
console.log('Server listening on: http://localhost:%s', httpPort)
})
Firebase ホスティング
以下を、firebase.json
に追加。
{
"hosting": {
"public": "dist",
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
対策3: SSR を導入する
日本語公式ドキュメントのサーバサイドレンダリングの記事 などを参考に Universal にする。
以上。