ipc

Electron

ipc通信とは

electronは

・メインプロセス(electronアプリがOS機能を使用する過程)

・レンダラープロセス(アプリのウィンドウが作成され、そのウィンドウがindex.htmlを読み込むと、ウィンドウ内でhtmlのUI(とそこから読み込まれるスクリプト)を描画する過程)

の2つに分かれている

レンダラープロセスはChroniumというウェブブラウザをつかって行われており、通常のウェブサイト(GoogleChrome等)と変わらないため、世界中の人々がアクセスできる

つまり、メインプロセスとレンダラープロセスを自由に行き来できると、世界中の人々がelectronアプリを通して開発者のローカルPCのOS機能を使用できてしまう

これはセキュリティ上よろしくないので、メインプロセスとレンダラープロセスの間をpreload.jsという小窓でつなぐ

preload.jsは、レンダラープロセスがメインプロセスの特定の機能にのみアクセスできるようにする小窓

具体的には、メインプロセス(main.js)でipcMainによって処理を設定し、レンダラープロセス(renderer.js)でipcRendererによってinvokeする

install

// electron-storeはアプリを閉じてもデータが失われないように、
// キーとバリュー形式でデータをローカルファイルconfig.jsonに保存するためのモジュール
// オンライン上のデータベース or ローカルストレージを使わないときに使用する
npm install electron@latest electron-store uuid

directory structure

electronproject
|---package.json
|---main.js
|---index.html
|
|---preload.js
|---store.js
|---renderer.js

main.js

// ES形式 ... importでJSモジュールを読み込む
// CommonJS形式 ... constとrequireで読み込む

// app ... アプリ全体を起動・終了する
// BrowserWindow ... アプリ内でウィンドウを作成する
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const store = require('./store');


let win;

function createWindow() {
  win = new BrowserWindow({
    width: 1000,
    height: 1200,
    title: 'electron app',
    // ipc通信の設定
    webPreferences: {
      // プレロードスクリプトが実行されてからレンダラープロセスが読み込まれる
      preload: path.join(__dirname, 'preload.js'),
      // レンダラープロセスがnode.jsの機能(OS機能)を利用できないようにする
      nodeIntegration: false,
      // メインプロセスとレンダラープロセスのcontextを分離してpreload.jsでつなぐ
      contextIsolation: true
    }
  });
  // ウィンドウにindex.htmlを読み込ませる
  win.loadFile('index.html');
}

// 初期化が完了したらcreateWindow関数を実行する
app.whenReady().then(createWindow);

// すべてのウィンドウが閉じたときの処理
// MacOS(darwin platform)以外ならアプリを終了させる
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});


ipcMain.handle('events:get', async () => {
  return Store.getAllEvents();
});

ipcMain.handle('events:add', async (event, ev) => {
  return Store.addEvent(ev);
});

ipcMain.handle('events:update', async (event, id, updates) => {
  return Store.updateEvent(id, updates);
});

ipcMain.handle('events:delete', async (event, id) => {
  return Store.deleteEvent(id);
});

preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  getAllEvents: () => ipcRenderer.invoke('events:get');
  addEvent: (ev) => ipcRenderer.invoke('events:add', ev);
  updaetEvent: (id, update) => ipcRenderer.invoke('events:update', id, updates);
  deleteEvent: (id) => ipcRenderer.invoke('events:delete', id)
});

store.js

const Store = require('electron-store').default;
const { v4: uuidv4 } = require('uuid');

const schema = {
  events: {
    type: 'array',
    default: []
  }
};

const store = new Store({ schema });

function getAllEvents() {
  return store.get('events', []);
}

function addEvent(ev) {
  const events = getAllEvents();
  const newEv = {
    id: uuidv4(),
    title: ev.title || 'Untitled',
    start: ev.start,
    end: ev.end || ev.start,
    notes: ev.notes || ''
  };
  events.push(newEv);
  store.set('events', events);
  return newEv;
}

function updateEvent(id, updates) {
  consts events = getAllEvents();
  const idx = events.findIndex(e => e.id === id);
  if (idx === -1) throw new Error('Not found');
  events[idx] = { ...events[idx], ...updates };
  store.set('events', events);
  return events[idx];
}

function deleteEvent(id) {
  let events = getAllEvents();
  events = events.filter(e => e.id !== id);
  store.set('events', events);
  return true;
}

module.exports = { getAllEvents, addEvent, updateEvent, deleteEvent };

index.html

<!DOCTYPE html>
<html>
...
<script src="renderer.js"></script>
</body>
</html>

renderer.js

BACK