Electronのproduction環境でasarにある画像などのリソースがnet::ERR_FILE_NOT_FOUNDで読み込めないのはBrowserRouterが原因でした

環境

  • Electron 2
  • Electron Builder 20
  • React: 16
  • webpack 4
  • React Router 4
  • Gentoo Linux

やりたいこと

url-loaderを排除してfile-loaderにしたい.

動機

とある画面を読み込むのがとても遅く,速くしたかった.

同じ画像を大量に読み込むので,url-loaderを削除してfile-loaderにしてみたらとても速くなった.

パスを変更すると読み込めなくなる問題も

loader: 'file-loader',
options: {
  publicPath: '/'
},

すれば解決しました.

問題

development環境ではちゃんと動きます.

しかし何故かproduction向けにビルドして実行ファイルを作ると読み込めません.

Failed to load resource: net::ERR_FILE_NOT_FOUND

になってしまいます.

asar l app.asarするとちゃんと画像ファイルがバンドルされていることが分かります.コンソール出力でパスも一致していることが確認できます.

同じパスに存在するJavaScriptファイルは読み込めているのになんで画像は読み込めないんだ?

結論

BrowserRouterじゃなくてHashRouter使えば解決です.

以下は作業ログなので纏まってないですし読まなくて良いです.

探索

などをものすごく読みましたがジャストで解決する問いと答えはありませんでした.

問題の再現を試みる

これはchentsulin/electron-react-boilerplate: Live editing development on desktop appを少し弄って画像が出力されないことを確認して,それを参考ソースにしてStack Overflowで質問するかと思って,electron-react-boilerplateを少し弄ってfile-loaderを使用するようにして,パッケージングして実行しました.

Hello Electron React
Hello Electron React

画像埋め込めてますね…我々の設定がおかしいのでしょう.

試行

どこかのissueにpackage.jsonのfilesを設定しろとか書いていた気がします.electron-react-boilerplateの設定の違いはそこにある気がします.

さっそくelectron-builderのfilesの意味を調べてみましょう.

Application Contents · electron-builder

えっ英語多すぎてつら…

どうも読んだ所ファイルにアクセスできるかには影響しない気がするのですが.追加するかどうかには影響する気がしますが,実際にasarには追加されていてアクセスが出来ないので何か違う気がします.

適当にfilesを弄ってもビルドエラーが出ますね.

何もわからないのでとりあえずelectron-react-boilerplateのfilesをコピーしてみることにします.

ビルドエラーが出ます.

どうもfilesにプロパティを追加していくとそれがasarにバンドルされるらしいです.とりあえず全てバンドルしてみましょう.

全てバンドルしてみましたが表示されませんでした.まあ当然ですね.元からバンドルされていることは確認していてそれでも表示されないのがこの問題なのですから.

ドキュメントにも無視するファイルがない場合は変更する必要ないと書いてありますし.

これが原因だと思った私の勘が間違っていたのでしょう.

node: {
  __dirname: false,
  __filename: false,
},

が悪いのかなと思って削除してみます.

削除してみたらindex.htmlすら読み込めなくなりました.

この設定を共通設定からrendererの設定だけに移してみましょう.これもindex.htmlが読み込めなくなりますね.

file-loaderのソースコードを読んでみます.んー__dirname__pathnameも使ってないですね.

file-loaderのnameオプションでファイル名が固定になったので,importを使わずに直にパスを打ち込みまくってみます.全部だめで読み込めない.そりゃパス名が同じことは既に確認しているんですから当然ですね.

どうやっても出来ない…と思いたいんですが,electron-react-boilerplateは実際出来てるんですから謎なんですよね…

プロトコルの不一致が原因?

win.loadURL(
  url.format({
    pathname: path.resolve(__dirname, 'index.html'),
    protocol: 'file:',
  })

を単純な

win.loadURL(`file://${__dirname}/index.html`);

に書き換えましたがダメ.関係がない.

アップデート処理に失敗して一度404が起きているからそこでロードが止まっている?一度コメントアウトしてアップデート処理を消してみます.関係なし.

file-loaderが関係しているのか切り出す必要がありそうです.CopyWebpackPluginでimageをコピーして直パスでアクセスしてみます.

変化なし.まあパスはちゃんと問題なくて,electron-react-boilerplateはfile-loaderでアクセスできているのですから当然ですね.

webpack production url-loader limit path error · Issue #1156 · chentsulin/electron-react-boilerplateを参考にしてoutputにpublicPathを設定してみます.

これはdistに出力する環境でやっているのですからこちらはappに出力するのでappに設定しなければいけないのではないか.ダメっぽい.

electron-react-boilerplateはproduction環境では__dirnameを弄っていないようですね.こちらもそれでindex.htmlが表示されるようにしてみましょう.元々設定していたのはDevtronが設定しないとバグるというのが原因だったのでproduction環境では問題ないはずです.

Not allowed to load local resource: file://index.html/ after webpacking main.js · Issue #5107 · electron/electronを参考にindex.htmlが読み込まれないのを書き換えていきます.

webPreferences: {
  webSecurity: false,
},

してみるとなるほどエラーは消えます.ウィンドウは何も描画せず何も起きなくなりました.表示されないのはoutput設定を行っていたからのようですね.webSecurity: falseにしても画像が読み込まれないのは変わらないけれど.

もう一度file-loaderを疑って,electron-react-boilerplateの真似をすれば良いのではないかと思い,electron-react-boilerplateの出力するファイルパスを真似たものを生で突っ込んでみます.

勿論どちらもnet::ERR_FILE_NOT_FOUNDです.

Importing Images in Electron React Boilerplate - daviseford

を見て絶対パス設定だとダメなのかな…と思いimportを相対パス設定に切り替えてみます.勿論それで結果のパスが変わるはずもなくエラー.

electron-react-boilerplateが./dist/じゃなくて/dist/だと画像読み込まないことを確認して,locationを確認.

Location {href: "file:///tmp/.mount_electrs7TXt2/app/resources/app.asar/app.html#/", ancestorOrigins: DOMStringList, origin: "file://", replace: ƒ, assign: ƒ, …}

私のアプリケーションではlocationはmount以下のhtmlではなく

Location {href: "file:///", ancestorOrigins: DOMStringList, origin: "file://", replace: ƒ, assign: ƒ, …}

になっていることを確認.

これを上に合わせればパスが合うのではないか.

どうやって合わせる?loadURL__dirnameを使う方法は既に失敗したぞ?falseにするのと合わせて試してはない.

__dirname: falseすることでまた何も読み込まれなくなってしまいました.

mainとrendererの間で__dirnameの値が異なったりするのか?と思ったので確認してみます.

webpackの設定を見るとmain側は__dirname: falseにしていてrendererはしていないように見えますね.双方のrendererにconsole.log(__dirname);を仕込んで確かめてみましょう.どっちもrendererで__dirname/でした.そこに違いはないんですね…

entryが複数あるからいけないのか?向こうに合わせて1つにしてappを指すようにしてみます.mainプロセスがwindowオブジェクトを参照するとかでエラーが出てきました.参照しているはずが無いんですが…winstonを参照しているのが悪いのかなと思って削除してみました.何も出てこなくなりました…まあ考えてみたらelectron-react-boilerplateではappディレクトリに元ソース入れてるんですから,我々のwebpackではsrcをentryに指定しないとダメですね.HtmlWebpackPluginが複数ある以上entryは複数ないとダメなのでここは変えられないですね.

publicPathの指定が関係する?関係なかったです.

output.pathjoinではなくresolveで解決しているから正規化された?関係なかったです.

わからん

locationを/ではなく,AppImageがmountされたディレクトリにする必要があることはわかってきましたが,その方法がわかりません.

React Routerが原因っぽい

会社出て歩き始めてから気がついたんですが,originがfile://の状態で/のルーティングに飛ばしたらそりゃfile:///になりますよね.

HashRouter使えばパス移動発生しない

HashRouter使いましょう.どうせElectronなのでURLが汚くなるとか関係ないし,戻る機能すら使っていません.

HashRouter使えばpublicPathも設定不要.

自決しました

私がこれに気がつくのに12時間ぐらいかかりました.これだけをやっていたわけではないとはいえ…生産性低すぎませんかね.つらい.自決したい.

要約

AppImageは実行する時にfile:///tmp/.mount_electrs7TXt2/app/resources/app.asar/app.htmlのようにasarのファイルを展開します.

React Routerで/に飛ばすとoriginはfile://なので当然file:///に飛んで行きます.

すると相対パスで展開されたファイルは読み込めなくなります.

ElectronでReact Routerやめませんか

これはただの負け惜しみなんですけど,webサイト作る訳でもないのにURL作っても仕方がなくないですか.コンポーネント遷移は単純にReduxに状態を突っ込んでしまえば良いじゃないですか.構造的にパラメータも入れられますし.

このエントリーをはてなブックマークに追加 fb-like g-plusone pocket