この記事について
この記事では、Electronを使ってデスクトップアプリを作成し、それを配布可能な状態にビルドするまでの過程を紹介します。
また、Electronアプリを作る際に、知っておくと便利な知識・ライブラリもあわせて紹介します。
使用する環境・バージョン
- OS : MacOS Mojave ver 10.14.5
- node.js v12.13.0
- npm 6.13.4
- electron 8.0.1
- electron-builder 22.3.2
前提条件
- node.jsとnpmは既にインストール済みで使用可能な状態とします。
読者に要求する前提知識
- 基本的なunixコマンドの意味がわかり、ターミナルで実行できること。
- Javascriptの基本的な文法がわかること。
Electronとは?
Githubによって開発された、クロスプラットフォームデスクトップアプリのフレームワークです。
クロスプラットフォームなので、Electronで作成したアプリは、MacでもWindowsでも動きます。
また、アプリの画面を作るにあたってhtmlとcss, jsといったWebフロントエンドの技術を使うので、Web系の知識がある人にとっては敷居が低いツールです。
参考:Electron公式ドキュメント
Electronのアプリケーションアーキテクチャ
Electronの仕組みは以下のようになっています。
引用:DeNA Engineers' Blog 「Electronのセキュリティは難しい?」
メインプロセス
アプリの画面ウィンドウを生成して、起動・終了などのアプリ本体の制御を行います。1つのアプリに対してメインプロセスは1つだけです。
アプリのウィンドウ生成をBrowserWindow
インスタンスの作成で行います。
Node.jsで動いています。つまり、npmモジュールや、ファイルの読み書きやネットワークなどのOS機能をAPI経由で使うことができます。
レンダラープロセス
メインプロセスで作成されたアプリ画面をChromiumでレンダリングして表示します。1つのアプリに対して複数個(=画面の数だけ)用意することができます。
アプリの画面レイアウト・装飾をhtml/CSS・JSで行います。
レンダラープロセスで使える機能は基本的にブラウザ上で動くJavascript(+α)です。
プロセス間通信(IPC通信)
メインプロセスとレンダラープロセス間でやりとりをする&レンダラープロセスから機能を呼び出すためには、IPC通信というものを使います。
IPC通信をするためには、メインプロセス側ではipcMain
モジュールを、レンダラープロセス側ではipcRenderer
モジュールをインポートする必要があります。
レンダラープロセス→メインプロセス
レンダラープロセス側からデータを送る場合は、ipcRenderer
モジュールのAPIを呼び出す形になります。
//ipcRendererモジュールをインポートconst{ipcRenderer}=require("electron");//メインプロセスのipcMain.on("test-send")に変数dataを送るipcRenderer.send("test-send",data);
メインプロセス側では、ipcMain
モジュールのAPIでそれを受け取ります。
//ipcMainモジュールをインポートconst{ipcMain}=require("electron");//レンダラープロセスから送られたdataの内容がargに格納されているipcMain.on("test-send",(event,arg)=>{//処理});
メインプロセス→レンダラープロセス
メインプロセス側からデータを送る場合は、基本的にレンダラープロセス側からのイベントに返信という形になります。
レンダラープロセスから送られたtest-sendイベントに、test-replyというチャネル名でdataを送ります。
const{ipcMain}=require("electron");ipcMain.on('test-send',(event,arg)=>{event.reply('test-reply',data)})
レンダラープロセス側では、ipcRenderer
モジュールのAPIでそれを受け取ります。
const{ipcRenderer}=require("electron");//メインプロセスから送られたdataの内容がargに格納されているipcRenderer.on('test-reply',(event,arg)=>{//処理})
参考:Electron アプリケーションアーキテクチャ
参考:ようこそ!Electron入門
参考:今日から始める Electron
ipcMain/Renderer
の関数
ipc通信をするためのAPIはipcMain/Renderer
モジュール内に他にも存在します。
詳しくは公式ドキュメントを参照してください。
ipcMain
:Electron公式ドキュメント ipcMainipcRenderer
:Electron公式ドキュメント ipcRenderer
アプリの作成
0.ディレクトリ・ファイル構造
一覧
アプリのソースを入れるフォルダを一つ作成してください(ここではapp
フォルダとします)。
今後ここがアプリのルートディレクトリになります。
$ mkdir app
このapp
フォルダの中に、最終的に以下のような構造になるようにファイルを配置します。
/app #アプリのルートディレクトリ
├─assets #アプリのアイコンを格納
│ ├─mac
│ │ └─icon_mac.icns #Mac用のアプリアイコン
│ └─win
│ └─icon_win.ico #windows用のアプリアイコン
├─dist #ビルドされたアプリの格納場所
├─node_modules
├─package.json
├─package-lock.json
└─src
├─main.js #メインプロセス
├─preload.js
├─index.html #レンダラープロセス
├─index.css #レンダラープロセス
└─index.js #レンダラープロセス
git管理に含めるファイル・含めないファイル
開発にあたり、ソースコードをgit管理したいという人もいるでしょう。
基本的には問題ないのですが、それをGithubにpushしたいと考えると、一部のファイルは管理対象外にした方が無難です。
というのも、Githubは大容量ファイルのpushを拒否するようになっているからです。(50MB超で警告、100MB超で拒否)
参考:Github公式ドキュメント 大容量ファイルの制限
そのため、以下のディレクトリは.gitignore
に追加しておきましょう。
dist
ビルドされたアプリ(数10MBになります)がこのディレクトリに入ります。当然重いです。node_modules
npmモジュールのソースがそのままここに入るので当然重くなります。
1.プロジェクトの作成
アプリルートディレクトリの中に、node.jsのプロジェクトを作成します。
$ cd app
$ npm init -y
このコマンドを実行すると、ルートディレクトリ中にpackage.json
ファイルが作成されます。
そのpackage.json
を以下のように編集します。
{"name":"your-app-name","version":"1.0.0","description":"your app's description","main":"src/main.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"keywords":[],"author":"your-name","license":"ISC"}
jsonの項目の意味は以下の通りです。
- name : アプリの名前。デフォルトだと
npm init
をしたディレクトリの名前 - version : アプリのバージョン。デフォルトは1.0.0
- description : アプリの説明。
- main : メインプロセスの相対パス。
デフォルトはindex.js
だが、Electronアプリではメインプロセスのファイル名はmain.js
とするのが一般的。 - scripts : 後述(アプリ起動の項で解説)。
- keywords : 今回はさして重要ではないので放置。(本来は
npm search
されたときの検索キーワード) - author : アプリの作者名。
- license : 配布時のライセンス。デフォルトはISC
参考:npm公式ドキュメント npm-package.json
参考:package.jsonの内容をまとめてみました
2.Electronのインストール
ルートディレクトリ直下で以下のコマンドを実行して、Electronをインストールします。
$ npm install-D electron
注意:インストールに時間がかかる場合がありますが、気長に待ちましょう。
注意:上記のようにdevDependenciesとして追加しないと、ビルド時にエラーが出ます。
この時点で、package-lock.json
と、node_modules
が作成されます。
3.メインプロセスの作成
スクリプト作成
src
フォルダの中に、メインプロセスのコードを記述するmain.js
を作成します。
$ mkdir src
$ cd src
$ touch main.js
そうして作成したmain.js
に以下のように書き込みます。
'use strict';//モジュールを使えるようにするconst{app,BrowserWindow}=require("electron");// メインウィンドウはGCされないようにグローバル宣言letmainWindow;//アプリの画面を作成functioncreateWindow(){mainWindow=newBrowserWindow({width:800,height:600,webPreferences:{nodeIntegration:false,contextIsolation:false,preload:__dirname+'/preload.js'}});mainWindow.loadURL('file://'+__dirname+'/index.html');}// Electronの初期化完了後に実行app.on('ready',function(){createWindow();});//アプリの画面が閉じられたら実行app.on('window-all-closed',()=>{// macOSでは、ユーザが Cmd + Q で明示的に終了するまで、// アプリケーションとそのメニューバーは有効なままにするのが一般的です。if(process.platform!=='darwin'){mainWindow=null;app.quit()}});app.on('activate',()=>{// macOSでは、ユーザがドックアイコンをクリックしたとき、// そのアプリのウインドウが無かったら再作成するのが一般的です。if(BrowserWindow.getAllWindows().length===0){createWindow()}});
このコード内では主に二つのモジュールを使用しています。
- app : アプリの起動・終了の制御を行う
- BrowserWindow : アプリ画面の制御を行う
参考:Electron公式ドキュメント 3分でわかるElectronアプリ開発
参考:30分で出来る、JavaScript (Electron) でデスクトップアプリを作って配布するまで
mainWindowの設定
new BrowserWindow
の作成の際に指定したオプション"webPreferences"の項目について解説します。
- nodeIntegration : レンダラープロセスでNode.jsの機能を使えるようにするか。false推奨
- contextIsolation : それぞれのプロセスを別々のJSコンテキストで実行するかどうか(詳細は
preload.js
の項目で解説)。 - preload : レンダラープロセス実行前に読み込まれるスクリプトを指定。
nodeIntegration: falseの重要性
先ほども述べたとおり、レンダラープロセスで使えるのは基本的にはブラウザ上で使えるJavascriptです。
しかし、ブラウザ上のJSにはセキュリティ上の理由で制限されている機能・実現不可能なことがあります。
例えば、<input type="file"/>
で設置されたフォームから入力されたファイルについて、JS側でvalue値を取得しようとするとファイル名のみが取得され、ローカルマシン上でのフルパスが入手できないようになっています。
参考:javascript - C:\ fakepathを解決する方法は?
そのため、例えば「ユーザーのローカルPC上にあるファイルを選択・中身を表示させるようなアプリを作るために、選択したファイルのパスをレンダラープロセスで取得したい」という場合は、Node APIを使う必要があります(Node.jsはサーバーサイドの環境なので、OSの機能を使うことができます)。
しかし、レンダラープロセス(=ユーザーが触れるアプリ画面(ブラウザ画面))でNode.jsの機能を使うことを認めてしまうと、クロスサイトスクリプティング(XSS)が発生することがあり危険です。
クロスサイトスクリプティングは、Webサイト閲覧者側がWebページを制作することのできる動的サイト(例:TwitterなどのSNSや掲示板等)に対して、自身が制作した不正なスクリプトを挿入することにより起こすサイバー攻撃です。
出所:クロスサイトスクリプティングとは?仕組みと事例から考える対策
つまり、アプリの画面からNode APIを使ってOSの機能を呼び出して、
- ファイルデータの改ざん・消去
- ローカルマシンの情報を取得・外部に送信
ということが可能になってしまいます。
実際に、ElectronのアプリでXSSを発生させ、ローカルマシンのデータを全消去させたという実験記事があるので挙げておきます。
ElectronアプリのXSSでrm -fr /を実行する
そのため、nodeIntegrationの値をfalseにしてレンダラープロセスでOSの機能にアクセスさせないことが推奨されているのです。
ここで、Electronアプリ開発の際に極めて重要な心構えが公式ドキュメントに記載されていたので、引用しておきます。
Electron で開発する時、Electron はブラウザではないということを意識することが重要です。 使い慣れたウェブ技術を使用して、機能あふれるデスクトップアプリケーションを構築できますが、あなたのコードの方がはるかに大きな力を発揮します。 JavaScript はファイルシステム、ユーザシェルなどにアクセスできます。 これはつまり、質の高いネイティブアプリケーションを作成することができる反面、あなたの書くコードに与えられた権限に応じて固有のセキュリティリスクが増加するということです。
それを念頭に置いて、信頼できないソースからの任意のコンテンツを表示するということは、Electron が扱うことを意図しない重大なセキュリティリスクを引き起こすということに注意してください。 実際、人気のある Electron アプリ (Atom、Slack、Visual Studio Code、等) は、主にローカル (あるいは信頼されており、なおかつ Node integration を使用しないリモート) のコンテンツを取り扱います。もしあなたのアプリケーションがオンライン上のリソースからコードを実行する場合、あなたの責任の下でそのコードが悪意のあるものではないことを確認する必要があります。
しかし、このままではNode.jsで提供されている多くの便利なnpmモジュールやElectron APIがレンダラープロセスで利用不可になってしまいます。
そのため、Node.jsのモジュールの中でレンダラープロセスで利用したいものを選び、そのモジュールだけを使用できるようにするという方法をとります。
4.preload.js
の作成
preload.js
の機能
nodeIntegrationの値をfalseのまま、レンダラープロセスでNode.jsのモジュールを利用できるようにする方法の一つにpreload.js
の作成があります。
preload.js
は、nodeIntegrationの値に関わらず、require('モジュール名')
でNode APIにアクセスすることができます。
そのため、「preload.js
で読み込んだモジュールをグローバルに共有→それをレンダラープロセスで使用」という形で、レンダラープロセスでのNode APIの利用を可能にできるのです。
(このモジュール共有を行うために、mainWindowの設定でcontextIsolationをfalseにする必要があります)
ファイル作成
src
フォルダの中に、preload.js
を作成し、例えば以下のように書き込みます。
$ mkdir src
$ cd src
$ touch preload.js
constelectron=require('electron');process.once('loaded',()=>{global.ipcRenderer=electron.ipcRenderer;global.app=electron.remote.app;});
このようにすることで、レンダラープロセスで以下のようにすることでipcRenderer
とapp
モジュールが使えるようになります。
//nodeIntegrationをfalseにしたことで、以下は使えなくなった//const {ipcRenderer, app} = require('electron');constipcRenderer=window.ipcRenderer;constapp=window.app;
参考:Electron で nodeIntegration: false にする方法
参考:Electron IPC通信を行う方法まとめ
参考:Electron Webviewのセキュリティで注意すべきこと
5.レンダラープロセスの作成
先ほどmain.js
の中で指定したレンダラープロセスindex.html
を、src
フォルダの中に作成します。
$ touch index.html
作成したindex.html
の中に、アプリの画面をhtmlで書いていきます。
ここでは動作確認をするために、hello,world!だけ記述します。
<h1>hello,world!</h1>
今後画面を装飾・機能を追加ということをしたい場合は、index.css
やindex.js
を読み込んでいけば実現可能です。
6.アプリの起動
起動コマンドを打つ
今の状態でアプリがどうなっているのかを、実際に起動して確かめてみましょう。
ルートディレクトリ直下のnode_modules/.bin
の中にelectron
コマンドが入っているので、それを実行します。
$ cd app
$ node_modules/.bin/electron .
すると、以下のような画面が立ち上がるはずです。
先ほどhtmlに書いたhello,worldが表示されていますので成功です。
起動ショートカットを設定する
アプリを起動するたびに、先ほどのような長いパスを打つのは面倒です。
そのため、簡単なショートカットで同様に起動できるようにpackage.json
に設定を追加しましょう。
{...(略)..."scripts":{"test":"echo \"Error: no test specified\"&& exit 1","start":"electron ."},...(略)...}
すると、以下のコマンドでアプリの起動が行えるようになります。
$ npm start
package.json
のscriptsの詳しい仕様については、以下の記事を参考にしてください。
参考:npm公式ドキュメント npm-scripts
参考:package.json の scripts
アプリ開発に使える便利機能
hello,worldができたら、自分の思うがままにアプリの機能をどんどん豊富にしていく段階です。
ここでは、Electronアプリ開発で使える便利な機能・ライブラリを紹介します。
デベロッパーツールの表示
アプリ画面のデバッグ等に使えるデベロッパーツールは、メインプロセス内に以下のコードを追記することで表示させることができます。
app.on('ready',function(){ ...(略)...mainWindow.webContents.openDevTools() ...(略)...});
追記することで、アプリ起動時にChromeのデベロッパーツールが表示されます。
注意:アプリをビルドして完成させ、配布するというときには該当箇所をコメントアウト・デベロッパーツールを非表示にさせることを忘れないでください。
よく使えるプロセスモジュール
プロセスモジュールを見れば、Electronでどんなことができるのかが大体わかります。
ここでは、公式ドキュメントに掲載してあるモジュールをざっくり紹介します。
モジュール名 | 説明 |
---|---|
autoUpdater | アプリの自動アップデート機能の追加(Mac,Winのみ) |
BrowserView | BrowserWindowに追加でウェブコンテンツを埋め込む子ウィンドウの制御 |
contentTracing | Chromiumからのトレースデータを収集 |
dialog | ローカルファイルの選択・新規作成・保存 |
globalShortcut | ショートカットキーの登録・操作の管理 |
inAppPurchase | Mac App Store のアプリ内購入機能の提供 |
net | HTTP/HTTPS リクエストの発行 |
netLog | ネットワークイベントのロギング |
Notification | OSのデスクトップ通知の作成 |
powerMonitor | PCの電源の状態を取得 |
powerSaveBlocker | システムの省電力モードの制御 |
protocol | カスタムプロトコルの登録 |
screen | 画面サイズ、ディスプレイ、カーソルの位置等の情報取得 |
session | ブラウザーセッション、クッキー、キャッシュ、プロキシの設定管理 |
systemPreferences | システム環境設定の取得 |
TouchBar | タッチバーレイアウトの作成 |
Tray | システムの通知領域にアイコンやコンテキストメニューを追加 |
webContents | ウェブページの描画・制御 |
desktopCapturer | デクストップのスクリーンショットやビデオキャプチャの制御 |
webFrame | ウェブページの描画のカスタマイズ |
clipboard | システムのクリップボードを利用したコピー・ペーストの操作提供 |
crashReporter | クラッシュレポートをリモートサーバーに送信 |
nativeImage | trayやDockやアプリケーションのアイコン画像ファイル作成 |
shell | デフォルトのアプリケーションを使用してのファイル・URL管理 |
アプリケーションメニューをつける
ウィンドウ上部に設定されるアプリケーションメニューを作るためには、Menu
やMenuItem
モジュールを使用します。
- Menu:Electron公式ドキュメント Menu
- MenuItem:Electron公式ドキュメント MenuItem
設置のためには、以下のコードをメインプロセス側に記述します。
//アプリケーションメニューconstMenu=electron.Menu//メニューバー内容lettemplate=[{label:'Your-App',submenu:[{label:'アプリを終了',accelerator:'Cmd+Q',click:function(){app.quit();}}]},{label:'Window',submenu:[{label:'最小化',accelerator:'Cmd+M',click:function(){mainWindow.minimize();}},{label:'最大化',accelerator:'Cmd+Ctrl+F',click:function(){mainWindow.maximize();}},{type:'separator'},{label:'リロード',accelerator:'Cmd+R',click:function(){BrowserWindow.getFocusedWindow().reload();}}]}]// Electronの初期化完了後に実行app.on('ready',function(){//メニューバー設置constmenu=Menu.buildFromTemplate(template);Menu.setApplicationMenu(menu);...(略)...});
すると、以下のようなアプリケーションメニューが表示されます。
(写真ではElectronとなっているところは、アプリをパッケージしたらYour-Appになります)
参考:JavaScript (Electron) を使ってアプリの見栄えを整える
組み込み式DBの導入(NeDB)
セットアップ
NeDBは、Javascriptで使える組込データベースです。APIはMongoDBのサブセットなので、Mongoを使ったことがある人にとっては扱いやすいかと思います。
インストールにはnpmを使います。
$ npm install--save nedb
実際に使うためには、preload.js
とレンダラープロセス側に以下を記述します。
constelectron=require('electron');process.once('loaded',()=>{global.app=electron.remote.app;global.Datastore=require('nedb');});
constapp=window.app;constDatastore=window.Datastore;constdb=newDatastore({filename:app.getPath('userData')+'/member.db',autoload:true});
注意:永続的なDBにするため指定するdbファイルのパスをNode APIのapp.getPath('userData')
でアプリケーションデータディレクトリを指定しないと、パッケージした後に動かなくなります。
参考:electron-vueのproductionビルドで気をつけるところ
基本操作
ここまで準備ができると、insertやfindなどの普通のDB操作が可能になります。
例えば、insertは以下のように行います。
vardoc={//examplefirst_name:Smith,last_name:Sam};db.insert(doc,function(err,newDoc){//処理});
他の操作については参考文献に譲ります。
参考:NeDB を使ってみた
参考:NeDBの基本
アプリのビルド
1.electron-builderのインストール
ルートディレクトリ直下で以下のコマンドを実行して、electron-builderをインストールします。
$ npm install-D electron-builder
注意:上記のようにdevDependenciesとして追加しないと、ビルド時にエラーが出ます。
2.アイコン画像の作成
アプリのアイコンを作成します。外部ツールを使って好きなようにデザインしてください。
ファイルの形式については以下の通りです。
- Mac用: icnsファイル
- Windows用: .icoファイル
参考:Electronの各Platform向けアプリアイコンを作成する
今回は、mac用のアイコンをassets/mac
に、Windows用のアプリをassets/win
に置きました。
3.ビルド設定を記述
package.json
にビルド用の設定を追加します。
この時、buildキーは一番上の階層(=nameやversionと同じ階層)に設置してください。
{...(略)... "build":{"appId":"com.electron.yourapp","directories":{"output":"dist"},"files":["assets","src","package.json","package-lock.json"],"mac":{"icon":"assets/mac/icon_mac.icns","target":["dmg"]},"win":{"icon":"assets/win/icon_win.ico","target":"nsis"},"nsis":{"oneClick":false,"allowToChangeInstallationDirectory":true}},...(略)...}
項目の意味は以下の通りです。
- appId : アプリのBundle ID。
- directories
- output : ビルドしたアプリの格納先
- files : ビルドに含めるファイル
- mac : Mac用にビルドするときの設定
- icon : アイコンファイルの相対パス
- target : パッケージ後のファイル形式
- win : Windows用にビルドするときの設定
- icon : アイコンファイルの相対パス
- target : パッケージ後のファイル形式
- nsis : インストーラ生成ツールNSISの設定
- oneClick : インストールから実行まで一気に行うかどうか
- allowToChangeInstallationDirectory : インストール先の変更を許可するかどうか
参考:electron-builder公式ドキュメント Common Configuration
参考:electron-builderでwindows用インストーラーを作る時の設定
4.ビルドコマンドを実行
ルートディレクトリ直下のnode_modules/.bin
の中にelectron-builder
コマンドが入っているので、それを実行します。
$ node_modules/.bin/electron-builder --mac--x64# Mac用のインストーラー(.dmg)が作成される$ node_modules/.bin/electron-builder --win--x64# Windows用のインストーラー(.exe)が作成される
コマンド実行後に、dist
ディレクトリ内にアプリのインストーラーが作成されていればビルド成功です。
- Mac用:
your-app-name-1.0.0.dmg
- Windows用:
your-app-name Setup 1.0.0.exe
このインストーラーを起動すれば、アプリがPCにインストールされて動き出します。お疲れ様でした。
参考:electronでリリース用パッケージを作る
参考:electron-builderを使ってdmgファイルを生成する