ElectronでcontextBridgeによる安全なIPC通信（受信編）


## はじめに

Electronにおけるメインプロセスとレンダラープロセス間のやり取りに関して、セキュアなIPC通信にはcontextBridge[^1]を使おう、[という記事](https://qiita.com/pochman/items/64b34e9827866664d436)を前回書いたらそれなりに読んでもらえているみたいです。ありがとうございます。

その時の例として、レンダラープロセスからメインプロセスへの**送信**を扱いましたが、**受信**についてもリクエストがあったので紹介します。基本的にはStackOverFlow[^2]からの引用です。

基本的にElectronにおけるメニュー操作はメインプロセスでハンドルすることになるので、それをレンダラープロセスへ送る際には、**メインプロセスからチャンネル付きで信号を送信し、レンダラープロセスで受信時にチャンネルに従って処理を分ける**、ということをするでしょう。これを目的としたcontextBridgeの利用法です。

## 前回からの改修点

まずは前回の記事の方法3までをお読みください。今回は前回の方法3からの改修点として次の様にしました。

- レンダラーからメインプロセスへの送信時には*チャンネル*を設定して複数の信号の送信に対応した。
- メインプロセスから送信してレンダラープロセスで受信する部分では：
  - アプリのメニュークリックで送信(メニュー操作はメインプロセスの範疇)
  - レンダラーで受信したらHTMLに反映

## 方法4：レンダラーでの受信

メインプロセスのコードは`main.js`とします。preloadファイルは`preload.js`、レンダラープロセスで読み込むhtmlファイルは`index.html`とします。各ファイルの中身は次のようになります。


```javascript
/* main.js, case 4(extend: send and recv) */
"use strict";
const {electron,BrowserWindow,app,ipcMain,Menu} = require('electron');
let mainWindow = null;
const CreateWindow = () => {
  mainWindow = new BrowserWindow({width: 800, height: 600,
    webPreferences: { 
      nodeIntegration: false,
      contextIsolation: true,
      preload: __dirname + '/preload.js'
    } });
  mainWindow.webContents.openDevTools(); 
  mainWindow.on('closed', function() {
    mainWindow = null;
  });

  /*menu creation*/
  const template = [ {
    label: 'File',
      submenu: [{
        label: 'Open',
        click: (menuItem, browserWindow, event) => { 
          // メニュークリック時に実行される関数//

          //ここでファイルを開いたりする(今回は暫定)//
          const openedPath = "./hogehoge.txt";
          const readData = "ファイルの中身だよ";
          //ここまででファイルは読み込んだものとする//

          //この関数でIPC送信(main to renderer)//    
          browserWindow.webContents.send(
            "openfile", //送信チャンネル名(自分で区別できるように)//
            {   //送信したいデータ一覧//
               filePath: openedPath,
               dataText: readData
            }
          ); 
        } 
      }]  
    }];
        
    const menu = Menu.buildFromTemplate(template)
    Menu.setApplicationMenu(menu);

    mainWindow.loadURL('file://' + __dirname + '/index.html');
}
app.on('ready', CreateWindow);

//IPC受信部//
ipcMain.on("msg_render_to_main1", (event, arg) => {
  console.log(arg); //printing "good job"
});
ipcMain.on("msg_render_to_main2", (event, arg) => {
  console.log("We are the", arg.teamName, "!");
  //printing "We are the Victories !"
});
```


```javascript
/* preload.js, case 4 (extend)*/
const { contextBridge, ipcRenderer} = require("electron");
contextBridge.exposeInMainWorld(
  "api", {
    send: (channel, data) => {//rendererからの送信用//
        ipcRenderer.send(channel, data);            
      },
    on: (channel, func) => { //rendererでの受信用, funcはコールバック関数//
        ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
  }
);
```


```html
<!--index.html, case 4 (extend)-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Test</title>
  </head>
  <body>
    <button id="button1">test1</button>
    <button id="button2">test2</button>
    <div id="previewF">受信ファイル名</div>
    <div id="previewD">受信データ</div>
  </body>
  <script type = "text/javascript">
      //適当なプログラム
    const button1 = document.getElementById("button1");  
    
    //送信用(チャンネル名指定)//
    button1.addEventListener("click", (e)=>{
      window.api.send("msg_render_to_main1", "god job");
    });
    
    //送信用(チャンネル名指定)//
    button2.addEventListener("click", (e)=>{
      window.api.send("msg_render_to_main2", {teamName: "Victories"});
    });

    //受信部(チャンネル名指定)//
    window.api.on("openfile", (arg)=>{
      document.getElementById("previewF").textContent = arg.filePath;
      document.getElementById("previewD").textContent = arg.dataText;
    });
 </script>
</html>
```

まず、レンダラープロセスからメインプロセスへ送信する部分ですが、ボタンを二つ配置し、チャンネル名を変えて二種類の信号が送れるようになっています。メインプロセスでは`ipsMain.on(チャンネル名, コールバック関数)` でチャンネル名を指定することで、処理を分けて行えるようになります。

次に、本目的のレンダラープロセスでの受信ですが、`preload.js`での`on`の部分の記述がポイントです。コールバック関数名を`func`としておいて、 `ipcRenderer.on()`の中ではスプレッド構文`...args`を使っています。これにより、メインプロセスから送られてきた引数のうち、`event`だけを取り除いてコールバック関数へ渡しています。コールバック関数は`index.html`内の`window.api.on("openfile",(arg)=>{ ... })`にて記述できるので、実質的に`ipcRenderer.on()`を直接書いていた時代と同様に利用できます。

## 注意点

contextBridgeはとっても良さそうなAPIですが、Electronのドキュメント[^3]には次のように書かれています。

"The `contextBridge` API has been published to Electron's master branch, but has not yet been included in an Electron release."

一応、私の環境のversion7.1.9では使えていますが、いつから使えるようになったのかはちょっと不明なので、気を付けてください。

## 感想

コールバック関数という表現が合っているのか不安ですが、Javascriptライト勢なのでご勘弁くだされ。


## 追記 2020/9/23

contextBridgeのより良い使い方として、もう少し注意すべき点があります。
上記のサンプルコードは、Electronのversion upの歴史に沿ってコードをどのように改変していくかを示すために残しておきますが、使用する前に下記の記事も是非読んでみてください。

[Electronのセキュリティについて大きく誤認していたこと](https://qiita.com/sprout2000/items/2b65f7d02e825549804b)

[Electron（v10.1.2現在）の IPC 通信入門 - よりセキュアな方法への変遷](https://qiita.com/hibara/items/c59fb6924610fc22a9db)


[^1]: [https://electronjs.org/docs/api/context-bridge](https://electronjs.org/docs/api/context-bridge)

[^2]: [StackOverFlow:With contextIsolation = true, is it possible to use ipcRenderer?](https://stackoverflow.com/questions/55164360/with-contextisolation-true-is-it-possible-to-use-ipcrenderer)

[^3]: [https://electronjs.org/docs/api/context-bridge/history](https://electronjs.org/docs/api/context-bridge/history)