はじめに
とあるレンタルサーバでVue Routerを用いたページが正しくルーティングされずNotFoundになる事態が発生しました。その原因は、history mode下で http://example.com/hoge
というURLにてアクセスした場合に、/hoge
が実体として存在しないにも関わらず hoge
にアクセスしてしまっていたためでした。この解決方法は、.htaccess
をVue Routerが提示している下記の設定にすることでした。
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
ref: HTML5 History Mode > Example Server Configurations > Apache
この .htaccess
を見た時に、「なぜこれでルーティングが解決されるんだ?」と思ったので、まとめと情報の共有のためのこの記事を執筆することにしました。
時間がない方のために結果だけ
# mod_rewriteというモジュールが利用できる場合
<IfModule mod_rewrite.c>
# 置換エンジンをオンにして
RewriteEngine On
# 置換後のベースパスを / に設定して
RewriteBase /
# 置換前のパスがindex.htmlと完全一致したらそれ以上置換を行わず
RewriteRule ^index\.html$ - [L]
# 置換前のパスが実ファイルでも実ディレクトリでもない場合
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# 置換前の文字列を/index.htmlに変換する
RewriteRule . /index.html [L]
</IfModule>
注意
今回調査したのは2021/04/18で、Apacheのドキュメントは2.4系、Vue Routerは3.x系です。バージョンが変化したり、ドキュメントが変化したりする場合があるので、一次情報を適宜確認していただくようお願いします。参照元は末尾にまとめてありますので、そちらからご確認ください。
Vue RouterのHTML5 Historyモードとは
https://example.com/#/user/id
のようにURLに #
が入らないようになるモードです。
Vue RouterにはURLのシミュレート方法に、hashモードとhistoryモードの2種類があります。これらの比較として、こちらの記事によくまとまっていたので、まとめられていた表を引用します。
hashモード | historyモード | |
---|---|---|
パスの形式 | http://localhost:8080/#/hoge |
http://localhost:8080/hoge |
メリット | 設定が簡単 historyモードより早い HTML5 History API不要 |
URLに# が入らない |
注意点 | URLに# が入る |
サーバへの通信が発生する サーバの設定が必要 HTML5 History API必要 |
これに加えて、historyモードには、適切なサーバーの設定をしないと、ユーザーがブラウザで直接URLにアクセスした場合に 404 エラーが発生する という問題があります。この問題の原因は、アプリケーションがシングルページクライアントアプリケーションであるためです。この問題を解決するために、URLがどの静的なアセットともマッチしなかった場合に、 アプリケーションが動作している index.html
にサーブすれば良いです。
.htaccess
とは
サーバの設定を記述するファイルです。
Apacheのドキュメントには次のように書かれています。
.htaccess files (or "distributed configuration files") provide a way to make configuration changes on a per-directory basis.
.htaccess
ファイル(分散設定ファイル)は、ディレクトリ毎にサーバの設定を変更させる方法を提供します。
.htaccess files should be used in a case where the content providers need to make configuration changes to the server on a per-directory basis, but do not have root access on the server system.
.htaccess
ファイルは、コンテンツプロバイダがディレクトリ毎にサーバの設定を変更させる必要がありますが、そのコンテンツプロバイダがサーバのrootアクセス権限を持っていない場合に使われるべきです。
Vue Routerのドキュメントで提示された .htaccess
を読む
提示された .htaccess
の用途をざっくりコメントで説明します。
# mod_rewriteというモジュールが利用できるか確認する
<IfModule mod_rewrite.c>
# ランタイム書き換えエンジンを有効化する
RewriteEngine On
# ディレクトリ毎に書き換える際のベースURLを設定する
RewriteBase /
# 書き換えエンジンのルールを定義する
RewriteRule ^index\.html$ - [L]
# 書き換えが行われる条件を定義する
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
それぞれ見ていきます。
IfModule
Apacheのドキュメントによると、構文は、 <IfModule [!]module-file|module-identifier> ... </IfModule>
で、「モジュールが存在する時に処理されるディレクティブを指定するために利用される」と書かれています。
The ... section is used to mark directives that are conditional on the presence of a specific module.
更に、「このセクションは、特定のモジュールが利用可能かどうかに関わらず機能する1つの設定ファイルが必要な場合にのみ使用してください。通常の操作ではディレクティブを セクションに配置する必要はありません。」と書かれています。
This section should only be used if you need to have one configuration file that works whether or not a specific module is available. In normal operation, directives need not be placed in sections.
この場合は、後続する RewriteHoge というディレクティブを使用するために、 mod_rewriteがあることを保証するために書かれていると考えられます。
RewriteEngine
Apacheのドキュメントによると、構文はRewriteEngine on|off
で、「ランタイム書き換えエンジンを有効または無効にします」と書かれています。
The RewriteEngine directive enables or disables the runtime rewriting engine.
ランタイムにURLを書き換えるために、このエンジンを有効化しています。
RewriteBase
Apacheのドキュメントによると、構文はRewriteBase URL-path
で、「ディレクトリ毎にURLの接頭辞を指定します」と書かれています。
The RewriteBase directive specifies the URL prefix to be used for per-directory (htaccess) RewriteRule directives that substitute a relative path.
今回は、URLを書き換えた後のベースURLを /
に設定しています。
RewriteRule
Apacheのドキュメントによると、構文は RewriteRule Pattern Substitution [flags]
で、「書き換えエンジンのためのルールを定義します」と書かれています。
Description: Defines rules for the rewriting engine
また、「ディレクティブは、単一の書き換えルールを定義する各インスタンスによって、複数回存在する可能性があります。これらのルールが定義される順序は、ランタイムで適用される順序なので重要です。」とも書かれています。
The directive can occur more than once, with each instance defining a single rewrite rule. The order in which these rules are defined is important - this is the order in which they will be applied at run-time.The order in which these rules are defined is important - this is the order in which they will be applied at run-time.
提示された .htaccess
では、下記の2つが順番に書かれています。
- RewriteRule ^index.html$ - [L]
- RewriteRule . /index.html [L]
構文に基づいて順番に見ていきましょう。
1つ目は、 RewriteRule ^index\.html$ - [L]
で、「index\.html
に完全マッチする場合はそれ以上置き換えを行わない」という設定ルールです。
厳密に解釈すると次のように解釈できます。
要素名 | 値 |
---|---|
Pattern(置換対象のパターン) | ^index.html$ |
Substitution(置換後の文字列) | - |
[flags](フラグ) | [L] |
置換パターンには、文字列の集合を一つの文字列で表現する方法であるperlの正規表現が利用されています。^index\.html$
は、index\.html
に完全に一致するURLパスが対象です。そして、置換後の文字列として設定されている -
は、置き換えをせずに動作のみ行います。更に、フラグの [L]
は、Lastを意味し、この条件にマッチしたURLをそれ以降書き換えない設定です。
2つ目の RewriteRule . /index.html [L]
も同様に考えると、 「任意の1文字を 「置換前の文字列を/index.htmlに変換する」 という設定ルールです。index.html
に書き換える」
ここは、.
ではなく、.*
にすべきだと思いました。何か.
にしている理由があるのでしょうか?有識者の方がいらっしゃいましたら、コメントをいただきたいです。
RewriteCond
Apacheのドキュメントによると、構文は RewriteCond TestString CondPattern [flags]
で、「書き換えが行われる条件を定義します」と書かれています。
Description: Defines a condition under which rewriting will take place
続いて、「1つ以上のRewriteCondをRewriteRuleディレクティブの前に置くことが出来ます。次のルールは、URIの現在の状態がそのパターンと一致し、これらの条件が満たされる場合にのみ使用されます」と書かれています。
One or more RewriteCond can precede a RewriteRule directive. The following rule is then only used if both the current state of the URI matches its pattern, and if these conditions are met.
「次のルール」を全て書くと長くなるので、提示された .htaccess
の理解に必要なサーバ変数の%{REQUEST_FILENAME}
に絞ります。
このサーバ変数には、この変数が参照された際にサーバによって既に指定されている場合は、リクエストにマッチするそのファイルかスクリプトの完全なローカルファイルシステムパスが格納されます。それ以外の場合は、REQUEST_URIの値と同じ値が格納されます。
その上で、提示された .htaccess
のRewriteCondの2つの構文を見てみます。
- RewriteCond %{REQUEST_FILENAME} !-f
- RewriteCond %{REQUEST_FILENAME} !-d
これらは順番に、!-f
は %{REQUEST_FILENAME}
がファイルが存在しないか、通常のファイルではない場合という条件、!-d
は %{REQUEST_FILENAME}
がディレクトリが存在しないか、ディレクトリでない場合という条件を指します。すなわち、2つ合わせて、実ファイルでも実ディレクトリでもないという条件になります。これは、存在しないパスを提示された場合という意味だと思います。
まとめ
以上より、.htaccess
は下記の通り解釈されます。
# mod_rewriteというモジュールが利用できる場合
<IfModule mod_rewrite.c>
# 置換エンジンをオンにして
RewriteEngine On
# 置換後のベースパスを / に設定して
RewriteBase /
# 置換前のパスがindex.htmlと完全一致したらそれ以上置換を行わず
RewriteRule ^index\.html$ - [L]
# 置換前のパスが実ファイルでも実ディレクトリでもない場合
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# 置換前の文字列を/index.htmlに変換する
RewriteRule . /index.html [L]
</IfModule>
気になったので調べただけでしたが、大分長くなってしまいました。
私と同じ疑問を持った方のためになれれば幸いです。