初めに・自己紹介
こんにちは。Web開発エンジニアをしているものです。
最近、Web開発エンジニアとしては珍しく、社内で使うデスクトップアプリケーションを開発する案件を担当していました。
そこで、使用技術として採用したのが、Electronでした。
今回は、初めてElectronを勉強する方が、Electronの仕組みの全体像をざっくりと理解できるようにまとめてみました。
Electronとは
初めに、Electronは、どういうものなのでしょうか?
公式のドキュメントを読んでみると以下のように書かれています。
https://www.electronjs.org/ja/docs/latest/
Electron は、JavaScript、HTML、CSS によるデスクトップアプリケーションを構築するフレームワークです。 Electron は Chromium と Node.js をバイナリに組み込むことで、単一の JavaScript コードベースを維持しつつ、ネイテイブ開発経験無しでも Windows、macOS、Linux で動作するクロスプラットフォームアプリを作成できます。
すなわち、Web開発の経験しかない人でも、デスクトップアプリケーションを簡単に作ることができるようにしてくれるフレームワークです。
Electronの構造
では、なぜ僕たちWeb開発経験者が簡単にデスクトップアプリケーションを作ることができるのでしょうか?
それを理解するには、Electronの構造を軽く知っておく必要があります。
レンダラープロセス
まず、Electronには、レンダラープロセスというものがあります。
再びドキュメントを読んでみましょう。
レンダラープロセス
各 Electron アプリは、開いている BrowserWindow (及び各ウェブ埋め込み) ごとに個別のレンダラープロセスを生成します。 その名の通り、レンダラーはウェブコンテンツの レンダリング を担います。
「プロセス」という言葉になじみがなければ、「実行中のプログラム」のことだと思ってください。
Webアプリケーションにおけるフロントエンドに相当するものです。
Web開発でフロントエンドを担当したことがある方なら、特別なことをほとんど意識せず、普段通りにHTML, CSS, JavaScirptを書けばOKです。
- HTML ファイルがレンダラープロセスのエントリーポイントです。
- UI のスタイル付けは Cascading Style Sheets (CSS) で追加します。
- 実行する JavaScript コードは
<script>要素で追加できます。
そのため、最近のWebフロント開発でよく使われる、ReactやVue.jsといったライブラリやフレームワークを用いることができます。
Webフロントと同じく、ユーザーがボタンをクリックしたり、フォームに値を入力したりする画面を作ります。
メインプロセス
次に、メインプロセスをご紹介します。
各 Electron アプリにつき一つのメインプロセスがあります。これはアプリケーションのエントリポイントとして機能します。 メインプロセスは Node.js 環境で動作します。つまり、モジュールを require したり Node.js のすべての API を利用したりできます。
Webアプリケーションにおけるバックエンドのようなものだと思うとイメージしやすいと思います。
Node.jsでできているため、npmでモジュールを管理できます。
nodeモジュールは豊富であるため、基本的な計算からフォルダやファイルの操作など様々なことがWebアプリケーションのバックエンドを作るときのように手軽に実現できます。

プリロードスクリプト
最後に紹介するのが、プリロードスクリプトです。
プリロードスクリプトは、グローバルな Window インターフェイスをレンダラーと共有し Node.js の API にアクセスすることができます。そのため、window グローバルに任意の API を公開してウェブコンテンツが利用できるようにすることで、レンダラーを強化する役割を果たしています。
これが少し難しいかもしれません。
プリロードスクリプトの役割は、レンダラープロセスとメインプロセスの橋渡しと思うと良いでしょう。

しかし、レンダラープロセス(画面側)からメインプロセス(PC内のOSの機能)への要求を無制限に通しているわけではありません。
ここがミソです。
それについては、この後サンプルコードも交えて解説します。
サンプルコード
例えば、こんな感じに各ファイルがTypeScriptで書かれたシンプルな構造のアプリケーションを想定しましょう。
プロジェクト構成
説明のための暫定的な構成なので中身を深く理解する必要はありません。
my-electron-react-os-notify/
├─ package.json
├─ electron/
│ ├─ main.ts
│ └─ preload.ts
└─ renderer/
├─ index.html
└─ src/
├─ App.tsx
└─ global.d.ts
メインプロセス
まずは、メインプロセスを見てみましょう。
(※説明のために簡略化していますので、これをコピペしても動きません。
ご了承ください。あくまで今回の目的はElectronの仕組みを理解することです)
import { app, BrowserWindow, ipcMain, Notification } from "electron";
import path from "node:path";
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
const devUrl = process.env.VITE_DEV_SERVER_URL;
if (devUrl) win.loadURL(devUrl);
else win.loadFile(path.join(__dirname, "../renderer/dist/index.html"));
}
// renderer -> main(OS機能を実行)
ipcMain.handle("os:notify", (_event, message: string) => {
new Notification({
title: "Electron Notification",
body: message,
}).show();
return true;
});
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
色々と書いてありますが、要は、createWindow関数でアプリケーションの画面を立ち上げます。
今回着目したいポイントは、以下の部分です。
ipcMain.handle("os:notify", (_event, message: string) => {
new Notification({
title: "Electron Notification",
body: message,
}).show();
return true;
});
Notification クラスは、Electronにもともと入っているものです。
細かいことは理解しなくて大丈夫です。
要するに、通知の機能を実現できるということです。
ipcMain.handle( ~~ )と書くことで、レンダラープロセスからの要求にこたえるAPIを登録することができます。
ここでは、「os:notifyにレンダラープロセスから要求が来たら、{ 処理の中身 }を実行するよ」と登録できます。
このos:notifyの部分をチャンネルと呼びます。
レンダラープロセス
次に、レンダラープロセスを見てみましょう。
(※Reactで書いています。普段Reactを書かない人は、雰囲気だけでもつかんでもらえれば全く問題ありません)
import { useState } from "react";
export default function App() {
const [message, setMessage] = useState("Hello from Renderer!");
const onClick = async () => {
await window.api.notify(message);
};
return (
<div style={{ padding: 24 }}>
<h1>OS Notification Demo</h1>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
style={{ width: 360, padding: 8 }}
/>
<button onClick={onClick} style={{ marginLeft: 8, padding: "8px 12px" }}>
通知を出す
</button>
</div>
);
}
ポイントは、以下の部分です。
const onClick = async () => {
await window.api.notify(message);
};
Reactを普段書かない方向けに簡単に言うと、フォームにメッセージを入力後、ボタンがクリックされたときに先ほど作成した通知機能のAPIを実行するように設定しています。
プリロードスクリプト
しかし、先ほど作成したAPIはnotifyという名前ではなかったですし、作成したチャンネルとやらはまだ使っていません。
そのため、「まだレンダラープロセスとメインプロセスは接続できないのではないか」と思った人が多いと思います。
そこで、重要になるのがこのプリロードスクリプトです。
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("api", {
notify: (message: string) => ipcRenderer.invoke("os:notify", message),
});
contextBridgeやexposeInMainWorldといった部分に目が行くと思いますが、いったん気にしなくて大丈夫です。
ここでのポイントとしては、ipcRenderer.invoke( ~~ )と書くことで、特定のチャンネルを設定しているメインプロセスが提供するAPIのみを呼び出すメソッドを定義できるということです。
すなわち、「レンダラープロセスでnotifyが呼び出されたら、ここで登録されている"os:notify"というチャンネルに登録されているメインプロセスの処理のみが実行されるようにする」ことができるということです。
これにより、画面にあるボタンをクリックすると通知が表示されるようになります。
これで、レンダラープロセスとメインプロセスの接続(IPC通信と呼びます)が完了です。
Webアプリケーションとデスクトップアプリケーションの違い
では、なぜこのような橋渡しをする必要があるのでしょうか。
それを理解するには、まずWebアプリケーションとデスクトップアプリケーションの違いを理解する必要があります。
Webアプリケーションとデスクトップアプリケーションの最大の違いは、「ローカルのPCにダウンロードして使うかどうか」、すなわち、「ローカルのOSの機能を最大限に活用できるかどうか」です。
例えば、ダウンロードして使う写真アプリは、ローカルストレージに保存されている写真を編集したり、複製したり、削除したりすることができます。
一方で、Webアプリケーションは、ローカルの画像ファイルを取り扱おうと思った場合、それは一度サーバー等に画像ファイルをアップロードしてからとなります。
そして、ChromiumをはじめとするたいていのWebブラウザには、ローカルのフォルダ操作などのクライアントのPCのローカルストレージをフル権限で操作できないように規制を書ける仕組みが備わっています。
なぜプリロードスクリプトが必要なのか
しかし、Electronのレンダラープロセスはこれまでの説明の通り、Webページの構造で作られているため、この規制の仕組みによって、メインプロセスへの自由なアクセスが禁止されています。
そのため、「特別にこのチャンネルのみはアクセスを許可します」という橋を架けるためにプリロードスクリプトが必要というわけです。
そもそもこの規制はなぜ必要なのか
では、なぜそもそもWebアプリケーションにおいては、ユーザーのPC内の環境を自由に操作することが禁止されていいるのでしょうか?
理由は、お察しの通り、セキュリティです。
閲覧したWebページがすべて、自分のローカルファイルの閲覧・編集などが簡単にできてしまったら、本来誰にも見せたくないはずのデータの窃盗・破壊などが横行してしまいます。
そのため、Webにおいては、このようなことができないように規制がかけられています。
また、Electronが作るデスクトップアプリケーションにおいても、レンダラープロセスはあくまでWebの仕組みでできているため、UI側からOS側への攻撃が可能です。
例えば、チャットアプリなどで送られてきた文章に悪意のある内容があって、それをひらいてしまうとElectronアプリ内のレンダラープロセス内で、それがOSを操作するプログラムだとご認識されてしまい、クロスサイトスクリプティング攻撃のようなことも起こりえます。
そのため、私たち開発者は、プリロードの仕組みを正しく使って、安全性のあるアプリ開発を意識する必要があります。
まとめ
ということで、簡単ではございましたが、Electronの全体像についてまとめてみました。
画面のWebフロントと同じ要領で作ることができるレンダラープロセスとNode.jsでOSの機能を扱うことができるメインプロセス、さらにそれらの橋渡しをするプリロードスクリプトについて、それぞれの役割や存在意義について勉強できましたね。
「図解で変わる」とk言いつつ、後半はほぼ文章のオンパレードでしたな。
しかも、全体的にテンションが低いのは、ここ数日食中毒で下痢と発熱に襲われていたためです。
皆さんもお肉を焼くときにはちゃんと中まで火を通しましょう。
