静的ファイルと SPA#
配り方は3つあります。どれもルーティング本体とは別のハンドラです(要件 F-W-20)。
| やりたいこと | 使うもの | 当たらなかったら |
|---|---|---|
| CSS・JS・画像をそのまま配る | AssetHandler |
404 |
| SPA(どのパスでも同じ index.html) | SpaHandler |
いちばん近い index.html |
| MPA(ディレクトリごとに index.html) | MpaHandler |
拡張子が無ければ <パス>/index.html |
中身はクラスパスから読みます。 jar に入れたまま配れますし、
開発中は build/resources/main から読まれます。どちらでも同じ動きです(要件 F-W-18)。
静的ファイル#
install(() -> AssetHandler.mount("/assets", "test-assets"));
第2引数はクラスパス上のディレクトリです。ファイルシステムのパスではありません。
AssetHandler.mount("/assets", "assets") なら
src/main/resources/assets/app.css が /assets/app.css で出ます。
GET と HEAD の2本(/assets/*)が登録されます。
[!WARNING] **
/assets自身(末尾なし)には応えません。**登録されるのは/assets/*だけです。 SPA と MPA は接頭辞そのものも登録するので、そこだけ形が違います。
登録されたルートは routes() で受け取れます。属性を付けられます。
AssetController assets = AssetHandler.mount("/assets", "assets");
for (Route route : assets.routes()) {
route.attribute(NO_AUTH, true);
}
install(() -> assets);
SpaHandler.mount(...) / MpaHandler.mount(...) でも routes() は同じように使えます。
ただし本数が違います——SPA と MPA は接頭辞自身も登録するので4本です
(/app と /app/* の GET と HEAD)。
キャッシュ#
| 拡張子 | Cache-Control |
|---|---|
.js .css .woff .woff2 |
public,max-age=31536000,immutable |
| そのほか | public,max-age=<assets.max_age>,must-revalidate |
動的なレスポンスの既定は no-store です(要件 F-X-07)。静的配信だけがここを上書きします。
assets {
max_age = 0 # 通常のファイル
immutable_max_age = 31536000 # 内容ハッシュ付きのファイル向け(1年)
etag = true
if_modified_since = true
}
注意
immutable が付くのは拡張子だけで判断しています。
app.js を中身を変えて置き直しても、ブラウザは1年取りに来ません。
ファイル名にハッシュを入れる(app.9f3a1c.js)か、assets.immutable_max_age を下げてください。
条件付き GET#
ETagは弱い ETag(パス・更新時刻・サイズの md5)Last-ModifiedはURLConnectionの更新時刻- 判定は
If-None-Matchが先。無ければIf-Modified-Since - 一致したら 本文を読まずに 304
無いもの#
| Range(部分取得) | ありません。206 は返りません |
| 個別の圧縮 | **ありません。**サーバー全体の server.compression(helidon)に任せています |
| ディレクトリ一覧 | 出しません |
SPA#
install(() -> SpaHandler.mount("/app", "test-spa"));
/app/... に来たとき、こう探します。
- 実ファイルがあればそれを返す(
AssetHandlerと同じヘッダ) - 無ければ、そのパスから上へ順に
index.htmlを探す - 最後にベース直下の
index.html - それも無ければ 404
通常のルートと共存します。/api/items を先に書いてあれば SPA には食われません。
ツリーの優先順(固定セグメント > パスパラメータ > ワイルドカード)がそのまま効きます。
パスごとに index.html を書き換える#
クローラや OGP のために <title> や <meta> を差し替えたいときに使います。
install(() -> SpaHandler.mount("/app", "test-spa", spa -> spa
.route("/app/items/{id}", (context, html) ->
html.replace("<!--title-->", "<title>item " + context.request().bodyPath().getString("id") + "</title>"))
));
パスの書き方は本体のルートと同じです({id}、末尾の /*)。
マッチも本体と同じルートツリーがやります(要件 D-75)。
| 優先順 | 固定 > 変数 > ワイルドカード。書いた順には依存しない |
| パスパラメータ | context.request().bodyPath().getString("id") |
| 重複したパス | 登録時に落ちる |
| 本体のルート一覧 | 混ざらない(別インスタンス。要件 F-W-20) |
補足
書き換えたときだけ ETag と Last-Modified は付きません(中身がリクエストごとに変わるため)。
Cache-Control は public,max-age=<assets.max_age>,must-revalidate になります。
MPA#
install(() -> MpaHandler.mount("/docs", "test-mpa"));
拡張子があればファイル、無ければ <パス>/index.html を返します。
/docs/guide → docs/guide/index.html です。
安全側の既定#
パスは配る前に必ず検査します。危ないものは 404 です(何があるかを教えないため)。
弾くもの:NUL 文字、バックスラッシュ、空のセグメント(//)、. と .. のセグメント。
こつ
%2e%2e のようなパーセントエンコードも弾けます。
セグメントごとにデコードしてから検査するので、.. が復元されたところで捕まります。
開発中に直したとき#
jimbleRun の監視対象には .html / .js / .css が入っています。
保存すればリソースがビルドし直され、次のリクエストで反映されます(ホットリロード)。
落とし穴
アプリを止めずにファイルだけ差し替えると、古いままになることがあります。
「ファイルがあるか」と index.html の中身をメモリに覚えているためで、
消す口は Resources.clearCache() しかありません。jimbleRun を使っていれば入れ替えのたびに作り直されます。