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?

Electron 同期的通信と非同期的通信

Last updated at Posted at 2024-12-12

ipcの同期、非同期通信

ネットをうろちょろしてsend、sendsync、invokeでできそうと思って、色々試してみたけど上手く動作しなかったので、ChatGPTさんに聞いたらサクッと解決した(笑)
優秀すぎて震える。
なので備忘録として残す。

同期的通信

非同期通信はいろいろ紹介されているけど同期的な通信がわからなかった。
そこでChatGPTさんにいろいろ質問してみたけど、質問の仕方が悪かったのか、断片的であまりはっきりした回答が得られないので、思い切って

ウインドウにボタンを配置し、そのボタンを押すと「ping」をmain.jpに送信し、main.jpはその返信ろして「pong」を返し、それをウインドウのtextエリアに表示する完全なプログラムを定時してください。なお、sendSyncを使い同期的な通信で実装してください。

と丸投げ的に質問、誤字はそのまま(笑)

で、この質問で完全に動作するプロジェクトが表示されました(動作確認ずみ)。優秀すぐる。

main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

let mainWindow;

app.on('ready', () => {
  mainWindow = new BrowserWindow({
    width: 400,
    height: 300,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'), // preloadスクリプトを指定
    },
  });

  mainWindow.loadFile('index.html'); // HTMLファイルをロード
});

// 同期通信の処理
ipcMain.on('sync-channel', (event, args) => {
  console.log('Received message from renderer:', args); // ログ出力
  if (args === 'ping') {
    event.returnValue = 'pong'; // pongを返信
  } else {
    event.returnValue = 'unknown command'; // その他のコマンドに対応
  }
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');

// 安全なAPIをレンダラープロセスに公開
contextBridge.exposeInMainWorld('electronAPI', {
  sendSync: (channel, data) => {
    const validChannels = ['sync-channel']; // 許可するチャンネルを制限
    if (validChannels.includes(channel)) {
      return ipcRenderer.sendSync(channel, data);
    }
    return null;
  },
});

ipcRenderer.sendSyncで送信して、ipcMain.onで受ける。
返信時のデータはevent.returnValueに入れるわけか。
送信の関数はcontextBridgeに記述して、renderer.jsから呼び出す。
ふむふむ、勉強になるなぁ。

renderer.js
// ボタンのクリックイベントリスナーを設定
document.getElementById('sendPingButton').addEventListener('click', () => {
  // メインプロセスに「ping」を送信し、返信を受け取る
  const response = window.electronAPI.sendSync('sync-channel', 'ping');
  
  // テキストエリアに返信を表示
  const textArea = document.getElementById('responseArea');
  textArea.value = response;
});
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Electron Sync Example</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      text-align: center;
      padding: 20px;
    }
    textarea {
      width: 100%;
      height: 100px;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h1>Electron Sync Communication</h1>
  <button id="sendPingButton">Send Ping</button>
  <textarea id="responseArea" readonly placeholder="Response will appear here...">
  </textarea>
  <script src="renderer.js"></script>
</body>
</html>

完璧!
これ見て、正直なところやっとcontextBridgeの書き方を理解したw
今までそこの記述がいまいちわからなかったので上手く動作しなかったのだ。

で、比較のため、非同期通信も教えてもらった

非同期通信

質問はこちら

これを非同期通信で実装するように書き直した完全なプログラムを提示してください。

main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

let mainWindow;

app.on('ready', () => {
  mainWindow = new BrowserWindow({
    width: 400,
    height: 300,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'), // preloadスクリプトを指定
    },
  });

  mainWindow.loadFile('index.html'); // HTMLファイルをロード
});

// 非同期通信の処理
ipcMain.handle('async-channel', async (event, args) => {
  console.log('Received message from renderer:', args); // ログ出力
  if (args === 'ping') {
    return 'pong'; // pongを返信
  }
  return 'unknown command'; // その他のコマンドに対応
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');

// 安全なAPIをレンダラープロセスに公開
contextBridge.exposeInMainWorld('electronAPI', {
  invoke: async (channel, data) => {
    const validChannels = ['async-channel']; // 許可するチャンネルを制限
    if (validChannels.includes(channel)) {
      return await ipcRenderer.invoke(channel, data);
    }
    return null;
  },
});

ipcRenderer.invokeで送信して、ipcMain.handleで受ける。
返信時のデータはreturnに書く。
ほうほう、なるほど、なるほど。

送信の関数はcontextBridgeに記述して、renderer.jsから呼び出す。これは同じ。

renderer.js
// ボタンのクリックイベントリスナーを設定
document.getElementById('sendPingButton').addEventListener('click', async () => {
  // メインプロセスに「ping」を送信し、返信を非同期に受け取る
  const response = await window.electronAPI.invoke('async-channel', 'ping');
  
  // テキストエリアに返信を表示
  const textArea = document.getElementById('responseArea');
  textArea.value = response;
});
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Electron Async Example</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      text-align: center;
      padding: 20px;
    }
    textarea {
      width: 100%;
      height: 100px;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h1>Electron Async Communication</h1>
  <button id="sendPingButton">Send Ping</button>
  <textarea id="responseArea" readonly placeholder="Response will appear here..."></textarea>
  <script src="renderer.js"></script>
</body>
</html>

こちらも完璧。
invokeを使った非同期通信になってる。

同期vs非同期

このままでは、ボタンを押しても即座に反応があるので、同期なのか非同期なのか判断できない。
そこで単純にテキストエリアに文字を書き込むボタンを追加するとともに、main.jsの通信部分の関数に時間がかかる処理を追加して検証してみた。

index.html
.
.
<body>
  <h1>Electron Sync Communication</h1>
  <button id="testButton">test</button>
  <button id="sendPingButton">Send Ping</button>
  <textarea id="responseArea" readonly placeholder="Response will appear here..."></textarea>
  <script src="renderer.js"></script>
</body>
.
.

renderer.js
// ボタンのクリックイベントリスナーを設定
document.getElementById('sendPingButton').addEventListener('click', () => {
    // メインプロセスに「ping」を送信し、返信を受け取る
    const response = window.electronAPI.sendSync('sync-channel', 'ping');
    
    // テキストエリアに返信を表示
    const textArea = document.getElementById('responseArea');
    textArea.value = textArea.value + response + '\n';
  });

  document.getElementById('testButton').addEventListener('click', () => {
    const textArea = document.getElementById('responseArea');
    textArea.value = textArea.value  +'test\n';
  });

ボタンを押すとtestと書き込むだけ。あと、処理の順がわかりやすいように上書きでなく改行して追加するようにした。
スクリーンショット 2024-12-12 20.00.01.png

main.js
.
.
// 同期通信の処理
ipcMain.on('sync-channel', async (event, args) => {
  console.log('Received message from renderer:', args); // ログ出力

  for(let i=0;i<10000000000;i++){
    let m = 3*10/10000+100;
  }

  if (args === 'ping') {

    const result = await busy(); // 時間のかかる処理を実行
    event.returnValue = result;
    // event.returnValue = 'pong'; // pongを返信
  } else {
    event.returnValue = 'unknown command'; // その他のコマンドに対応
  }
});

main.jsの通信処理の中に、無駄なループを仕込んで無理やり時間を経過させる。

結果

同期も非同期もループが終わるまで待たされた(笑)ついでにレインボーマークがくるくる回るしw

そりゃそうだmain.jsもシグルスレッドで動作しているんだから、main.jsの中でこんなループを実行したらアプリ全体が止まってしまう(笑)
main.jsの中で時間がかかるようにするには、その処理も非同期で実行しないとスレッドが止まってしまうのだ。
でもPromiseとか非同期処理とか、いまいち理解できてないので、これもChatGPTさんに丸投げ(笑)

上記のプログラムで、pingを受信してから時間のかかる処理(function busy()等)をする場合、どのように修正するといいでしょう?

来ました来ました、完璧な回答

main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

// シミュレーション用: 時間のかかる処理
function busy() {
  return new Promise((resolve) => {
    console.log('Starting busy process...');
    setTimeout(() => {
      console.log('Busy process completed.');
      resolve('pong'); // 処理結果を返す
    }, 5000); // 5秒の遅延をシミュレーション
  });
}

let mainWindow;

app.on('ready', () => {
  mainWindow = new BrowserWindow({
    width: 400,
    height: 300,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  mainWindow.loadFile('index.html');
});

// 非同期通信の処理
ipcMain.handle('async-channel', async (event, args) => {
  console.log('Received message from renderer:', args);
  if (args === 'ping') {
    const result = await busy(); // 時間のかかる処理を実行
    return result;
  }
  return 'unknown command';
});

なるほど、setTimeoutを使うのね。

早速、テスト

今度は完璧

同期

pingボタンを押して、続けてtestボタンを押しても反応はなく、でも、pongと表示された直後にtestが書き込まれる。
確かに同期だ。
なお、ボタンのビジュアルの反応はないけどマウスイベントはキャッシュされているらしく、押した回数だけ時間が経過したあとで書き込まれる。
場合によっては2度押しを阻止するような処理が必要かもしれない。

非同期

pingボタンを押して、続けてtestボタンを押すとちゃんとボタンは反応しすぐにtestが書き込まれる。しばらくしてpongが追加される。
確かに非同期だ。

以上、同期と非同期の通信の実装と違いの検証でした。

ちょい気になったこと

同期の通信でmain.jsを書き換えるにあたって時間のかかる処理を組み入れるために非同期と同じようにPromiseを返すように修正したのだが、問題なく動いている。
受信側ではPromiseを考慮してないけど、いいのだろうか...
やはりまだPromiseは理解できていない(泣)

main.js
// シミュレーション用: 時間のかかる処理
function busy() {
    return new Promise((resolve) => {
      console.log('Starting busy process...');
      setTimeout(() => {
        console.log('Busy process completed.');
        resolve('pong'); // 処理結果を返す
      }, 5000); // 5秒の遅延をシミュレーション
    });
  }

// 同期通信の処理
ipcMain.on('sync-channel', async (event, args) => {
  console.log('Received message from renderer:', args); // ログ出力

  if (args === 'ping') {

    const result = await busy(); // 時間のかかる処理を実行
    event.returnValue = result;
    // event.returnValue = 'pong'; // pongを返信
  } else {
    event.returnValue = 'unknown command'; // その他のコマンドに対応
  }
});

あっ、いいのか、Promiseを返しているんじゃなくて、この処理が非同期処理ってことね(笑)
返してるのは単純な文字列だ。

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?