HTMLファイルをダブルクリックすると、file:///C:/tools/index.htmlのようなURLでブラウザが開きます。同じファイルをローカルサーバーから配信すると、http://localhost:8080/のようなURLになります。厳密にはfile:はURL scheme、localhostはhostnameなので、この記事ではfile://で直接開く場合とhttp://localhostで配信する場合を比較します。中身が同じHTMLなら、どちらでも同じように動きそうです。
ところが実際には、file://ではJavaScript Moduleの読み込みや隣のファイルへのfetchが失敗し、localhostでは動くことがあります。逆に、単一HTMLとして直接開けること自体が大きな利点になる場面もあります。Browser Kittyではこの2つを別の実行環境として確認しています。違いの中心にあるのは、ファイルが同じかどうかではなく、ブラウザがそのページをどのURL・Origin・HTTP条件で扱うかです。
まず分けて考えたい4つのこと
ファイルを直接読む
Webサーバーを介さず、ブラウザが端末内のHTMLを直接開きます。HTTPレスポンスは存在せず、file URL特有のOriginや保存領域の扱いを受けます。
HTTPで自分の端末から受け取る
ローカルWebサーバーへHTTPリクエストを送り、レスポンスとしてHTMLを受け取ります。通常のOriginの扱いに加え、Content-TypeやRange関連、CSPなどをHTTPレスポンスヘッダーとして設定できます。
安全なコンテキストかは別軸
localhostは開発用に信頼できるOriginとして扱われ、file URLも潜在的に信頼できるものとして扱われます。ただし、Secure Contextなら全APIが使えるという意味ではありません。
ローカル処理かどうかも別軸
file://でも、CORSなど相手側の条件を満たせば外部APIへ通信できますし、localhostでも外部通信なしで処理できます。完全ローカル処理かどうかはURLではなく、実際のデータ経路で判断します。
同じHTMLでも、ブラウザから見た実行環境は同じではない
問題を短くすると、HTMLの中身ではなく、そのHTMLへどう到達したかによってブラウザが適用するルールが変わります。file://ではファイルシステム上のファイルを直接開き、localhostではWebサーバーからHTTPレスポンスとして受け取ります。
その違いから、Origin、CORS、HTTPヘッダー、Web APIの利用条件、保存領域などに差が生まれます。コードを一切変更していなくても、開き方だけで動作差が出るのはこのためです。
HTMLが同じでも、URL schemeと配信方法が変わればブラウザが適用する条件も変わる
file://は直接読み込み、localhostはHTTP配信
file://で開く場合、間にWebサーバーはいません。HTML、JavaScript、画像などはローカルファイルとしてブラウザから参照されます。localhostではブラウザが自分の端末上で動くWebサーバーへHTTPリクエストを送り、HTTPレスポンスとしてリソースを受け取ります。
localhostという名前が付いていても、それだけでクラウドへアップロードしているわけではありません。localhostは通常ループバック先として同じ端末を指します。ただし、ページ内のコードが別の外部URLへ通信することはできるため、『localhostだから通信しない』とも言えません。
| 観点 | file:// | http://localhost |
|---|---|---|
| 読み込み | ファイルを直接読む | ローカルWebサーバーからHTTPで受け取る |
| Origin | 通常opaque originとして扱われる | scheme・host・portで決まる通常のOrigin |
| HTTPレスポンス | なし | あり |
| レスポンスヘッダー | HTTPレスポンス自体がないため、HTTPレスポンスヘッダーもない | CSP、Content-Type、Range関連などをHTTPレスポンスヘッダーとして返せる |
| 単一HTML | ダブルクリックで使える構成に向く | Web配信としての挙動確認に向く |
大きな違いの一つがOrigin
通常のWebページでは、Originはおおまかにscheme・hostname・portの組み合わせで決まります。たとえばhttp://localhost:8080から配信されたページと同じOriginのリソースなら、同一Originとして扱いやすくなります。
file URLは特殊です。MDNでは、現在のブラウザはfile:///で読み込まれたファイルを通常opaque originとして扱うと説明しています。opaque originは他のOriginと同一とは判定されないため、同じフォルダにある別ファイルでも自動的に同一Originとはみなされません。なお、file URLのOriginの扱いには実装依存の部分もあります。
JavaScript Moduleやfetchで差が出やすい
分かりやすい例がJavaScript Moduleです。HTMLから別ファイルのModuleを読み込む構成をfile://で試すと、Moduleのセキュリティ要件によってCORSエラーになることがあります。MDNも、Moduleをローカルファイルとして試すのではなくサーバー経由でテストするよう案内しています。
fetchで隣のJSONやテキストを読む構成でも同様です。ファイルが同じフォルダに存在していても、それだけでJavaScriptから自由に読めるとは限りません。localhostなら、そのファイルを同じHTTP Originからレスポンスとして返す構成にできます。
localhostにはHTTPレスポンスがある
localhostではWebサーバーが応答するため、Content-Type、Content-Length、Content-Range、Content-Security-Policy、Cross-Origin-Opener-Policy、Cross-Origin-Embedder-Policyなど、用途に応じたHTTPヘッダーを返せます。file://にはサーバーから返されるHTTPレスポンスそのものがありません。
メディア再生ではこの違いが具体的に現れます。HTTP Range Requestは、動画プレーヤーなどが大きなリソースの必要な範囲だけ取得するために使えます。Browser Kittyのローカルプレビューサーバーでも紹介動画のRange Requestを処理しています。実装中には、シークなどで古いRange接続をブラウザが閉じたあと、サーバーがその接続へ書き込もうとして例外になる問題もありました。HTMLだけでなく、Webサーバー側の振る舞いもlocalhost環境の一部です。
Secure Contextだけでは説明できない
『file://は安全ではないから動かず、localhostは安全だから動く』と整理すると不正確です。Secure Contextの仕様ではlocalhostのようなループバック先は開発環境として潜在的に信頼できるOriginとして扱われます。またfile schemeも潜在的に信頼できる対象として扱われます。
つまり、file://とlocalhostの違いをSecure Contextだけで説明することはできません。実際には、Origin、URL scheme、CORS、API固有の条件、権限、ユーザー操作などを分けて見る必要があります。
Secure Contextでも、file://では使えないAPIがある
Service Workerが分かりやすい例です。Service WorkerはSecure Context向けの機能ですが、register()へ渡すscript URLやscope URLにはさらに条件があり、http:またはhttps:以外のschemeはTypeErrorになるとMDNに記載されています。localhostはローカル開発用のSecure ContextとしてService Workerを利用できますが、file://のHTMLからfile:のService Workerをそのまま登録することはできません。
このように、『Secure Contextである』はWeb API利用条件の一部にすぎません。ある機能がfile://で動くかを判断するときは、そのAPI固有の仕様まで確認する必要があります。
localStorageもfile://では同じ前提にできない
通常のWebサイトではlocalStorageはOriginごとに分かれます。http://localhost:8080で開くページなら、そのOriginに対応する保存領域として考えられます。
一方、MDNではfile: URLから読み込んだ文書のlocalStorageの要件は未定義で、ブラウザによって挙動が変わり得ると説明しています。現在はfile URLごとに別の保存領域のように見えるブラウザがあっても、それを将来も保証される仕様として依存するべきではありません。単一HTMLで設定や作業状態を保存する場合は、この差も考慮が必要です。
だからBrowser Kittyではfile://とlocalhostを別々に確認する
Browser Kittyでは、可能なツールを単一HTMLとして配布できる構成にしています。JavaScript、CSS、SVG、Worker、Wasmなどを可能な範囲で1ファイルへまとめれば、隣のファイルをfetchしたりModuleとして読み込んだりする必要が減り、file://でも成立しやすくなります。これは配布しやすさだけでなく、ローカルファイル特有の制約を減らす意味もあります。
一方、開発時にはlocalhostでも確認します。Web版としてのMIME type、Range Request、CSP・COOP・COEPなどをHTTPレスポンスヘッダーとして設定した場合の挙動など、HTTP配信して初めて確認できる部分があるからです。単一HTMLで動いたからWeb版も同じ、localhostで動いたからダブルクリックでも同じ、とは限りません。
同じHTMLなのに動作が違うときは、コードだけでなくアドレスバーも確認します。file://なのか、http://localhostなのか、公開されたhttps://なのか。Webアプリでは、その違いも実行環境の一部です。
Developer Toolbox
Base64、JSON、JWT、cron、正規表現、Hashなど33種類の開発者向けユーティリティを1つにまとめたツールです。
注意点
- 『file://で開ける』と『完全ローカル処理』は同じ意味ではありません。file://のHTMLでも、CORSなど相手側の条件を満たせば外部URLへ通信するコードを実行できます。
- localhostは通常自分の端末へ戻るループバックですが、ページ内の外部通信まで止める仕組みではありません。NetworkパネルやCSPなどで別に確認します。
- 単一HTML化で別ファイル読み込みの問題は減らせますが、API自体がhttp: / https:や特定のOrigin条件を要求する場合まで回避できるわけではありません。
よくある質問
localhostを使うと、ファイルはインターネットへ送られますか?
localhost上のローカルWebサーバーからHTMLを配信すること自体は、通常その端末内のループバック通信です。ただし、ページ内のJavaScriptが外部APIや外部リソースへ通信すれば、その通信は別に発生します。
file://なら完全ローカル処理ですか?
いいえ。file://はHTMLをローカルファイルから開いていることを示しますが、そのページが外部へ通信しないことまでは保証しません。完全ローカル処理かどうかは、ユーザーデータと実行時通信の実際の経路で判断します。
file://もSecure Contextなら、なぜService Workerが使えないのですか?
Secure Contextは条件の一つだからです。ServiceWorkerContainer.register()にはさらにURL schemeの条件があり、script URLやscope URLはhttp:またはhttps:である必要があります。localhostは開発用の信頼できるHTTP Originとして扱えます。
単一HTMLなら必ずfile://で動きますか?
必ずではありません。別ファイルへのfetchやimportを減らせるので動きやすくなりますが、利用するWeb API自体にOrigin、scheme、権限、ユーザー操作などの条件があれば、その制約は残ります。
開発ではfile://とlocalhostのどちらを使えばよいですか?
Webサイトとして動かすものはlocalhostで確認するのが基本です。ダウンロードして直接開く単一HTMLも提供するなら、localhostだけでなくfile://でも主要機能を別途確認するのが安全です。
参考資料
file URLのOrigin、JavaScript Module、Secure Context、Service Worker、localStorage、HTTP Range Requestについては、以下の仕様・公式ドキュメントを参照しています。Browser Kitty固有の記述は、v168時点のローカル開発サーバーと単一HTML運用で実際に確認した内容です。
- MDN Web Docs 同一オリジンポリシー — ファイルのオリジン
- MDN Web Docs JavaScript モジュール
- W3C Secure Contexts
- MDN Web Docs ServiceWorkerContainer: register() メソッド
- MDN Web Docs Window: localStorage プロパティ
- MDN Web Docs HTTP 範囲付きリクエスト
- MDN Web Docs ローカルテストサーバーを設定するには?