Closed20

Electron の main process と renderer process とのやりとりについてメモ

nbstshnbstsh

Inter-Process Communication

IPC (Inter-Process Communication) という言葉が出てくるが、これは main process と renderer process とのやりとりのこと

https://www.electronjs.org/docs/latest/tutorial/ipc

nbstshnbstsh

IPC の種類

  1. renderer → main
  2. renderer → main → renderer
  3. main → renderer

それぞれ見ていく

nbstshnbstsh

どのファイルで何をやるのか

大雑把に整理すると、3つの領域でコードを書いていく

  • main.js で main process 側の処理を書く
  • UI (web frontend) 側で renderer 側の処理を書く
  • preload.js で main と renderer を繋ぐための処理を書く
nbstshnbstsh

1. renderer → main

renderer から main の処理を呼び出すパターン

https://www.electronjs.org/docs/latest/tutorial/ipc#pattern-1-renderer-to-main-one-way

nbstshnbstsh

preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronApi', {
  setTitle: (title) => ipcRenderer.send('set-title', title)
})

main.js

  ipcMain.on('set-title', (event, title) => {
    const webContents = event.sender
    const win = BrowserWindow.fromWebContents(webContents)
    win.setTitle(title)
  })

renderer (React)

const ChangeWindowTitle: React.FC = () => {
  const [title, setTitle] = useState('');

  return (
    <div>
      <input value={title} onChange={(e) => setTitle(e.target.value)} />

      <button
        onClick={() => {
          window.electronApi.setTitle(title);
        }}
      >
        Set title
      </button>
    </div>
  );
};
nbstshnbstsh

何をやっているのか

preload.js

window に main.js 側の処理を呼び出すための api を生やしている。
ipcRenderer.send で "main.js 側の event 処理を発火させている" イメージ。

ipcRenderer.send('set-title', title)

"set-title" という event を send している

main.js

renderer から呼び出したい処理を定義する。

  ipcMain.on('set-title', (event, title) => {
    const webContents = event.sender
    const win = BrowserWindow.fromWebContents(webContents)
    win.setTitle(title)
  })

'set-title' event が呼び出された時に、引数で渡された title の値に browser window の title を変更している。

renderer

あとは、renderer から window に生やした api を通じて main.js 側の処理を呼び出す。

      <button
        onClick={() => {
          window.electronApi.setTitle(title);
        }}
      >
        Set title
      </button>
nbstshnbstsh

2. renderer → main → renderer

renderer から main の処理を呼び出して、その結果を main から renderer に返すパターン

https://www.electronjs.org/docs/latest/tutorial/ipc#pattern-2-renderer-to-main-two-way

nbstshnbstsh

preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronApi', {
  openFile: () => ipcRenderer.invoke('dialog:openFile')
})

main.js

  ipcMain.handle('dialog:openFile', async () => {
    const { canceled, filePaths } = await dialog.showOpenDialog({});
    if (!canceled) {
      return filePaths;
    }
  });

renderer (React)

const SelectFile = () => {
  const [filePath, setFilePath] = useState('');

  return (
    <div>
      <div>Selected File: {filePath ?? 'None'}</div>
      <button
        onClick={async () => {
          const filePath = await window.electronApi.openFile();
          setFilePath(filePath);
        }}
      >
        Open
      </button>
    </div>
  );
};
nbstshnbstsh

何をやっているのか

preload.js

window に main.js 側の処理を呼び出すための api を生やしている。
ipcRenderer.invoke はより直感的に "呼び出す" ための api 感がある。

ipcRenderer.invoke('dialog:openFile')

"dialog:openFile" という処理を invoke している。

main.js

renderer 側から invoke で呼び出せる処理を定義している。
ここで return した値を renderer 側へ受け渡すことができる。

  ipcMain.handle('dialog:openFile', async () => {
    const { canceled, filePaths } = await dialog.showOpenDialog({});
    if (!canceled) {
      return filePaths;
    }
  });

"dialog:openFile" では、file 選択 dialog を表示して、選択された file の path を返している。

renderer (React)

あとは、renderer 側から呼び出してあげればOK。
main.js 側から return された値を戻り値として取得できる。

      <button
        onClick={async () => {
          const filePath = await window.electronApi.openFile();
          setFilePath(filePath);
        }}
      >
        Open
      </button>
nbstshnbstsh

3. main → renderer

今度は、逆に main から renderer 側の処理を発火するパターン。

https://www.electronjs.org/docs/latest/tutorial/ipc#pattern-3-main-to-renderer

nbstshnbstsh

preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
 onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
})

main.js

  const menu = Menu.buildFromTemplate([
    {
      label: app.name,
      submenu: [
        {
          click: () => mainWindow.webContents.send('update-counter', 1),
          label: 'Increment'
        },
        {
          click: () => mainWindow.webContents.send('update-counter', -1),
          label: 'Decrement'
        }
      ]
    }

  ])
  Menu.setApplicationMenu(menu)

renderer (React)


const MenuCounter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    window.electronApi.onUpdateCounter((event, value) => {
      setCount((v) => v + value);
    });
  }, []);

  return (
    <div>
      <div>Count: {count}</div>
    </div>
  );
};
nbstshnbstsh

何をやっているのか

preload.js

ipcRenderer.on で main 側から送信される event に callback を登録できる関数を生やす。

  onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)

これで main 側から "update-counter" event が発火された場合、renderer 側の callback が呼び出される。

main.js

Desktop アプリの Menu を作成している。

Menu をクリックした際に、mainWindow.webContents.send('update-counter', 1) が呼び出される。これで、renderer 側に "update-counter" event が送信される。

  const menu = Menu.buildFromTemplate([
    {
      label: app.name,
      submenu: [
        {
          click: () => mainWindow.webContents.send('update-counter', 1),
          label: 'Increment',
        },
        {
          click: () => mainWindow.webContents.send('update-counter', -1),
          label: 'Decrement',
        },
      ],
    },
  ]);

renderer (React)

useEffect 内で "update-counter" event の際に発火される callback を登録している。
シンプルに、main 側から送信された値を元に counter の値を変化させている。

useEffect(() => {
    window.electronApi.onUpdateCounter((event, value) => {
      // increment の場合は value は 1
      // decrement の場合は value は -1
      setCount((v) => v + value);
    });
  }, []);
nbstshnbstsh
nbstshnbstsh

main → renderer → main

main から renderer 側の処理を発火したのち、renderer 側から main に結果を返したいケース。

やることとしては、1. renderer → main と同じで、

  • renderer 側から main へ event を発火
  • ipcMain.on で main 側で実行したい処理を実装

renderer

const counter = document.getElementById('counter')

window.electronAPI.onUpdateCounter((event, value) => {
  const oldValue = Number(counter.innerText)
  const newValue = oldValue + value
  counter.innerText = newValue
  event.sender.send('counter-value', newValue)
})

event.sender.send で ``ipcRenderer.send` 同様に main 側へ event を送信できる。

main.js

main 側で renderer 側から発火したい処理を用意しておく。

// ...
ipcMain.on('counter-value', (_event, value) => {
  console.log(value) // will print value to Node console
})
// ...
このスクラップは2023/10/11にクローズされました