◆ Node.js の代わりに Electron があるイメージ(Electron がランタイム)
  ◆ Node.js 自体は必須じゃない
◆ Electron にアプリフォルダを指定するか resources/app にアプリフォルダを配置して起動
◆ package.json にメインの JavaScript ファイルを指定
◆ メインプロセスでブラウザウィンドウを表示させて 表示する HTML ファイルを設定
◆ HTML ファイルでは画面の処理を行う
  ◆ DOM API と Node.js API の両方が使える

Electron の簡単な使い方の紹介をしましたが ほぼデフォルトを使いまわしで Node.js も使わず HTML を書くような前提で 単にサーバとの通信不要なウェブアプリをローカルアプリ化するようなものでした

あれから 3 年以上経ってますし いろいろ変わってるかもと思って もう少しちゃんとした使い方をまとめてみます
Electron は Node.js アプリともまたちょっと違うので 最初はなにすればいいのかもわかりづらくて 0 から作ろうとしても困ることが多いのですよね

Install

まずはインストールです
Node.js で動かすタイプのアプリと違って Electron で動かすアプリになるので Node.js は必須ではありません
Node.js を入れるようなものと考えて Electron をインストールします

今の最新版だと

https://github.com/electron/electron/releases/tag/v4.1.4

から

electron-v4.1.4-win32-x64.zip

をダウンロードすれば良いです
ここでは Windows を前提にしてますが 環境に応じて置き換えてください

npm を使ってインストールもできます

yarn global install electron

Electron

ダウンロードしたら electron.exe を起動します
通常はこんなデフォルト画面が出ます

electron-tu001

昔はホーム画面にアプリをドロップして起動できたのですが今は無理のようです

なので electron でのアプリの実行方法はコマンドから electron の引数に Electron アプリのフォルダを指定して起動します

electron app-folder

ファイルじゃなくてフォルダというところに注意です

windows だとフォルダをドラッグして electorn.exe にドロップしてもいいです
コマンドで引数に渡すのと同じことです

ただ コマンドからじゃないと console.log で出力したログが見れない不便なところもあるので 開発中はコマンドからが良さそうです

実行する Electron が Node.js や Python のようにアプリを動かすランタイムと考えるならこの方法がベストですが パッケージングした特定のアプリ専用と考えるなら electron フォルダ内にアプリを配置できます
Electron のフォルダ内の resources フォルダに app という名前のフォルダを作ります
app フォルダがあれば その Electron はコマンドラインの引数があったとしても無視して app フォルダを実行するアプリとして起動します

アプリ専用の Electron としてダウンロードしたならこっちの方法でもいいと思います

Electron Application

起動方法がわかったのでここからは Electron のアプリを作っていくことになります

package.json

Node.js のアプリと同じくフォルダの中に package.json が必要です
最低 main の設定だけあればおっけいです

{
"main": "main.js"
}

main では最初に実行するエントリポイントの JavaScript を設定します
名前は何でもいいので main.js でも app.js でも [アプリ名].js でもおっけいです

Main Process

main で指定された JavaScript はメインプロセスと言われて ここでウィンドウを作ります
自分で作らないとウィンドウは表示されないのでバックグラウンドで動作するアプリも作れます

最低限これだけであればウィンドウを出せます

const { app, BrowserWindow } = require("electron")

app.on("ready", () => {
new BrowserWindow()
})

ウィンドウを表示するだけでは何も表示されないので ファイルや URL をロードします

app.on("ready", () => {
const main_window = new BrowserWindow()
main_window.loadFile("index.html")
})

loadFile でローカルファイルをロードし loadURL で URL のページをロードします
通常のブラウザではローカルファイルにアクセスなどができないサンドボックス内での実行なのでセキュリティ的に守られていますが Electron の場合は普通にローカルファイルのアクセスもできるので外部の URL を開く場合は注意が必要です
ページ内の JavaScript で Node.js の機能を無効にする設定もあります

Renderer Process

ウィンドウにロードされた HTML の JavaScript はレンダラープロセスと言われます
普通のウェブページを作るように HTML や JavaScript を書けます

index.html
<!doctype html>
<meta charset="utf-8">

<script defer src="index.js"></script>

<h1>Hello.</h1>

<p>Current time is <span id="now"></span></p>

index.js
const now_elem = document.getElementById("now")

setInterval(() => {
now_elem.textContent = new Date().toLocaleString()
}, 1000)

と書けばこういう画面が出ます

electron-tu002

ウィンドウサイズは少し縮めてます

レンダラープロセスでも Node.js の機能が使えます
ウェブページを操作することもできるので DOM の API と Node.js の API の両方が使えます

index.html
<!doctype html>
<meta charset="utf-8">

<script defer src="index.js"></script>

index.js
const child_process = require("child_process")
const util = require("util")
const iconv = require("iconv-lite")

const exec = util.promisify(child_process.exec)

const ls = async () => {
if (process.platform === "win32") {
const { stdout } = await exec("dir", { encoding: "buffer" })
return iconv.decode(stdout, "shift_jis")
} else {
const { stdout } = await exec("ls -la")
return stdout
}
}

ls().then(text => {
const pre = document.createElement("pre")
pre.textContent = text
document.body.append(pre)
})

electron-tu003

util や sub_process といった Node.js の機能を使って得られたデータを DOM に出力しています
Windows だとコマンドの結果が Shift_JIS になるので iconv-lite で変換しています
npm のライブラリを使うときは Node.js と同じようにインストールして require すれば使えます

yarn add iconv-lite

通常は Electron アプリのフォルダの中に node_modules を置きますが親階層にあっても探してくれます

C:\dev3\playground\electron\a1

が Electron アプリのフォルダなら dev3 や playground に node_modules があっても大丈夫です

REPL

Electron は --interactive オプションをつけると対話型で REPL 実行できるようです
デバッグ用途では助かりそうです

ですが 今のところ Windows はサポート対象外らしく動きませんでした

ただ レンダラープロセスなら devtools 出せばいいし メインプロセスなら

const { app, BrowserWindow } = require("electron")

で始めてるわけなので Node.js を REPL で使って上のコードを実行すればよさそうです
しかし やってみたら require("electron") の返り値が electron.exe ファイルのパスの文字列でした

electron パッケージの index.js はこうなっています

var fs = require('fs')
var path = require('path')

var pathFile = path.join(__dirname, 'path.txt')

function getElectronPath () {
if (fs.existsSync(pathFile)) {
var executablePath = fs.readFileSync(pathFile, 'utf-8')
if (process.env.ELECTRON_OVERRIDE_DIST_PATH) {
return path.join(process.env.ELECTRON_OVERRIDE_DIST_PATH, executablePath)
}
return path.join(__dirname, 'dist', executablePath)
} else {
throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')
}
}

module.exports = getElectronPath()

Electron のパスを返していますね
npm でインストールできるものの Node.js から実行することはサポートしていなくて Electron として実行しないと Electron の機能は使えなくなってるようです

Release

自分で使う分にはこれまでの開発用の方法で問題ないですが Electron アプリを他の人に使ってもらう場合はいくつかの方法があります

Electron 持ってる場合

相手の人が Electron を持ってる人の場合は アプリケーションフォルダを zip 化してわたせばそれだけで使えます
重い Electron 部分は渡さなくて良くて アプリのソース部分だけなので楽ですね

ただ Electron 持ってる人って言うと開発者など限られた人になります
また Electron のバージョンで Node.js バージョンや Chromium バージョンが変わって動かないこともあります
なので この方法が使える場合は限られてきます

zip でパッケージング

リリース用に Electron をダウンロードして app フォルダを作る方法を使います
app フォルダに配置したら Electron のフォルダごと zip 化して相手に渡します

解凍して Electron を実行するだけで使えます
ただ zip ファイルのサイズが大きくなるのがデメリットです

app フォルダはただのフォルダですが 中身を見られたり変更されたくない場合には asar というアーカイブファイルにすることができます
app フォルダの代わりに app.asar を resources に配置しても同じように Electron アプリを実行できます
ただ 専用形式なだけで zip みたいなものなので 展開ツールさえあれば簡単に中身が見れます

Electron が使われてるアプリケーションで 一部動作を変えたくて unpack して修正して再 pack したことがありますが ファイルが壊れていると言われて使えないアプリがありました
単純に asar のハッシュ値をチェックしたりしてるだけかもですが 変更を許容しない方法はありそうです

electron-builder

自分で zip を作る方法でもいいですが ちゃんとしたビルドツールも存在します

https://www.electron.build/

使うには package.json に name と version プロパティが必須になります
main だけしか書いてないなら追加してから実行します

yarn global add electron-builder
cd a1
electron-builder build --win

アプリのフォルダのパスを指定できないようなので カレントディレクトリを移動して実行します
--win を設定すると Windows 用にビルドされたファイルが生成されます

name を test1 にした場合の例です

C:\dev3\playground\electron\a1>electron-builder --win
Configuring yargs through package.json is deprecated and will be removed in the next major release, please use the JS API instead.
Configuring yargs through package.json is deprecated and will be removed in the next major release, please use the JS API instead.
• electron-builder version=20.39.0
• description is missed in the package.json appPackageFile=C:\dev3\playground\electron\a1\package.json
• author is missed in the package.json appPackageFile=C:\dev3\playground\electron\a1\package.json
• writing effective config file=dist\builder-effective-config.yaml
• no native production dependencies
• packaging platform=win32 arch=x64 electron=4.1.4 appOutDir=dist\win-unpacked
• default Electron icon is used reason=application icon is not set
• downloading parts=1 size=5.6 MB url=https://github.com/electron-userland/electron-builder-binaries/releases/download/winCodeSign-2.4.0/winCodeSign-2.4.0.7z
• downloaded duration=7.472s url=https://github.com/electron-userland/electron-builder-binaries/releases/download/winCodeSign-2.4.0/winCodeSign-2.4.0.7z
• building target=nsis file=dist\test1 Setup 1.0.0.exe archs=x64 oneClick=true perMachine=false
• downloading parts=1 size=1.4 MB url=https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-3.0.3.2/nsis-3.0.3.2.7z
• downloaded duration=3.453s url=https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-3.0.3.2/nsis-3.0.3.2.7z
• downloading parts=1 size=1.0 MB url=https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-resources-3.3.0/nsis-resources-3.3.0.7z
• downloaded duration=3.664s url=https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-resources-3.3.0/nsis-resources-3.3.0.7z
• building block map blockMapFile=dist\test1 Setup 1.0.0.exe.blockmap

アプリフォルダに dist フォルダができていて 中にはこういうファイルがあります

a1\dist\win-unpacked
a1\dist\builder-effective-config.yaml
a1\dist\test1 Setup 1.0.0.exe
a1\dist\test1 Setup 1.0.0.exe.blockmap

インストーラを用意してくれます
win-unpacked が Windows 用の Electron のフォルダになっていて その resources フォルダの app.asar にアプリが配置されています
自動で pack してくれてます
また electron.exe が name に応じた名前になります
この例だと test1.exe になってました

blockmap は差分更新用の gzipped json らしいです
https://github.com/electron-userland/electron-builder/issues/2851

Document

Electron のドキュメントは公式にあって情報も多いです

https://electronjs.org/docs

英語しか無いと思ってたのですが 右上から言語を選べて日本語がありました
英語に比べるとちょっと古いようで 英語にある項目がなかったりもしますが 読みづらい機械翻訳というわけでもなさそうです
もっと早く気づいていれば……

その他

package.json のエラー

package.json にエラーがあるとデフォルトアプリが表示されます
キーに "" がなかったり最後に , があったり JSON として不正なものです

アプリを配置してるのになぜかデフォルト画面が出るということが起きたら package.json を確認してみるとよさそうです

メインウィンドウの参照

メインプロセスではこれだけでもいいのですが サンプルを見るともうちょっと複雑でした

const { app, BrowserWindow } = require("electron")

app.on("ready", () => {
const main_window = new BrowserWindow()
main_window.loadFile("index.html")
})

const { app, BrowserWindow } = require("electron")

let main_window = null

app.on("ready", () => {
main_window = new BrowserWindow()
main_window.loadFile("index.html")
main_window.on("closed", () => {
main_window = null
app.quit()
})
})

  • main_window をトップレベルの変数に格納
  • main_window で参照を解除
  • app.quit

main_window が GC で回収されて消えるとウィンドウが消えるそうです
なので main_window をトップレベルで参照を持つようにしているそうです

トップレベルと言ってもブラウザの実行環境じゃないので グローバル変数とは違いますし ready のリスナの中でもトップレベルでも変わらない気がします
それに 使われてる以上 GC されないように思います

信じられなかったので 試してみました
数十分で勝手に終了したという報告を見かけたので 数時間起動したままにしましたが 特に回収されることはなかったです
ただ まぁ公式ドキュメントがそうなってるわけですし とりあえずこうしておくのが安心かと思います

外部に参照を持ってるので closed イベントで null に出力しています
マルチウィンドウのアプリなら閉じたのは GC されるように参照を外すべきですが シングルウィンドウだと閉じれば勝手に消えるので closed イベントは無視でいいように思います
Windows で試した限りでは ウィンドウを閉じると自動でプロセスが終了していました
しかし Electron にフォルダ指定で動かしたのと app フォルダを用意したのでは動きが違うようで app フォルダのアプリを実行した場合のみ ウィンドウを閉じただけだと自動で終了してくれませんでした
他にどういう違いがあるかはわかりませんが app.quit は明示的にするべきみたいです

ここではシングルウィンドウなので main_window の closed イベントで app.quit していますが マルチウィンドウだと app の window-all-closed イベントで app.quit します

innerHTML

普通にウィンドウを開くと 自動でメニューバーが画面の上に存在します
HTML に何も書いてないので HTML で表示する領域の外側だと思ってました
しかし 実はこれも HTML で管理する領域でした
イベントが起きたら画面を一新したくて document.body.innerHTML を置き換えると メニューバーも消えてしまいました
body の中に root になる div を用意してそこを書き換えると良さそうです