当記事ではElectronの導入方法や使い方を解説します。ただし、発展的・応用的な事柄は取り上げず、私が初めてElectronを初めて触ったときに苦労した経験から、Electronを初めて触る人が本当に知りたいと思うであろうことに絞ってまとめています。
Windows 10 Home 64bit版で動作確認しています。
1. Electronとは
Electronとはデスクトップアプリを開発するためのフレームワークです。元々はGitHubを運営しているGitHub, Inc.が開発したAtom Shellというフレームワークだったのですが、後にOSS化され、名前もElectronに改名されました。
Electronの最大の特徴は、フロントエンドで用いられる3つの言語――HTML・CSS・JavaScriptを使って、デスクトップアプリを作ることができることです。また、1つのソースコードからWindows・macOS・Linuxのそれぞれに対応したアプリを生成することも容易です。
ただし、他のデスクトップアプリ開発技術(C#・VBに対応した.NET Frameworkや、Javaに対応したSwing・JavaFXなど)と比べると動作速度に劣るようです。加えて、配布可能な状態にパッケージングしたときにアプリケーションの容量が大きくなってしまうという短所があります。後者は、大容量のストレージを採用することで対処可能と言えなくもないですが、前者は(おそらく)対処不可能ですのでゲームを始めとする処理速度が必要なデスクトップアプリを作るのには不向きと言えます。
2. Electronの技術的特徴
Electronの実態はJavaScriptで書かれたnpmパッケージです。ただし、その内部からはGoogleが開発したChromiumというブラウザアプリを参照しているとされます。おそらく、Electronでデスクトップアプリを作った場合、以下のような処理構造になると考えられます。
■ デスクトップアプリごとの独自処理
↑
↓
■ Electron
↑
↓
■ Chromium
「ん? Electronはデスクトップアプリを作るためのフレームワークだよね? 何故ブラウザアプリが出てくるの?」と思われるでしょうが、そこがElectronの肝です。驚くべきか、ElectronはGUIを再現するのにChromiumを利用しているのです。冒頭で「ElectronはHTML・CSS・JavaScriptでデスクトップアプリを作ることができるフレームワークである」と述べたのはGUIの描画にChromiumを利用しているからなのですね。
それを証明できる面白い試みがあります。このQiitaのURLを、Google ChromeとElectronに、それぞれ引数として渡して起動すると同様のWebページが描画されるのです。それは両者とも内部にChromiumを格納しているためです。
PS C:\Users\AGadget> chrome "https://qiita.com/"
PS C:\Users\AGadget> electron "https://qiita.com/"
また、処理速度が遅いというのもElectron製デスクトップアプリの本質がWebコンテンツであるためです。どうしても、やはり、その他のデスクトップ開発技術を利用して作られたものよりはモッサリとしてしまうのは仕方がない部分なのかと思います。配布可能な状態にパッケージングした後の容量が大きいのも、アプリを実行するために必要なChromiumを丸々突っ込んでいるからです。
3. 開発環境構築手順
Electronを導入するにはnpmが、導入したElectronを動かすにはNode.jsが必要になります。「Node.jsって何だ?」「npmって何だ?」という方は以下記事などを参照してください。
Node.jsとnpmが導入できましたら、さっそくElectronの導入を始めていきましょう。まずはプロジェクト用に適当なディレクトリを用意し、当該ディレクトリに移動後、package.jsonを生成しておきます。
PS C:\Users\AGadget> Set-Location "C:\"
PS C:\> New-Item "test-project" -ItemType "Directory"
PS C:\> Set-Location ".\test-project"
PS C:\test-project> npm init --yes
準備できましたらElectronを導入します。パッケージのサイズは大きく、ダウンロードとインストールに1分ほどかかります。
PS C:\test-project> npm install electron
> core-js@3.8.1 postinstall C:\test-project\node_modules\core-js
> node -e "try{require('./postinstall')}catch(e){}"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> electron@11.1.1 postinstall C:\test-project\node_modules\electron
> node install.js
Downloading electron-v11.1.1-win32-x64.zip: [==================================================] 100% ETA: 0.0 seconds
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN test-project@1.0.0 No description
npm WARN test-project@1.0.0 No repository field.
+ electron@11.1.1
added 89 packages from 99 contributors and audited 89 packages in 41.182s
6 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
導入できましたら、念のために動作確認をしておきましょう。まずはElectronの本体となる実行ファイルが存在するか確認してください。node_modules\electron\dist\electron.exeが本体です。確認できましたら、シェルから適当にコマンドを叩いて、正常に応答するかも見ておきます。以下例では導入されたバージョン情報を確認しています。
PS C:\test-project> .\node_modules\.bin\electron --version
v11.1.1
ここまで問題なく動いていたら大丈夫です。
4. アプリの基本構造
Electronが導入できましたので、ここからはアプリの作り方と実行方法について言及します。
まずはElectron製アプリのエントリポイントとなる.jsファイルを用意します。
const electron = require("electron");
// Electron起動時に発火するイベントハンドラです。
electron.app.on("ready", () => {
// アプリの画面となるウィンドウを生成・表示します。
new electron.BrowserWindow();
});
node_modulesやpackage.jsonなどと同じ階層に配置します。
■ C:\
└ ■ test-project
├ ■ node_modules
├ ■ index.js
├ ■ package.json
└ ■ package-lock.json
そしてpackage.jsonが格納されたディレクトリのパスを第1引数にして、Electronを起動させます。
ここが1つ目の注意点です。Electronを起動させるときはpackage.jsonが格納されたディレクトリのパスを指定する必要があります。package.jsonを指定してはいけませんし、エントリポイントとなるとなる.jsファイル(この例だとindex.js)を指定してもいけません。
PS C:\test-project> .\node_modules\.bin\electron .\
問題がなければtest-projectというキャプションの、File・Edit・View・Window・Helpという5つの項目からなるメニューバーを備えた、真っ白なウィンドウが表示されるはずです。
これがElectronの起動方法、およびアプリの構造となります。アプリを停止するときはメニューバーにあるFileからExitを選択するか、そのままウィンドウを閉じてください。
4-1. UIを描画する
超基本となる動作が分かったところで、もう1歩踏み込んでUIを表示させてみましょう。
まずはUIを構成する以下3つのファイルを用意します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta content="width=device-width" name="viewport">
<title>テスト</title>
<link href="./test.css" rel="stylesheet">
</head>
<body>
<p id="click-point"></p>
<script src="./test.js"></script>
</body>
</html>
body {
background: rgb(255, 238, 238);
}
// 定数です。
const MESSAGE_1 = "テストだよっ!";
const MESSAGE_2 = "ホントかなぁ?";
// クリックされるたびに文章が切り替わるようにします。
const element = document.getElementById("click-point");
element.addEventListener("click", () => {
if (element.innerHTML === MESSAGE_1) {
element.innerHTML = MESSAGE_2;
} else {
element.innerHTML = MESSAGE_1;
}
});
element.click();
これらを以下のように配置します。
■ C:\
└ ■ test-project
├ ■ node_modules
├ ■ index.js
├ ■ package.json
├ ■ package-lock.json
├ ■ test.css
├ ■ test.html
└ ■ test.js
さらにindex.jsに処理を追加します。
const electron = require("electron");
electron.app.on("ready", () => {
// インスタンスを変数に格納しておきます。
const browserWindow = new electron.BrowserWindow();
// loadFile()を使って、ウィンドウに.htmlファイルを読み込ませます。
browserWindow.loadFile("./test.html");
});
ここまで準備できたら、もう一度Electronを起動させてみましょう。新たに追加したHTML・CSS・JavaScriptの通りに画面が描画されているはずです。
PS C:\test-project> .\node_modules\.bin\electron .\
4-2. Electronらしい機能を使ってみる
ここまでに実装した処理はElectronでなくとも、普通のフロントエンド技術で実現可能なことばかりです。わざわざElectronを使うからには、フロントエンド技術だけで作られたWebコンテンツでは実現できない機能――例えば、ファイルの操作などを実装したいところです。というわけで、今度は<input>タグに入力された文字列をファイルに追記していく処理を実装してみます。
まずは以下のようなファイルを用意します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta content="width=device-width" name="viewport">
<title>テスト Mk-2</title>
</head>
<body>
<input id="text-box">
<button id="button">追記</button>
<script src="./test-mk2.js"></script>
</body>
</html>
const fs = require("fs");
// ボタンが押されるたび、テキストボックスに入力されたメッセージをファイルに追記していきます。
const button = document.getElementById("button");
button.addEventListener("click", () => {
const FILE_PATH = "./test.txt";
const textBox = document.getElementById("text-box");
fs.appendFile(FILE_PATH, `${textBox.value}\n`, (error) => {
if (error) {
alert(error.message);
}
});
});
この2ファイルを以下のように配置します。
■ C:\
└ ■ test-project
├ ■ node_modules
├ ■ index.js
├ ■ package.json
├ ■ package-lock.json
├ ■ test-mk2.html
└ ■ test-mk2.js
index.jsのbrowserWindow.loadFile()
の引数も新たなファイル名に変更しておきます。
const electron = require("electron");
electron.app.on("ready", () => {
const browserWindow = new electron.BrowserWindow();
// ファイル名を変更しました。
browserWindow.loadFile("./test-mk2.html");
});
さて、これで実行するとどうなるでしょうか。実際に動かしてもらえば分かりますが、ボタンを押しても何も起こりません。何故ならば、Electron ver5.0.0以後、Electronの標準設定では、レンダラープロセスでNode.js特有の機能(require()
など)を利用できなくなったためです。
新たに登場した単語――レンダラープロセスとは何なのか。次項より解説します。
4-3. メインプロセスとレンダラープロセス
Electronの処理は、1つのメインプロセスと複数のレンダラープロセスから成ります。メインプロセスとはElectron起動時に呼び出した.jsファイルに記述された処理のことです。対して、レンダラープロセスとはメインプロセスから呼び出されたウィンドウごとの処理――あるいは、BrowserWindow.prototype.loadFile()で指定されたHTML・CSS・JavaScriptに記述された処理と言えます。
繰り返しなりますが、メインプロセスは常に1つであり、レンダラープロセスはウィンドウの数だけ存在します。レンダラープロセスが閉じられてもアプリが終了するとはかぎりませんが、メインプロセスが閉じるとアプリも終了します。ややこしく聞こえるかもしれませんが、触っていけば意外と単純な処理構造であることが分かると思います。
■ メインプロセス
├■ レンダラープロセス1
├■ レンダラープロセス2
:
:
:
└■ レンダラープロセスn
話を戻しますが、標準の設定に、レンダラープロセスからNode.jsの機能が呼び出せないように設定されているのは、セキュリティ面で問題があるためです。詳細は「Electron セキュリティ」とか「レンダラープロセス セキュリティ」といった検索条件で表示される記事群にお任せするとして、当記事では、この問題に対する対応方法を紹介していきます。
4-4. レンダラープロセスからNode.jsの機能を使う方法
レンダラープロセスからNode.jsの機能を使う方法は大きく2系統あります。
1つ目は、レンダラープロセスからNode.jsの機能を呼び出せるように設定を変更する、という至極単純な方法です。まずは改修前のコードをご覧ください。
const electron = require("electron");
electron.app.on("ready", () => {
const browserWindow = new electron.BrowserWindow();
browserWindow.loadFile("./test-mk2.html");
});
レンダラープロセスの設定を弄るにはBrowserWindowオブジェクトをインスタンス化するときに、第1引数に設定内容を記述したオブジェクトを渡します。
const electron = require("electron");
electron.app.on("ready", () => {
// この設定で、Node.js機能の利用が許可されます。
const setting = {
webPreferences: {
nodeIntegration: true
}
};
// インスタンス化時に第1引数に渡します。
const browserWindow = new electron.BrowserWindow(setting);
browserWindow.loadFile("./test-mk2.html");
});
上記のようにobject.webPreferences.nodeIntegration
をtrue
にすることで、レンダラープロセスからNode.jsの機能を簡単に利用できるようになりますが、セキュリティ的には不安が生じることを覚えておいてください。ちなみに、他にも色々な設定があります。詳しくはElectron公式ドキュメントを参照してください。
2つ目の方法は、処理を迂回させる――というか、必要な機能だけをレンダラープロセスに読み込ませるような方法です。以下記事に、その手順が大変丁寧に書かれていますので、そちらに誘導します。
ElectronでcontextBridgeによる安全なIPC通信 - Qiita
5. 配布する
ここまでの解説で、とりあえず何らかのアプリを作るための知識的基盤が整ったと判断します。
今度は作ったアプリを配布可能な状態にする方法を紹介します。
まず、現状を再確認しましょう。ここまでの解説によって作られる成果物はアプリと呼ぶには、あまりにも使いにくいものです。実行環境にNode.jsが入っていないと動きませんし、起動させるにしてもシェルからコマンドを叩かないと動きません(コマンドプロンプトとかPowerShellでスクリプトを組むという手もあります)。
いずれにせよ、全くユーザーフレンドリーな状態ではありません。アプリとして配布するには、Node.jsが入っていない環境でも実行できることと、ワンクリックで実行できる状態になっているべきです。
5-1. electron-builderを導入する
Electron製アプリを配布可能な状態にする(パッケージング)には、いくつかの方法がありますが一般的に使われているのはelectron-builderというnpmパッケージを利用する方法でしょう。古くはelectron-packagerというnpmパッケージが人気を博していたようですが、2020年現在はelectron-builderのほうが多機能らしいので、そちらの方法を紹介します。
まずはelectron-builderを導入します。
PS C:\test-project> npm install --save-dev electron-builder
ここで注意してもらいたいのはinstall
コマンドに続けて、--save-dev
オプション(あるいは-D
オプション)を付ける必要があることです。ご存じの方は多いでしょうが、当該オプションを付けて導入したnpmパッケージはNode.js側(およびnpm側)からは「このnpmパッケージはアプリを動かすのに必須ではないけれど、アプリ開発を助ける補助的なnpmパッケージなんだなぁ」と認識されます。
このオプションを付けて導入したnpmパッケージはpackage.jsonのdevDependenciesキーに追加されます。逆に、このオプションを付けずに導入した場合はdependenciesキーに追加されます。
{
"name": "test-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"electron": "^11.1.1"
},
"devDependencies": {
"electron-builder": "^22.9.1"
}
}
そして、ここからが大事なのですが、electron-builderはdevDependenciesキーに入っていないとエラーになる仕様になっています。そのため--save-dev
オプションを付けたわけです。
あるいは、導入時に--save-dev
オプションを付け忘れた場合は、導入後にpackage.jsonの内容を上記のように書き直せば問題ありません。
5-2. 実はelectronもdevDependencies
あと、elctron-builderを使ってパッケージングするときはelectronもdevDependenciesキーに入れておく必要があります。ですので、導入時に--save-dev
オプションを付けておくか、あるいは後ほどpackage.jsonを以下のように書き直しておいてください。
{
"name": "test-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"electron": "^11.1.1",
"electron-builder": "^22.9.1"
}
}
5-3. パッケージング
ここまで準備できましたら、ようやくパッケージングができます。方法は簡単で以下のようにコマンドを叩くだけです。
PS C:\test-project> .\node_modules\.bin\electron-builder build
実行すると、大量のメッセージが1分ほどかけて順番に表示されていきますので気長に待ちましょう。全てのメッセージが表示し終えて、プロンプトが再度表示されましたらパッケージング完了です。
PS C:\test-project> .\node_modules\.bin\electron-builder build
(node:7284) electron: The default of contextIsolation is deprecated and will be changing from false to true in a future release of Electron. See https://github.com/electron/electron/issues/23506 for more information
PS C:\test-project> .\node_modules\.bin\electron-builder build
• electron-builder version=22.9.1 os=10.0.19042
• description is missed in the package.json appPackageFile=C:\test-project\package.json
• writing effective config file=dist\builder-effective-config.yaml
• packaging platform=win32 arch=x64 electron=11.1.1 appOutDir=dist\win-unpacked
• downloading url=https://github.com/electron/electron/releases/download/v11.1.1/electron-v11.1.1-win32-x64.zip size=78 MB parts=8
• downloaded url=https://github.com/electron/electron/releases/download/v11.1.1/electron-v11.1.1-win32-x64.zip duration=26.644s
• default Electron icon is used reason=application icon is not set
• building target=nsis file=dist\test-project Setup 1.0.0.exe archs=x64 oneClick=true perMachine=false
• downloading url=https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-3.0.4.1/nsis-3.0.4.1.7z size=1.3 MB parts=1
• downloaded url=https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-3.0.4.1/nsis-3.0.4.1.7z duration=3.791s
• downloading url=https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-resources-3.4.1/nsis-resources-3.4.1.7z size=731 kB parts=1
• downloaded url=https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-resources-3.4.1/nsis-resources-3.4.1.7z duration=3s
• building block map blockMapFile=dist\test-project Setup 1.0.0.exe.blockmap
なお、electronかelectron-builder、あるいは両方を--save-dev
オプションを付けずに導入していると以下のようなエラーメッセージが表示されます。
PS C:\test-project> .\node_modules\.bin\electron-builder build
• electron-builder version=22.9.1 os=10.0.19042
• description is missed in the package.json appPackageFile=C:\test-project\package.json
⨯ Package "electron" is only allowed in "devDependencies". Please remove it from the "dependencies" section in your package.json.
PS C:\test-project> .\node_modules\.bin\electron-builder build
• electron-builder version=22.9.1 os=10.0.19042
• description is missed in the package.json appPackageFile=C:\test-project\package.json
⨯ Package "electron-builder" is only allowed in "devDependencies". Please remove it from the "dependencies" section in your package.json.
PS C:\test-project> .\node_modules\.bin\electron-builder build
• electron-builder version=22.9.1 os=10.0.19042
• description is missed in the package.json appPackageFile=C:\test-project\package.json
⨯ Package "electron" is only allowed in "devDependencies". Please remove it from the "dependencies" section in your package.json.
Package "electron-builder" is only allowed in "devDependencies". Please remove it from the "dependencies" section in your package.json.
個人的に、とても分かりやすいエラーメッセージで好感が持てます(上から目線)。
5-4. パッケージングされた成果物について
パッケージングに成功すると、package.jsonなどと同じ階層にdistというディレクトリが生成され、その直下に成果物が配置されます。成果物は以下のようになっています。
■ dist
├ ■ win-unpacked
├ ■ builder-effective-config.yaml
├ ■ test-project Setup 1.0.0.exe
└ ■ test-project Setup 1.0.0.exe.blockmap
win-unpackedというディレクトリには成果物が丸々入っています。パッケージングが正しく行えたかどうか確認するときなどに、このディレクトリの直下にある.exeファイル(ファイル名はプロジェクト名と同じになる)を起動させればアプリが立ち上がります。
残る3つのファイルですが、.yamlファイルと.blockmapファイルの用途は不明です。electron-buiderの公式ドキュメントのどこかに書いてあるのかもしれませんが面倒なので探していません。
それよりも重要なのはtest-project Setup 1.0.0.exeという実行ファイルです。これがelectron-builderでパッケージングしたElectronアプリのインストーラーとなります。アプリを配布するときは、このファイルを配布すれば問題なく使ってもらえるはずです。
ちなみに、このファイルを実行すると「インストールしますか?」などと聞くこともなく、すぐさまインストールが始まりますので注意してください。インストールされたファイルはC:\Users\ユーザー名\AppData\Local\Programsの直下に配置されます。また、デスクトップに実行ファイルまでのショートカットが自動的に配置される他、スタートメニューにも追加されます。
6. 個人的に躓いたところ
最後に、私がElectronを初めて触ったときに躓いた事柄を列挙して締めたいと思います。
以下内容は、これまでに解説した内容と被っている場合があります。
6-1. Electronがエントリポイントとなる.jsファイルを見つける仕組み
これまでの解説において、エントリポイントとなる.jsファイルはpackage.jsonと同じ階層に配置し、名前もindex.jsに固定してきました。ただ、人によっては「ソースファイルは、ソースファイル用のディレクトリのなかに格納しておきたい」や「index.jsという名前はindex.htmlと被って気持ち悪いから、main.jsというファイル名に変更したい」と考える方もいらっしゃるでしょう。私がそうです。
しかしながら、安易にそのようなことをしてはいけません。例えば、index.jsをmain.jsと改名してからElectronを立ち上げた場合、次のようなエラーメッセージがポップアップします。
Error launching app
Unable to find Electron app at C:\test-project
Cannot find module 'C:\test-project\index.js'. Please verify that the package.json has a valid "main" entry
意訳すると「Electron製アプリのエントリポイントとなるC:\test-project\index.jsが見つかりません。package.jsonのmainキーに指定されたファイルは本当に存在するか確認してください」ということになります。
このエラーメッセージから分かるように、Electron製アプリのエントリポイントとなる.jsファイルはpackage.jsonのmainキーに指定されたものが呼び出されるわけです。ファイル名や配置場所を変更した場合は書き直す必要があります。
6-2. Electron起動時に渡すもの
基本的に、Electron製アプリを立ち上げるときはpackage.jsonが格納されたディレクトリのパスを第1引数に指定します。
ただ、エントリポイントとなる.jsファイルを指定しても同じ結果になります(たぶん)。
PS C:\test-project> .\node_modules\.bin\electron .\index.js
まぁ、いくつかの記事を読んだかぎり、この方法を提示・推奨しているものは1つも見つけられなかったので、この方法を採用する意味は無いと思います。
6-3. Electron製アプリ内での相対パス
Electronに限らず、相対パスの取扱いは厄介なところがあります。例えば、./index.html
と指定した場合、その処理からの相対パスなのか、エントリポイントとなるファイルからの相対パスなのか、ルートパスと定義されるパスからの相対パスなのかで、相対パスの指定内容が変わってきます。
Electronの場合ですと、基本的にはpackage.jsonが格納されたディレクトリがルートとなります(ちなみに、electron.exeの第1引数に.jsファイルを指定した場合は当該ファイルからの相対パスになります)。
例を挙げましょう。例えば、以下のようなファイル構成だったとします。
■ C:\
└ ■ test-project
├ ■ node_modules
├ ■ index.js
├ ■ package.json
├ ■ package-lock.json
├ ■ test.css
├ ■ test.html
└ ■ test.js
このときindex.jsからtest.htmlを参照する場合、相対パスだと./test.html
となります。
ただし、以下のような配置の場合は./src/test.html
と書かないといけません。
■ C:\
└ ■ test-project
├ ■ node_modules
├ ■ src
│ ├ ■ index.js
│ ├ ■ test.css
│ ├ ■ test.html
│ └ ■ test.js
├ ■ package.json
└ ■ package-lock.json
index.jsとtest.htmlは同じ階層にあるのに、なんだか気持ち悪いですね。そのようなときは__dirnameという特殊な変数を参照することで直感的に書くことができます。この変数は、その変数を参照したファイルが配置されているディレクトリまでの絶対パスを格納しています。
この変数と、path.prototype.join()などを利用すれば比較的素直にファイルを指定できます。
const electron = require("electron");
const path = require("path");
electron.app.on("ready", () => {
const browserWindow = new electron.BrowserWindow();
browserWindow.loadFile(path.join(__dirname, "test.html"));
});