3

I'm having trouble getting images to load in Electron consistently. I'm using Electron Forge with the webpack template https://www.electronforge.io/templates/webpack-template

My src directory looks like this:

├── images
│   └── black.png
├── index.css
├── index.html
├── index.js
├── main.js
└── renderer.js

My HTML code looks like this:

<img src="images/black.png">

I'm using copy-webpack-plugin to copy the images directory.

When running in development (npm run start) the root of the dev server is .webpack/renderer so the image loads. When running in production (npm run package) the HTML file is being opened from the file system so the image tag is trying to access .webpack/renderer/main_window/images which is the wrong location and it doesn't load.

I have gotten it to work in both development and production by doing:

<img src="../images/black.png">

This is a hacky way and isn't correct to the way the files are stored in the src directory. This is something that should be simple but I've spent hours trying to figure it out and don't have a real solution yet.

I've seen a solution expressed in these links but I could not get it to work in both development and production without putting "../" in front of the path.

https://github.com/electron-userland/electron-forge/issues/1196

https://github.com/electron-userland/electron-forge/issues/941

I can think of a few ways to solve this:

  1. The webpack config needs to know via some kind of environment variable or flag if it is running in development or production and change the copy-webpack-plugin's "to" path.
  2. Change the development server to run so its root is .webpack/renderer/main_window

I've seen the recommendation to import the image into renderer.js but I have a few thousand images. Should I do it like this?

import './images/1.png';
import './images/2.png';
import './images/3.png';
// ...
import './images/1000.png';

Is there a way to programmatically import? Something like:

let imageMap = {};
for (let i of Iter.range(1, 1001)) {
  let image = import(`./images/${i}.png`);
  imageMap[i] = image;
}

Then how would I refer to it in HTML? Can it be done without DOM editing code?

I prefer not to import my images into my JavaScript file and run them through webpack loaders. I just want to reference static files from my HTML code in a way that works in both development and production.

I also have a 5MB JSON file that I need to access using fetch(). I tried to import this through a loader but the build process took more than 5 minutes and I killed it.

Loading static files is a basic part of making web pages and should be a part of the project template unless I am missing something super obvious.

hekevintran
  • 22,822
  • 32
  • 111
  • 180
  • I had a similar problem...and just dropped the webpack. That solved a ton of problems, and in my case I did not need the webpack part of it. Only caused issues. For the front end maybe use a single page technology like React or Angular, and then webpack those according to their platform tools. Webpack + Node.js just seems to always bing big issues that are hard to solve. – Vincil Bishop Apr 25 '20 at 21:16
  • Webpack is not the cause of the problem. The problem happens because the people who made electron forge set up the webpack dev server in development mode and have no server at all just static files in the production mode. The configuration provided by electron forge itself is wrong. – hekevintran May 01 '20 at 01:24

5 Answers5

4

For someone who doesn't want to start a server to serve the static resource, here are the steps with the latest version of electron-forge + typescript + webpack + react to use static resources.

Suppose your project has the following file structure:

├── src
|    └── index.ts
|    └── index.html
|    └── App.tsx
|    └── renderer.ts
|    └── assets
|          └── images/a.png, b.png
|          └── fonts/a.ttf, t.ttf
├── forge.config.js
├── webpack.main.config.js
├── webpack.renderer.config.js
├── webpack.plugins.js
├── webpack.rules.js
├── package.json

Note: The configuration for forge was merged to package.json by default, I exact that to a separated file forge.config.js, see here for details.

Step 1.

First we need to register a protocol in src/index.ts(This is the entry point of electron's main process), we call this protocol static, and you would need to use static:// as the prefix of you image resource later.

import { app, BrowserWindow, session } from 'electron';
import path from 'path';

// ...

app.on('ready', () => {
  // Customize protocol to handle static resource.
  session.defaultSession.protocol.registerFileProtocol('static', (request, callback) => {
    const fileUrl = request.url.replace('static://', '');
    const filePath = path.join(app.getAppPath(), '.webpack/renderer', fileUrl);
    callback(filePath);
  });

  createWindow();
});

Step 2.

Then, in webpack.renderer.config.js, use copy-webpack-plugin plugin to copy static resources from src/assets to .webpack/renderer folder

const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');

const assets = ['assets'];
const copyPlugins = new CopyWebpackPlugin(
  {
    patterns: assets.map((asset) => ({
      from: path.resolve(__dirname, 'src', asset),
      to: path.resolve(__dirname, '.webpack/renderer', asset)
    }))
  }
);

Step 3.

Last, in src/App.tsx, write the react code to render an image.

import React from 'react'
import * as ReactDOM from 'react-dom/client'

const root = ReactDOM.createRoot(document.getElementById('app'))
root.render(<img src='static://assets/images/a.png'/>)

Add a new div with id = 'app' in src/index.html file

<body>
  <div id="app"></div>
</body>

and don't forget to reference it in src/renderer.ts

import './App';

Trouble shooting

If you encounter the Content-Security-Policy issue, please put the following line in your forge.config.js.

"plugins": [
    [
        "@electron-forge/plugin-webpack",
        {
            "mainConfig": "./webpack.main.config.js",
            "devContentSecurityPolicy": "default-src 'self' 'unsafe-eval' 'unsafe-inline' static: http: https: ws:", // <--- this line
            // ...
        }
    ]
]

Be careful of unsafe-inline, it might cause security issues, make sure to use a safe source or just remove it if you don't need it.

zdd
  • 8,258
  • 8
  • 46
  • 75
  • Amazing, works great and seems much simpler than spinning up a express server – Jan Schmutz Oct 17 '22 at 13:56
  • The fix for security policy doesn't work for me. I get an error which confuses me: "Multiple plugins tried to take control of the start command, please remove one of them --> webpack, webpack" I did already have other entries under 'plugins', placed there by default from Forge, such as new WebpackPlugin, and another config entry for ''@electron-forge/plugin-auto-unpack-natives'. I guess I just don't understand how to put your config in there with the pre-existing stuff. – Dave Munger Feb 20 '23 at 18:08
  • Okay, looks like the syntax for plugin config has changed. I finally got that straightened out but then I found what appears to be a typo in the example index.ts file. It's missing the final segment on the path ('assets'), so it should look like this: const filePath = path.join(app.getAppPath(), '.webpack/renderer/assets', fileUrl); – Dave Munger Feb 20 '23 at 20:20
2

I was able to solve this by running a static Express server in production serving the renderer directory. I use absolute paths (/images/foo.png) for image tags and can access my static files.

webpack.renderer.config.js

const path = require('path');
const rules = require('./webpack.rules');
const CopyWebpackPlugin = require('copy-webpack-plugin');

const assets = ['data', 'images'];
const copyPlugins = assets.map(asset => {
  return new CopyWebpackPlugin([
    {
      from: path.resolve(__dirname, 'src', asset),
      to: asset
    }
  ]);
});

module.exports = {
  module: {
    rules: [
      ...rules,
    ],
  },
  plugins: [
    ...copyPlugins,
  ]
};

main.js

import { app, BrowserWindow } from 'electron';
import path from 'path';
import express from 'express';

function isDebug() {
  return process.env.npm_lifecycle_event === 'start';
}

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
  app.quit();
}

const createWindow = () => {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 1200,
    height: 740,
  });

  if (isDebug()) {
    // Create the browser window.
    mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
    mainWindow.webContents.openDevTools();
  } else {
    const exApp = express();
    exApp.use(express.static(path.resolve(__dirname, '..', 'renderer')));
    const server = exApp.listen(0, () => {
      console.log(`port is ${server.address().port}`);
      mainWindow.loadURL(`http://localhost:${server.address().port}/main_window/`);
    });
  }
};

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
hekevintran
  • 22,822
  • 32
  • 111
  • 180
2

I also had the issue to load static files in production, but I think I found a way to solve it. We can use electron-is-dev package to check if the app is running in production or not, then we can specify different path for our files. For my case, I was having issue to load the icon for the app, and here is the way to solve it:

import isDev from "electron-is-dev";

const createWindow = () => {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    height: 1000,
    width: 1000,
    webPreferences: {
      nodeIntegration: true,
    },
  });

  mainWindow.setIcon(
    isDev
      ? path.join(__dirname, "../../src/assets/app.ico")
      : path.join(__dirname, "./assets/app.ico")
  );
};
billcyz
  • 1,219
  • 2
  • 18
  • 32
1

I found a hacky way...
I use the webpack url-loader, and this is part of my webpack.renderer.config.js

{
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 20 * 1024, // 20Kb
            outputPath: 'imgs',
            publicPath: '../imgs',
            name: '[name]-[hash:6].[ext]',
            esModule: false
          }
        }
      }
    ]
  }
}

I just put the parent path of outputPath in publicPath, and it works in my app!

wanZzz
  • 11
  • 1
0

Thank you wanZzz for your clue, but i have to make an adjustment by changing the outputPath: '/' and the publicPath;

My webpack.rules is

 {
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    use: {
      loader: "url-loader",
      options: {
        limit: 20 * 1024, // 20Kb
        outputPath: "/",
        publicPath: "/src/assets/images",
        name: "[path][name].[ext]",
        esModule: false,
      },
    },
  },
Abbatyya
  • 11
  • 2