12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Electron]contextBridgeでipcRenderer.invokeを使う

Last updated at Posted at 2020-09-30

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.js
// Renderer process
ipcRenderer.invoke('some-name', someArgument).then((result) => {
  // ...
})

// 以下のようにも書けます。
const result = ipcRenderer.invoke('some-name', someArgument)
main.js
// Main process
ipcMain.handle('some-name', async (event, someArgument) => {
  const result = await doSomeWork(someArgument)
  return result
})

上記のようにPromise処理のようにプロセス通信を実装できます。
ですが、上記の通りに実装しようとするとnodeIntegration: trueにすることになります。

main.js
// 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.js
//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.js

// Preload (Isolated World)

const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld(
  'electron',
  {
    doThing: () => ipcRenderer.send('do-a-thing')
  }
)
renderer.js

// 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.js
//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.js
// 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.js
// Renderer (Main World)
const result = await electron.doThing()
console.log(result)
12
7
1

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
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?