はじめに
React + Viteで作ったアプリケーションを、いちいちサーバーを立ち上げずにローカルファイルとして開きたい...
Finderから直で開けたら最高なのに...
と思ったReact初心者による記事です。
作者は初心者なので、内容に間違いがある可能性があります。
この情報は鵜呑みにせず、一度自分で調べることをお勧めします。
急いでいる方はまとめまで飛ばしてください。
環境
ランタイムとパッケージマネージャにはBunを使用しています。
Node.jsやNPMを使っている方は、適宜読み替えてください。
- MacOS Sonoma
- React 18.3.1
- Vite 5.4.0
- Bun 1.1.17
- ファイルの観覧: Vivaldi 6.8.3381.55
やりたいこと
ビルドコマンドを打つと、dist/
にコンパイルされたHTML/CSS/JavaScriptファイルが生成されます。
このコンパイルされたファイルですが、そのまま開くと動きません。
この動かない原因を突き止め、どうにかして正常に動かすのが目標です。
この際、ビルドしたあと手動でファイルを変更するのを想定しています。
エラー内容と対処法
ここからは、発生している各エラーとその対処法を書いていきます。
...とその前に、一度HTMLファイルの中身をおさらいします。
ここではHTML/CSS/JavaScriptのみの構成で、CSSをHTML側で読み込むという構成の場合の例を示します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" crossorigin href="/assets/index-[文字列].css">
<script type="module" crossorigin src="/assets/index-[文字列].js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
ここで注目すべきは以下の3つです。
-
link
タグでバンドルされたCSSを読み込む -
script
タグでバンドルされたJavaScriptファイルを読み込む - それぞれのファイルは
/assets
にあることを前提としている
CORS
Access to script at 'file:///assets/index-[文字列].js' from origin 'null' has been blocked by CORS policy:
Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.
Access to CSS stylesheet at 'file:///assets/index-CT_B-64d.css' from origin 'null' has been blocked by CORS policy:
Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.
これは、ファイルを開いたら真っ白な画面が表示され、違和感を持ってコンソールを開いたら一番上に表示されているエラーです。
エラー内容を見る感じ、どうやらJavaScriptファイルとCSSファイルの読み込み時にエラーが発生しているようです。
原因
このエラーはfile://
プロトコルがクロスオリジンリクエストに対応していないというものみたいです。
通常ならばHTMLからJavaScriptやCSSを読み込むのにクロスオリジンリクエストなんてしないはずですが、いったい何があったのでしょうか?
<link rel="stylesheet" crossorigin href="/assets/index-[文字列].css">
<script type="module" crossorigin src="/assets/index-[文字列].js"></script>
なにやらcrossorigin
というそれっぽい属性がついています。
この属性はCORSポリシーを適切に設定している外部リソースを読み取るときに使うらしいです。
詳しいことは私もよくわからなかったので、MDNのページを見てください。
とりあえず一つだけ言えるのは、file
プロトコルではCORSは使えず、使おうとするとエラーになるということです。
解決策
ということで、crossorigin
属性は消します。
なくてもアクセスはできますし、そんなに問題はないでしょう。多分。
<link rel="stylesheet" href="/assets/index-[文字列].css">
<script type="module" src="/assets/index-[文字列].js"></script>
これでCORSエラーは解決できました。
type="module"
さて、上でCORSエラーは解決したはずなので、もう一度コンソールを覗いてみます。
Access to script at 'file:///assets/index-[文字列].js' from origin 'null' has been blocked by CORS policy:
Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.
CSSのほうのエラーは消えているのですが、JavaScriptのほうな残ってしまっています。
crossorigin
属性は消したのに...いったいなぜでしょうか?
原因
file
プロトコルではscript
タグのtype="module"
が使えないのが原因です。
どうやらJavaScriptのセキュリティ要件を満たさないらしく、CORSエラーが発生するようになっています。
詳細はこちらをご覧ください。
ちなみに、この現象はローカルファイルでimport/export
を試そうとしたときにも起こります。
解決策
ということで、tyep="module"
は消します。
<link rel="stylesheet" href="/assets/index-[文字列].css">
<script src="/assets/index-[文字列].js"></script>
これで今度こそCORSエラーが解決できました。
ちなみに、type="module
属性を消したことによってJavaScriptのコード内でimport/export
が使えなくなりました。
といっても、適切にNPMパッケージを使用していればパッケージの内容ごとバンドルされるはずなので、そこまで問題ではないでしょう。
ただし、コード内でURLを使用したimport
を行っている場合は注意が必要かもしれません。(未検証)
CDNを利用しているなど、あり得ない話ではないと思います。
その場合は以下の方法を取ることができます。
- NPMパッケージに置き換える(パッケージがある場合)
- HTMLファイル内に
script
タグとして書く - 諦めてローカルサーバーを使う
script
タグとして書く場合、モジュールになっているものではなくグローバル変数として定義するものがあるなら、そのURLのscript
タグを書くだけで大丈夫です。
それもない場合、script
タグでimport
してグローバル変数にするという手法を取ることができると思われます。(未検証)
<script type="module">
import * as Module from 'ここにURL'
window.Module = Module
</script>
これは、CORSエラーが発生するのはtype="module"
とsrc
を併用した時だけだという仕様?を使ったものです。
また、この際JavaScript内にあるimport
宣言を削除する必要があります。
import
はファイルの先頭にあるはずです。
パス
CORSのエラーが解決できたので、もう一度コンソールを開いてみます。
GET file:///assets/index-[文字列].js net::ERR_FILE_NOT_FOUND
GET file:///assets/index-[文字列].css net::ERR_FILE_NOT_FOUND
だそうです。
原因
どうやらアセットをfile://assets
から読み込もうとして、ファイルが見つからず404エラーになっているようです。
パスの設定が間違っているみたいですね。
パスが適切に設定されている場合、こちらのエラーは起こりません。
この問題に関しては、複数のアプローチを取ることができます。
解決策1: 手動で直す
特に何も考えず、パスを手動で変更すると直ります。
具体的には、パスの始まりを/
ではなく./
にすればOKです。
<link rel="stylesheet" href="./assets/index-[文字列].css">
<script src="./assets/index-[文字列].js"></script>
解決策2: ベースパスの設定を変更する
こちらの内容は実際に試していません。
おそらくこの方法でも大丈夫たと思いますが、もし無理だったらすみません。
こちらの問題ですが、ベースパスの設定を./
に変更しても直すことができます。
ベースパスの設定を./
にした場合、アセットファイルのパスが./
から始まるようになるみたいです。
import { defineConfig } from 'vite'
// プラグインなどの読み込み
export default defineConfig({
// 他の設定
base: './'
})
また、設定ファイルを変更しなくても、ビルドコマンドにbase
オプションをつけてもいけます。
Bunの場合、bun run build --base=./
でビルドする他、package.json
のbuild
スクリプトの箇所を変更することもできます。
ビルドをやり直す場合、こちらの手順はCORSからやり直しになります。
といってもcrossorigin
とtype="module"
を消すだけです。
こちらを参考にしました。
実行タイミング
これでパスの問題が解決できたはずなので、再びブラウザをのぞいてみます。
今度はCSSがうまく読み込めているはずです。
ですが、JavaScriptの実行時にエラーが出てしまっています。
Uncaught Error: Minified React error #299; visit https://reactjs.org/docs/error-decoder.html?invariant=299 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
at be.createRoot (index-[文字列].js:40:55426)
at index-[文字列].js:336:4915
メッセージを見る感じ、最小化されたReactエラー#299とやらが発生しているみたいです。
開発環境を使用するとエラーの解決に役立つかもと書かれていますが、そもそも開発環境でそれらしきエラーは発生していませんでした。
とりあえずリンク先のページを見ています。
どうやら、Reactではファイルのバイト数を減らすため、エラーメッセージの全文は表示されないみたいです。
そして、実際に発生しているエラーはこちらになります。
Target container is not a DOM element.
原因
こちらのエラーは、createRoot
で指定された値がDOM要素でないことを示しています。
ひとまずcreateRoot
を呼び出しているmain.tsx
の中身を見てみますが、問題はないように思えます。
import { StrictMode } from "react";
import { createRoot } from 'react-dom/client'
import { App } from "./App";
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
いや待てよ..なんだこの!
は...
getElementById('root')!
の最後にある!
は、TypeScriptで使えるアノテーションです。
これは前の値(document.getElementById('root')
)の値がnull
ではないということを示しています。
そして、HTMLが読み込まれる前に要素を取得しようとしてもnull
になります。
ここでもう一度HTMLを見てみましょう。
<head>
<script src="assets/index-[文字列].js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
div#root
の前にscript
タグがありますね。
原因はこれだと思われます。
ちなみに、ローカルサーバーを使った状態で、ビルドした時の状態でこのエラーが発生しないのには理由があります。
それはtype="module"
の実行タイミングです。
何も属性をつけていない時のscript
タグは、そのタグの位置までHTMLがパースされたら同期的に読み込まれて実行されます。
上のようなHTMLの場合、div#root
が読み込まれる前にJavaScriptが実行されたため、エラーが発生してしまいました。
そして、type="module"
をつけたスクリプトは、非同期に読み込まれてHTMLのパースが終わった後に実行されます。
だからエラーが出なかったんですね。
こちらが非常にわかりやすかったので、詳しくはリンク先をご覧ください。
解決方法
これも解決方法は2通りあります。
一つ目は、script
タグの位置を下にずらす方法です。
HTMLのパースが最後まで終わってからJavaScriptを読み込み/実行するようにします。
<head>
- <script src="assets/index-[文字列].js"></script>
</head>
<body>
<div id="root"></div>
+ <script src="assets/index-[文字列].js"></script>
</body>
</html>
もう一つはdefer
属性をつける方法です。
この属性をつけると、スクリプトの実行をHTMLのパース後まで遅らせることができます。
<head>
<script defer src="assets/index-[文字列].js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
defer
属性についても先ほどの記事がわかりやすかったので、詳しくはリンク先をご覧ください。
これでローカルファイルでもReactアプリが動くようになったと思います。
まとめ
React + Viteで作ったアプリをローカルファイルで開くには、以下の手順を踏みます。
- (パスを設定する)
- ビルドする
-
index.html
を編集する- パスを修正する(1で行っていない場合)
-
crossorigin
属性とtype="modules"
属性を消す
パスを修正するには以下の方法があります。
-
vite.config.js
にbase: './'
を指定する(ビルド前) - コマンドの
base
オプションに./
を渡す -
index.html
のパスを手動で編集する(ビルド後)
vite.config.js
のパスの設定例はこちらです。
import react from "@vitejs/plugin-react";
export default {
plugins: [react()],
base: './',
}
これにより、パスが./
で始まるようになります。
ちなみに、設定なしだと/
から始まるので、ビルドのたびに手動で変更する必要があります。
<link rel="stylesheet" crossorigin href="./assets/index-[文字列].css">
<script type="module" crossorigin src="./assets/index-[文字列].js"></script>
あとは、ここからcrossorigin
とtype="module"
を消し、script
にdefer
属性をつけたら、ローカルファイルでもアプリが動くようになります。
<link rel="stylesheet" href="./assets/index-[文字列].css">
<script defer src="./assets/index-[文字列].js"></script>
defer
をつける代わりに、script
タグを</body>
の前に移動することもできます。