search
LoginSignup
294

More than 1 year has passed since last update.

posted at

updated at

Electronで1からデスクトップアプリを作り、electron-builderを使ってビルド・リリースするまで

この記事について

この記事では、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, JavaScriptといったWebフロントエンドの技術を使うので、Web系の知識がある人にとっては敷居が低いツールです。
参考:Electron公式ドキュメント

Electronのアプリケーションアーキテクチャ

Electronの仕組みは以下のようになっています。
electron-security-2.png
引用:DeNA Engineers' Blog 「Electronのセキュリティは難しい?」

メインプロセス

アプリの画面ウィンドウを生成して、起動・終了などのアプリ本体の制御を行います。1つのアプリに対してメインプロセスは1つだけです。
アプリのウィンドウ生成をBrowserWindowインスタンスの作成で行います。
Node.jsで動いています。つまり、npmモジュールや、ファイルの読み書きやネットワークなどのOS機能をAPI経由で使うことができます。

レンダラープロセス

メインプロセスで作成されたアプリ画面をChromiumでレンダリングして表示します。1つのアプリに対して複数個(=画面の数だけ)用意することができます。
アプリの画面レイアウト・装飾をHTML/CSS・JavaScriptで行います。
レンダラープロセスで使える機能は基本的にブラウザ上で動くJavaScript(+α)です。

プロセス間通信(IPC通信)

メインプロセスとレンダラープロセス間でやりとりをする&レンダラープロセスから機能を呼び出すためには、IPC通信というものを使います。
IPC通信をするためには、メインプロセス側ではipcMainモジュールを、レンダラープロセス側ではipcRendererモジュールをインポートする必要があります。

レンダラープロセス→メインプロセス

レンダラープロセス側からデータを送る場合は、ipcRendererモジュールのAPIを呼び出す形になります。

(レンダラープロセス)index.js
//ipcRendererモジュールをインポート
const { ipcRenderer } = require("electron");

//メインプロセスのipcMain.on("test-send")に変数dataを送る
ipcRenderer.send("test-send", data); 

メインプロセス側では、ipcMainモジュールのAPIでそれを受け取ります。

(メインプロセス)main.js
//ipcMainモジュールをインポート
const { ipcMain } = require("electron");

//レンダラープロセスから送られたdataの内容がargに格納されている
ipcMain.on("test-send", (event, arg) => {
  //処理
});

メインプロセス→レンダラープロセス

メインプロセス側からデータを送る場合は、基本的にレンダラープロセス側からのイベントに返信という形になります。
レンダラープロセスから送られたtest-sendイベントに、test-replyというチャネル名でdataを送ります。

(メインプロセス)main.js
const { ipcMain } = require("electron");

ipcMain.on('test-send', (event, arg) => {
  event.reply('test-reply', data)
})

レンダラープロセス側では、ipcRendererモジュールのAPIでそれを受け取ります。

(レンダラープロセス)index.js
const { ipcRenderer } = require("electron");

//メインプロセスから送られたdataの内容がargに格納されている
ipcRenderer.on('test-reply', (event, arg) => {
    //処理
})

参考:Electron アプリケーションアーキテクチャ
参考:ようこそ!Electron入門
参考:今日から始める Electron

ipcMain/Rendererの関数

IPC通信をするためのAPIはipcMain/Rendererモジュール内に他にも存在します。
詳しくは公式ドキュメントを参照してください。

アプリの作成

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を以下のように編集します。

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"
}

package.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に以下のように書き込みます。

src/main.js
'use strict';

//モジュールを使えるようにする
const {app, BrowserWindow} = require("electron");

// メインウィンドウはGCされないようにグローバル宣言
let mainWindow;

//アプリの画面を作成
function createWindow (){
  mainWindow = new BrowserWindow({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 : それぞれのプロセスを別々のJavaScriptコンテキストで実行するかどうか(詳細はpreload.jsの項目で解説)。
  • preload : レンダラープロセス実行前に読み込まれるスクリプトを指定。

nodeIntegration: falseの重要性

先ほども述べたとおり、レンダラープロセスで使えるのは基本的にはブラウザ上で使えるJavaScriptです。
しかし、ブラウザ上のJavaScriptにはセキュリティ上の理由で制限されている機能・実現不可能なことがあります。
例えば、<input type="file"/>で設置されたフォームから入力されたファイルについて、JavaScript側で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 を使用しないリモート) のコンテンツを取り扱います。もしあなたのアプリケーションがオンライン上のリソースからコードを実行する場合、あなたの責任の下でそのコードが悪意のあるものではないことを確認する必要があります。

Electron公式 セキュリティ、ネイティブ機能、あなたの責任

しかし、このままでは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
preload.js
const electron = require('electron');

process.once('loaded', () => {
    global.ipcRenderer = electron.ipcRenderer;
    global.app = electron.remote.app;
});

このようにすることで、レンダラープロセスで以下のようにすることでipcRendererappモジュールが使えるようになります。

(レンダラープロセス)index.js
//nodeIntegrationをfalseにしたことで、以下は使えなくなった
//const {ipcRenderer, app} = require('electron');

const ipcRenderer = window.ipcRenderer;
const app = window.app;

参考:Electron で nodeIntegration: false にする方法
参考:Electron IPC通信を行う方法まとめ
参考:Electron Webviewのセキュリティで注意すべきこと

5.レンダラープロセスの作成

先ほどmain.jsの中で指定したレンダラープロセスindex.htmlを、srcフォルダの中に作成します。

$ touch index.html

作成したindex.htmlの中に、アプリの画面をHTMLで書いていきます。
ここでは動作確認をするために、hello,world!だけ記述します。

src/index.html
<h1>hello,world!</h1>

今後画面を装飾・機能を追加ということをしたい場合は、index.cssindex.jsを読み込んでいけば実現可能です。

6.アプリの起動

起動コマンドを打つ

今の状態でアプリがどうなっているのかを、実際に起動して確かめてみましょう。
ルートディレクトリ直下のnode_modules/.binの中にelectronコマンドが入っているので、それを実行します。

$ cd app
$ node_modules/.bin/electron .

すると、以下のような画面が立ち上がるはずです。
Electron.png
先ほどHTMLに書いたhello,worldが表示されていますので成功です。

起動ショートカットを設定する

アプリを起動するたびに、先ほどのような長いパスを打つのは面倒です。
そのため、簡単なショートカットで同様に起動できるようにpackage.jsonに設定を追加しましょう。

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アプリ開発で使える便利な機能・ライブラリを紹介します。

デベロッパーツールの表示

アプリ画面のデバッグ等に使えるデベロッパーツールは、メインプロセス内に以下のコードを追記することで表示させることができます。

main.js
app.on('ready', function(){
 ...()...
  mainWindow.webContents.openDevTools()
 ...()...
});

追記することで、アプリ起動時にChromeのデベロッパーツールが表示されます。
devtool.png
注意:アプリをビルドして完成させ、配布するというときには該当箇所をコメントアウト・デベロッパーツールを非表示にさせることを忘れないでください。

よく使えるプロセスモジュール

プロセスモジュールを見れば、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管理

アプリケーションメニューをつける

ウィンドウ上部に設定されるアプリケーションメニューを作るためには、MenuMenuItemモジュールを使用します。

設置のためには、以下のコードをメインプロセス側に記述します。

main.js
//アプリケーションメニュー
const Menu = electron.Menu

//メニューバー内容
let template = [{
  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() {
  //メニューバー設置
  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);

  ...()...
});

すると、以下のようなアプリケーションメニューが表示されます。
(写真ではElectronとなっているところは、アプリをパッケージしたらYour-Appになります)
menu.png
参考:JavaScript (Electron) を使ってアプリの見栄えを整える

組み込み式DBの導入(NeDB)

セットアップ

NeDBは、JavaScriptで使える組込データベースです。APIはMongoDBのサブセットなので、Mongoを使ったことがある人にとっては扱いやすいかと思います。
インストールにはnpmを使います。

$ npm install --save nedb

実際に使うためには、preload.jsとレンダラープロセス側に以下を記述します。

preload.js
const electron = require('electron');

process.once('loaded', () => {
    global.app = electron.remote.app;
    global.Datastore = require('nedb');
});
(レンダラープロセス)index.js
const app = window.app;
const Datastore = window.Datastore;

const db = new Datastore({ 
    filename: app.getPath('userData')+'/member.db',
    autoload: true
});

注意:永続的なDBにするため指定するdbファイルのパスをNode APIのapp.getPath('userData')でアプリケーションデータディレクトリを指定しないと、パッケージした後に動かなくなります。
参考:electron-vueのproductionビルドで気をつけるところ

基本操作

ここまで準備ができると、insertやfindなどの普通のDB操作が可能になります。
例えば、insertは以下のように行います。

var doc = {
    //example
    first_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と同じ階層)に設置してください。

package.json
{
  ...(略)...
 "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ファイルを生成する

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
294