0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React + Viteで作ったアプリをローカルファイルで開きたい

Posted at

はじめに

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側で読み込むという構成の場合の例を示します。

dist/index.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

エラー1
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.
エラー2
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を読み込むのにクロスオリジンリクエストなんてしないはずですが、いったい何があったのでしょうか?

dist/index.html
<link rel="stylesheet" crossorigin href="/assets/index-[文字列].css">
<script type="module" crossorigin src="/assets/index-[文字列].js"></script>

なにやらcrossoriginというそれっぽい属性がついています。

この属性はCORSポリシーを適切に設定している外部リソースを読み取るときに使うらしいです。
詳しいことは私もよくわからなかったので、MDNのページを見てください。

とりあえず一つだけ言えるのは、fileプロトコルではCORSは使えず、使おうとするとエラーになるということです。

解決策

ということで、crossorigin属性は消します。
なくてもアクセスはできますし、そんなに問題はないでしょう。多分。

dist/index.html
<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"は消します。

dist/index.html
<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のエラーが解決できたので、もう一度コンソールを開いてみます。

エラー1
GET file:///assets/index-[文字列].js net::ERR_FILE_NOT_FOUND
エラー2
GET file:///assets/index-[文字列].css net::ERR_FILE_NOT_FOUND

だそうです。

原因

どうやらアセットをfile://assetsから読み込もうとして、ファイルが見つからず404エラーになっているようです。
パスの設定が間違っているみたいですね。

パスが適切に設定されている場合、こちらのエラーは起こりません。

この問題に関しては、複数のアプローチを取ることができます。

解決策1: 手動で直す

特に何も考えず、パスを手動で変更すると直ります。
具体的には、パスの始まりを/ではなく./にすればOKです。

dist/index.html
<link rel="stylesheet" href="./assets/index-[文字列].css">
<script src="./assets/index-[文字列].js"></script>

解決策2: ベースパスの設定を変更する

こちらの内容は実際に試していません。
おそらくこの方法でも大丈夫たと思いますが、もし無理だったらすみません。

こちらの問題ですが、ベースパスの設定を./に変更しても直すことができます。
ベースパスの設定を./にした場合、アセットファイルのパスが./から始まるようになるみたいです。

vite.config.js
import { defineConfig } from 'vite'
// プラグインなどの読み込み

export default defineConfig({
    // 他の設定
    base: './'
})

また、設定ファイルを変更しなくても、ビルドコマンドにbaseオプションをつけてもいけます。
Bunの場合、bun run build --base=./でビルドする他、package.jsonbuildスクリプトの箇所を変更することもできます。

ビルドをやり直す場合、こちらの手順はCORSからやり直しになります。
といってもcrossorigintype="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を見てみましょう。

dist/index.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を読み込み/実行するようにします。

dist/index.html(一部略)
<head>
- <script src="assets/index-[文字列].js"></script>
</head>
<body>
  <div id="root"></div>
+ <script src="assets/index-[文字列].js"></script>
</body>
</html>

もう一つはdefer属性をつける方法です。
この属性をつけると、スクリプトの実行をHTMLのパース後まで遅らせることができます。

dist/index.html(一部略)
<head>
  <script defer src="assets/index-[文字列].js"></script>
</head>
<body>
  <div id="root"></div>
</body>
</html>

defer属性についても先ほどの記事がわかりやすかったので、詳しくはリンク先をご覧ください。


これでローカルファイルでもReactアプリが動くようになったと思います。

まとめ

React + Viteで作ったアプリをローカルファイルで開くには、以下の手順を踏みます。

  1. (パスを設定する)
  2. ビルドする
  3. index.htmlを編集する
    1. パスを修正する(1で行っていない場合)
    2. crossorigin属性とtype="modules"属性を消す

パスを修正するには以下の方法があります。

  • vite.config.jsbase: './'を指定する(ビルド前)
  • コマンドのbaseオプションに./を渡す
  • index.htmlのパスを手動で編集する(ビルド後)

vite.config.jsのパスの設定例はこちらです。

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>

あとは、ここからcrossorigintype="module"を消し、scriptdefer属性をつけたら、ローカルファイルでもアプリが動くようになります。

<link rel="stylesheet" href="./assets/index-[文字列].css">
<script defer src="./assets/index-[文字列].js"></script>

deferをつける代わりに、scriptタグを</body>の前に移動することもできます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?