Electronチュートリアルをやってみる
経緯
既存のReactアプリで作られたWebアプリの一部を流用し、Windowsアプリとしたいという要件が出たため実現のためのFWを調査しています。(比較してみた内容の記事は改めて出したいと思います)
Electron自体存在は知っていましたが触ったことがなかったため、ものは試しで公式チュートリアルをやってみた備忘録です。
公式ページ
はじめに
Electron Fiddle
ドキュメントの一番最初にこれを入れることを推奨されます。
ElemctronのAPIを試したりするための学習ツールとして使えるそうです、脳死でインストールしてみました。
クイックスタート
ウェブページの作成
Electronでは、各ウィンドウがローカルHTMLファイルまたはリモートURLから読み込んだウェブコンテンツを表示します
ブラウザウインドウでウェブページを開く
ローカルで作成したhtmlファイルをアプリケーションウィンドウに読み込ませる、そのために以下の2つのモジュールが必要。
app:アプリケーションのイベントライフサイクルを制御するappモジュール。
BrowserWindow:アプリケーション・ウィンドウの作成と管理を行う。
ローカルに作成した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が必要。
const { app, BrowserWindow } = require('electron')
※v28.0.0からは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イベントを待つことができる。
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用のデバッグ設定があったので丸ごとコピー
{
"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は異なる種類のプロセスを実行するので、その橋渡しをするための特別なスクリプトを使用する必要がある。それがプリロードスクリプト
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
// 関数だけでなく変数も公開できます
})
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をレンダラープロセスに渡す場合は以下
+ 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 呼び出しを送信する前にハンドラーの準備完了が保証されます。
- 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つ。
- レンダラープロセスのウェブアプリコードに複雑性を追加する
- OSやNode.jsとの連携をより深める
1.は単純にWebページをリッチにするということです、現状はペラいちのページなので実際に実装するにはもう少し複雑になります。ただそれはElectronのリソースは不要なので、通常通りWebアプリを実装する手順で良さそうです。
2.はElectronが提供するツールを利用して対応が必要になります。例えば、Trayアイコンの作成からグローバルショートカットの追加、ネイティブメニューの表示までなど。この機能がブラウザタブとElectronアプリケーションとの違いであり、Electronドキュメントの焦点となっている。
入門サンプル集
サンプル集が記載されていました。
冒頭のElectron Fiddleを使用するだけでサンプルを簡単に実行できるそう。
アプリケーションのパッケージ・公開と更新
これ以降の手順はアプリケーションのパッケージ化とその公開手順についてでした。
こちらが気になるところですが、長くなってきたので本記事では割愛します。
NCDC株式会社( ncdc.co.jp/ )のエンジニアチームです。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください! ※エンジニア以外も記事を投稿することがあります
Discussion