Electronとは
html/css/javascriptのみで簡単にマルチプラットフォーム対応のアプリを開発できるフレームワークです。デスクトップPC向けのハイブリッドアプリみたいな立ち位置をしています。VScodeやSlackとかで使われています。
[参考] https://slack.engineering/building-hybrid-applications-with-electron/
リファレンス読んでも解決しなくて、業務改善をするためのアプリ開発で丸一日躓いてしまったため、
その戒めと備忘録のために記事にしておきます。
※普段分かっているはずのことが、フレームワーク×基本の組み合わせだとミスってしまう事例でした
ipcRenderer.invokeとは
Node.jsとChromiumを使用しており、html/css/javascriptのweb画面 + Node.jsで動作します。
画面(レンダラープロセス)の入力値を使って、メインプロセス(Node.js)で処理したい時、プロセス間通信が必要になります。そこでipcRenderer.invokeの出番です。
ipcRenderer.send(送信) → ipcMain.on → ipcRenderer.on(結果の受信)と書かなければいけないものを
ipcRenderer.invoke(送信/結果の受信) → ipcMain.handle のみで実装できます。
公式ページには以下のように使い方を紹介されています。
[引用] https://www.electronjs.org/docs/api/ipc-renderer#ipcrendererinvokechannel-args
// Renderer process
ipcRenderer.invoke('some-name', someArgument).then((result) => {
// ...
})
// 以下のようにも書けます。
const result = ipcRenderer.invoke('some-name', someArgument)
// Main process
ipcMain.handle('some-name', async (event, someArgument) => {
const result = await doSomeWork(someArgument)
return result
})
上記のようにPromise処理のようにプロセス通信を実装できます。
ですが、上記の通りに実装しようとするとnodeIntegration: trueにすることになります。
// Main process
mainWindow = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
nodeIntegration: true
// レンダラープロセスでElectronのAPIを使うためには
// nodeIntegration: true にする必要がある。
},
});
chromeやfirefoxでは害のないwebページでも、nodeIntegration: trueにしたアプリではXSSの危険性が出てしまいます。
※悪意のあるwebページ以外にも危険性があり、推奨されていません。やめときましょう。
contextBridgeとは
上記のリスクを避けて、安全にレンダラープロセスにAPIを渡してあげるための仕組みがcontextBrigeです。
[引用] https://www.electronjs.org/docs/api/context-bridge#contextbridge
//Main process
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
ipcMain.on("msg_render_to_main1", (event, arg) => {
console.log(arg); //printing "good job"
});
// Preload (Isolated World)
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing')
}
)
// Renderer (Main World)
window.electron.doThing()
//以下のようにwindowは省略できます。
electron.doThing()
contextBridgeでipcRenderer.invokeを使う
結論から言うと、contextBridgeでipcRenderer.invokeを使うときは、returnを使いましょう。
生のipcRenderer.invokeを使うときは、特段意識する必要はありませんが、contextBridgeでレンダラープロセスに渡すメソッドはasyncで定義しているので、returnを入れないとpreload.jsで定義したメソッドdoThingのresultがundefinedになってしまいます。自分はここで丸一日躓きました。
//Main process
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
ipcMain.handle('some-name', async (event, someArgument) => {
const result = await doSomeWork(someArgument)
return result
})
// Preload (Isolated World)
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld(
'electron',
{
doThing: async () => return await ipcRenderer.invoke('do-a-thing')
// メソッドdoThingはasync functionを渡しているので、
// returnがないとresultがundefinedになります。
}
)
// Renderer (Main World)
const result = await electron.doThing()
console.log(result)