🙆

Electronチュートリアルをやってみる

2024/12/21に公開

経緯

既存のReactアプリで作られたWebアプリの一部を流用し、Windowsアプリとしたいという要件が出たため実現のためのFWを調査しています。(比較してみた内容の記事は改めて出したいと思います)
Electron自体存在は知っていましたが触ったことがなかったため、ものは試しで公式チュートリアルをやってみた備忘録です。

公式ページ

https://www.electronjs.org/ja/docs/latest/#

はじめに

Electron Fiddle

ドキュメントの一番最初にこれを入れることを推奨されます。
ElemctronのAPIを試したりするための学習ツールとして使えるそうです、脳死でインストールしてみました。

クイックスタート

ウェブページの作成

Electronでは、各ウィンドウがローカルHTMLファイルまたはリモートURLから読み込んだウェブコンテンツを表示します

ブラウザウインドウでウェブページを開く

ローカルで作成したhtmlファイルをアプリケーションウィンドウに読み込ませる、そのために以下の2つのモジュールが必要。

app:アプリケーションのイベントライフサイクルを制御するappモジュール。
BrowserWindow:アプリケーション・ウィンドウの作成と管理を行う。

ローカルに作成したhtml

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using Node.js <span id="node-version"></span>,
    Chromium <span id="chrome-version"></span>,
    and Electron <span id="electron-version"></span>.
  </body>
</html>

Electron自体のメインプロセスはNode.jsで動作するので、これらをCommonJSモジュールとしてアプリケーションのエントリポイントでimportが必要。

main.js
const { app, BrowserWindow } = require('electron')

※v28.0.0からはESMもサポートされているよう
https://www.electronjs.org/ja/docs/latest/tutorial/esm

In Electron, browser windows can only be created after the app module's ready event is fired. You can wait for this event by using the app.whenReady() API.

エレクトロンではappモジュールのreadyイベントが発火した後じゃないとbrowser windowを作成することはできない。app.whenReady()でreadyイベントを待つことができる。

main.js
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600
  })

  win.loadFile('index.html')
}

app.whenReady().then(() => {
  createWindow()
})

ここまでの手順をやるとElectronアプリケーションでWebページを確認できた

ウインドウのライフサイクルを管理する

各OS毎にアプリケーションのウィンドウは異なる動作をするので、追加で雛形のコードが必要。
ElectronはOS毎のウィンドウ動作の実装責任をアプリ開発者にしている。

一般的には、process グローバルの platform 属性を使用して、特定のオペレーティングシステム専用のコードを実行できます。

全ウインドウを閉じた時にアプリを終了する (Windows & Linux)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

チュートリアル

(前述まではクイックスタート内容、ここからがチュートリアルの内容です)

必要な環境

Node.jsのセットアップやGitのセットアップの説明、割愛

テンプレートからプロジェクトを作成したい場合、Electron Forge の create-electron-appコマンド

初めてのアプリのビルド

クイックスタートでやった内容と同じ、文末にVS Code用のデバッグ設定があったので丸ごとコピー

.vscode/launch.json
{
  "version": "0.2.0",
  "compounds": [
    {
      "name": "Main + renderer",
      "configurations": ["Main", "Renderer"],
      "stopAll": true
    }
  ],
  "configurations": [
    {
      "name": "Renderer",
      "port": 9222,
      "request": "attach",
      "type": "chrome",
      "webRoot": "${workspaceFolder}"
    },
    {
      "name": "Main",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args": [".", "--remote-debugging-port=9222"],
      "outputCapture": "std",
      "console": "integratedTerminal"
    }
  ]
}

プリロードスクリプトの利用

Electron のメインプロセスは、オペレーティングシステムにフルアクセス可能な Node.js 環境です。

一方、レンダラープロセスはウェブページを実行するもので、セキュリティ上の理由からデフォルトでは Node.js を実行しません。

Electron の異なる種類のプロセスをブリッジするために、プリロード と呼ばれる特別なスクリプトを使用する必要があります。

つまりElectronは異なる種類のプロセスを実行するので、その橋渡しをするための特別なスクリプトを使用する必要がある。それがプリロードスクリプト

preload.js
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('versions', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron
  // 関数だけでなく変数も公開できます
})
main.js
const { app, BrowserWindow } = require('electron')
+ const path = require('node:path')

const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
+    webPreferences: {
+      preload: path.join(__dirname, 'preload.js')
+    }
  })

  win.loadFile('index.html')
}

app.whenReady().then(() => {
  createWindow()
})

プロセス間通信

Electronのメインプロセス(Node.js)とレンダラープロセスには別々の責務があるため、互換性はない。
つまりレンダラープロセスからNode.jsのAPIに直接アクセスすることはできず、メインプロセスからもHTMLのDOMにアクセスすることはできなし。
これを解決するために、ElectronのipcMain/ipcRendererモジュールを利用してプロセス間通信(IPC)を行う。

例えば、メインプロセスでのping APIをレンダラープロセスに渡す場合は以下

preload.js
+ const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('versions', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron,
+  ping: () => ipcRenderer.invoke('ping')
  // 関数だけでなく、変数も公開できます
})

次に、メインプロセスに handle リスナーをセットアップします。 HTML ファイルを読み込む 前 にこれを行うことで、レンダラーが invoke 呼び出しを送信する前にハンドラーの準備完了が保証されます。

main.js
- const { app, BrowserWindow } = require('electron/main')
+ const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')

const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  win.loadFile('index.html')
}
app.whenReady().then(() => {
+  ipcMain.handle('ping', () => 'pong')
  createWindow()
})

機能の追加

アプリケーションに複雑性を追加する

ここまでの手順で、静的なユーザーインターフェースを持つElectronアプリケーションができている。
この後必要になるのは以下の2つ。

  1. レンダラープロセスのウェブアプリコードに複雑性を追加する
  2. OSやNode.jsとの連携をより深める

1.は単純にWebページをリッチにするということです、現状はペラいちのページなので実際に実装するにはもう少し複雑になります。ただそれはElectronのリソースは不要なので、通常通りWebアプリを実装する手順で良さそうです。
2.はElectronが提供するツールを利用して対応が必要になります。例えば、Trayアイコンの作成からグローバルショートカットの追加、ネイティブメニューの表示までなど。この機能がブラウザタブとElectronアプリケーションとの違いであり、Electronドキュメントの焦点となっている。

入門サンプル集

サンプル集が記載されていました。
冒頭のElectron Fiddleを使用するだけでサンプルを簡単に実行できるそう。

https://www.electronjs.org/ja/docs/latest/tutorial/examples

アプリケーションのパッケージ・公開と更新

これ以降の手順はアプリケーションのパッケージ化とその公開手順についてでした。
こちらが気になるところですが、長くなってきたので本記事では割愛します。

NCDCエンジニアブログ

Discussion