概要
Electronのアプリケーションを開発中に、埋め込み画像を表示しようとして「Not allowed to load local resource」というエラーに遭遇したので、その対策をメモしておきます。
パッケージ化すると対策を打たなくても画像は表示されるかもしれません。
検証したら後日追記すると思います。
結論から言うと、fileプロトコルをやめて、<img src="data:image/png;base64,<画像をbase64化した文字列>"/>
を使って解決しました。
相対パスでの指定の問題(別の問題が発生するため、後述の対策を実施)
Electronアプリは、Reactを使っていてcreate-react-appで作ったアプリで、それをElectron化したものです。
index.htmlの横に、logo.pngを配置、タグで、<img src="../build/logo.png"/>
として表示させようとしていました。
画像は、Webアプリとしては表示されますが、Electron上ではsrc="file:///build/logo.png"
となり、絶対パスで探すようになります。
この対策として、以下のサイトを参考に、protocol.interceptFileProtocol
というAPIで、"file:///%RELATIVE_PATH%" (RELATIVE_PATHはマジックワード)で始まっていれば、相対パスとしてハンドルするよう実装しました。
imgタグの指定は、<img src="%RELATIVE_PATH%../build/logo.png" />
とし、ソースはこんな感じです。
protocol.interceptFileProtocol('file', (req, callback) => {
const requestedUrl = req.url.substr(7);
if (path.startsWith("/%RELATIVE_PATh%") {
callback(path.normalize(path.join(__dirname, requestedUrl.substring("/%RELATIVE_PATh%".length)));
} else {
callback(requestedUrl);
}
この対策は、Macでは動作しましたが、Windowsでは「Not allowed to load local resource」というエラーに遭遇し、動作しませんでした。
Not allowed to load local resource 対策(解決済み)
Googleでいくつか調べましたが、WebsecurityをOffにする、create-react-appをeject(不可逆の操作で、create-react-appの良さが失われる)したり、webpackのコンフィグを変えるなどといったもので、適用しても効果がありませんでした。
- 一番有効そうだった解決策(WebsecurityをOffにする)
https://github.com/electron/electron/issues/5107#issuecomment-299971806 - Webpackの設定変更
https://github.com/electron/electron/issues/5107
結局は、以下に引用した https://stackoverflow.com/questions/50272451/electron-js-images-from-local-file-system に書かれているように、fileプロトコルを使っている限りセキュリティをOffにするなどの解決策しかなく、問題は解決しませんでした。
Electron by default allows local resources to be accessed by render processes only when their html files are loaded from local sources with the file:// protocol for security reasons.
If you are loading the html from any http:// or https:// protocol even from a local server like webpack-dev-server, access to local resources is disabled.
If you loading html pages from a local server only during development and switching to local html files in production, you can disable websecurity during development, taking care to enable it in production.
最終的な解決策
解決した後のフォルダ構成は、以下の通りです。
build/ <= publicとsrcのビルド結果が格納される。
public/index.html
logo.png
src/ ...
libs/ImageLoader.js
main.js <= ElectronのMainプロセスの起動ファイル
解決のためのステップは以下の通りです。
- main.jsで、imagesフォルダ以下のpng/jpgファイルをbase64化し、globalの変数に詰める。
- ImageLoader.jsクラスを作成し、srcに渡す文字列を生成する。
- jsxで、imgタグに、step2で生成した文字列を渡す。
ソースは以下の通り。
- 画像ファイルを捜査、globalマップに詰めるところまで。
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const url = require('url')
const fs = require('fs')
const info = {
win: null,
images: {}
}
global.info = info
let win
function createWindow () {
win = new BrowserWindow({width: 800, height: 600, resizable:true})
info.win = win
// images以下のフォルダを捜査
const files = fs.readdirSync(path.join(__dirname, '/../build/images'))
files.forEach((file)=>{
const ext = path.extname(file)
if(ext === '.png' || ext === '.jpg') {
const binary = fs.readFileSync(path.join(__dirname, '/../build/images/' + file))
// base64化
const base64data = new Buffer(binary).toString('base64')
// マップに詰める
info.images[file] = {
type: ext.substring(1),
name: file,
data: base64data
}
}
})
// 通常のElectronのメイン処理
win.loadURL(url.format({
pathname: path.join(__dirname, '/../build/index.html'),
protocol: 'file:',
slashes: true
}))
// 以下省略・・・
- ImageLoader.js(グローバルマップから、data:image/png,base64,... を生成)
const electron = window.require('electron')
//globalを取得するには、electron.remoteで取得する
const remote = electron.remote
class ImageLoader {
static loadSrc = (imagePath) => {
const info = remote.getGlobal('info')
const image = info.images[imagePath]
for(let key in info.images) {
console.log(key + " " + info.images[key].type + " " + info.images[key].name)
}
if(image === undefined)
return "image not found"
// data:image/<png|jpg>;base64,<base64 encoded data>の生成
return `data:image/${image.type};base64,${image.data}`
}
}
export default ImageLoader
- 画像表示させるところ
import React from 'react'
import ImageLoader from '../libs/ImageLoader'
/* アプリケーション画面の定義 */
const App = () => (
<div>
<img src={ImageLoader.loadSrc('logo.png')} height="64px" width="64px"/>
// 以下省略・・・
さいごに
対策の備忘録として記載しています。
他の方法もあるかもしれませんので、ご存知でしたらお知らせいただければ幸いです。