はじめに
Windows のデスクトップアプリを作るのに、自分は Electron も使っていて、使い始めてから随分になります。使い方を整理しておこうと思いました。
この続きです。
Electron を始めるの続き
以前に作成した Electron プログラムに手を入れていきます。
Electron のバックエンドとフロントエンド
Electron は、main.js
を Node.js エンジンが実行するバックエンドのプログラムと、それが index.html
をロードして、さらに renderer.js
などを内蔵ブラウザが実行するフロントエンドのプログラムに分かれています。バックエンドのプログラムが動作するのは「メインプロセス」、フロントエンドのプログラムが動作するのは「レンダラプロセス」と呼ばれます。
Node.js で実行されるバックエンドのプログラムは、ファイルの操作や OS 機能の呼出など、ブラウザで実行されるプログラムができない機能が使えます。ここに書かれたプログラムを、フロントエンドのプログラムで呼出して利用します。そのための手順が Electron に用意されています。
バックエンドのメソッドを呼出する準備
main.js
と別に preload.js
を作成して、アプリの起動時点で読込するようにします。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('main', {
// ここにフロントエンドから呼出するメソッドを記述
}
(前略)
const win = new BrowserWindow({
(中略)
webPreferences: {
preload: path.join(__dirname, "preload.js")
}
})
フロントエンドからバックエンドのメソッドを呼出①
上記のコードに追記して、フロントエンドからバックエンドのメソッドを呼出してみましょう。
バックエンドで実行されている Electron のバージョンを取得するメソッド info()
を用意したいと思います。
まず、preload.js
にメソッド info()
を記述します。
contextBridge.exposeInMainWorld('main', {
info: () => {
return `Electron v ${ process.versions.electron }`;
},
}
上記の実装で、フロントエンドのプログラム renderer.js
でメソッド main.info()
が用意されて呼出できるようになります。
(前略)
<p id="result"></p>
(後略)
window.onload = function(){
document.querySelector('#result').innerText = main.info();
}
実行フロー↓

フロントエンドからバックエンドのメソッドを呼出②
上記の例は、バックエンドのプログラム main.js
に実装したメソッドは呼出していませんでした。
main.js
にメソッド greet()
を実装して呼出してみましょう。
まず、フロントエンドのプログラムを実装します。
(前略)
<input id="name" type="text" />
<button id="greet">Greet</button>
<p id="result"></p>
(後略)
document.querySelector('#greet').addEventListener('click', function() {
var name = document.querySelector('#name').value;
if (!name) return;
// ここでバックエンドのメソッドを呼出して結果を表示する
})
呼出したいメソッド greet()
を用意します。
function greet(name) {
return `Hello, ${name}!`
}
main.js
の ipcMain.handle()
を preload.js
の ipcRenderer.invocke()
で呼出します。このときチャンネル名を指定します。今回は greet
にしました。
ipcMain.handle('greet', (e, arg) => {
return greet(arg);
})
contextBridge.exposeInMainWorld('main', {
(中略)
greet: (arg) => {
return ipcRenderer.invoke('greet', arg);
}
上記の設定によって renderer.js
で main.greet()
が呼出できるようになります。これを呼出すると main.js
の greet()
が呼出されます。
main.greet()
は Promise
を返すようになっています。結果を受取するには
document.querySelector('#greet').addEventListener('click', function(){
(中略)
main.greet(name)
.then((result) => {
document.querySelector('#result').innerText = result;
})
あるいは
document.querySelector('#greet').addEventListener('click', async function(){
(中略)
var result = await main.greet(name);
document.querySelector('#result').innerText = result;
実行フロー↓

バックエンドのメソッドで失敗を返す
上記の main.js
の greet()
で問題あって処理に失敗したことを、呼出元に返したいとします。
バックエンドでエラーを投げてフロントエンドでキャッチしたい
greet()
で throw new Error()
して、呼出元で catch
したいところです。
function greet(name) {
if (!name) {
throw new Error("名前が指定されていません。")
}
return `Hello, ${name}!`
}
(後略)
document.querySelector('#greet').addEventListener('click', async function() {
var name = document.querySelector('#name').value;
try {
var result = await main.greet(name);
document.querySelector('#result').innerText = result;
}
catch (err) {
document.querySelector('#result').innerText = err.message;
}
})
実行すると、ターミナル画面に
Error occurred in handler for 'greet': Error: 名前が指定されていません。 at greet (....\HelloElectron\src\main.js:40:15)
フロントエンドの画面に
Error invoking remote method 'greet': Error: 名前が指定されていません。
greet()
で投げたエラーを素直にキャッチできているのではなさそうです。↑
バックエンドのハンドラ ipcMain.handle()
でエラーをキャッチして、Promise
を返す処理にしてみるのはどうでしょうか。
ipcMain.handle('greet', (e, arg) => {
try {
const result = greet(arg);
return Promise.resolve(result);
}
catch (err) {
return Promise.reject(err.message);
}
})
実行すると、ターミナル画面に
Error occurred in handler for 'greet': 名前が指定されていません。
フロントエンドの画面に
Error invoking remote method 'greet': Error: 名前が指定されていません。
期待したようになりませんね。↑
結果とエラー値を一緒に返してみる
例外を投げるのでなく、エラー値を普通に返すことにします。結果値とエラー値を配列にして返してみます。
ipcMain.handle('greet', (e, arg) => {
try {
const result = greet(arg);
return [result, null];
}
catch (err) {
return [null, err.message];
}
})
var [result, err] = await main.greet(name)
if (!err) {
document.querySelector('#result').innerText = result;
}
else {
document.querySelector('#result').innerText = err.message;
}
実行フロー↓

これなら期待したように動作します。
フロントエンドでラッパーを用意する
呼出元のコードは try ~ catch
で書けるとエラー処理が分かりやすいですね。main.greet()
をラップして新たに greet()
を用意したらどうでしょうか。
async function greet(name) {
var [result, err] = await main.greet(name)
if (!err) {
return result
}
else {
throw new Error(err)
}
}
document.querySelector('#greet').addEventListener('click', async function () {
var name = document.querySelector('#name').value;
try {
var result = await greet(name);
document.querySelector('#result').innerText = result;
}
catch(err) {
document.querySelector('#result').innerText = err.message;
}
});
実行フロー↓

うまく動作しますが、ここまでしなくていいでしょうか。機械的に実装できるので、開発ツールで自動で実装して欲しいですね。
バックエンドからフロントエンドのメソッドを呼出
フロントエンドからバックエンドのメソッドを呼出できるようになりました。逆にバックエンドからフロントエンドのメソッドを呼出できるでしょうか。
例えば、アプリのウィンドウを移動したとき、フロントエンドのプログラムは把握できないがバックエンドのプログラムは把握できます。これをフロントエンドのプログラムで表示したいとします。
まず、main.js
にウィンドウを移動するイベントのハンドラを記述します。そこで webContents.send()
します。
win.on('move', (e) => {
var size = win.getPosition();
win.webContents.send('onmove', size);
})
ipcRenderer.on()
するハンドラを preload.js
に記述します。これを contextBridge.exposeInMainWorld
でフロントエンドに公開します。メソッド名は onmove()
にしておきます。バックエンドから送られた情報はメソッドのコールバックにセットします。
contextBridge.exposeInMainWorld('main', {
(中略)
onmove: (callback) => {
ipcRenderer.on('onmove', (e, arg) => callback(arg))
},
フロントエンドのプログラムで利用します。
(前略)
<p id="result"></p>
(後略)
window.onload = function(){
main.onmove(function(arg){
var [x, y] = arg;
document.querySelector('#result').innerText = `x:${x} y:${y}`;
});
}
実行フロー↓
