フューチャー技術ブログ

Electronの使い方 Web開発の技術でデスクトップアプリを作ろう


TIGの伊藤真彦です。

最近Electronを用いたアプリケーション開発を行っています。技術ブログで今まで取り扱った事のないテーマであるため、まずは入門記事を書いてみました。

Electronとは

Electronは、GitHubが開発したオープンソースのソフトウェアフレームワークです。

ChromiumとNode.jsをコアとして採用する事で、Web開発と同じようにHTML,CSS,JavaScriptを用いて開発したものを、デスクトップアプリケーションとしてビルドすることが可能になります。クロスプラットフォームであることも利点の一つであり、同一のソースコードからmacOS、Windows、Linuxへのアプリケーションビルドが可能です。

つまりWeb開発の技術でデスクトップアプリが作成できるものです。

Electronを使って開発されているもの。

Electronを使って開発されているアプリケーションの中でも有名なものは公式ページにリストアップされています。

とりわけ有名なものとして、Facebook MessengerSlackTwitch, 更にはVisual Studio Code等が存在します。

エンジニアの皆さんから、そうでない方まで、Electronのお世話になっていない人の方が少ないかもしれません。習得するとVisual Studio CodeのGithubリポジトリにコミットできるかも…ロマンを感じませんか?

Electronを使って開発する方法

Electronはnpmパッケージとして提供されています。

1
npm install electron

ReactやVue.js等のフロントエンドライブラリと組み合わせて使うようなユースケースが現在では一般的ですが、それらライブラリに依存しない形での開発も可能です。

electron-quick-startのリポジトリが参考になります。

git cloneから起動までを体験できる一連のコマンドは公式ページにも記載があります。

1
2
3
4
5
6
7
8
# Clone the Quick Start repository
$ git clone https://github.com/electron/electron-quick-start

# Go into the repository
$ cd electron-quick-start

# Install the dependencies and run
$ npm install && npm start

npm startコマンドでアプリケーションが実行されます、公式にしては少々素朴ですがHello Worldが表示されます。

proxy環境下でのElectronアプリの開発

proxy認証が必要なネットワークの場合、環境変数の設定及びnpmでのproxy設定をしないとnpm installが失敗します。

npm config-gオプションでグローバル設定にすることができますが、Electronのライブラリがグローバル設定を見に行かない場合があるため、うまく動かない場合はグローバルで設定した覚えがある方でも-gオプション無しで設定してみてください。

下記6つの設定を済ませればproxy環境下でも問題なく動きます。id、pass、proxyのドメインは適宜組み替えてください。

1
2
3
4
5
6
npm config set https-proxy http://id:pass@proxy.example.com:port
npm config set proxy http://id:pass@proxy.example.com:port
SET(linuxの場合export) ELECTRON_GET_USE_PROXY=true
SET GLOBAL_AGENT_HTTPS_PROXY=http://id:pass@proxy.example.com:port
SET HTTPS_PROXY=http://id:pass@proxy.example.com:port
SET HTTP_PROXY=http://id:pass@proxy.example.com:port

Vue.jsでElectronアプリを作る

個人的にはElectronアプリを作成する場合は、Vue.jsとの組み合わせがオススメです。

Vue CLI Plugin Electron Builderの存在が大きいです。このプラグインを導入することで、とても簡単に、Vue.jsで作成したWebアプリケーションをElectronアプリとしてビルドすることができます。

実際に試してみましょう。

まずは環境に応じてnodeをインストールし、npmコマンドが叩けるようになっている必要があります。Windowsの場合、nodejs公式サイトからインストーラをダウンロードし、インストールしてください。Macの場合、homebrew経由でバージョン管理ツールであるnodebrewのインストールを行うのが一般的です。

本記事ではnpmコマンドが利用できる状態となっていることを前提条件として取り扱います、詳細なインストール方法は割愛します。

npmコマンドが利用できるようになったら、Vue CLIをインストールします。

1
npm install -g @vue/cli

インストールが完了したら、Vue CLIを用いてVue.jsのプロジェクトを作成します。

プロジェクト用のフォルダやgitリポジトリを作成することを推奨します。

1
2
3
mkdir vue-cli-electron-sample
cd vue-cli-electron-sample
vue create vue-cli-electron-sample

Vue CLIの案内に従い、バージョンや構成をいくつか選択します。

全てDefault設定で問題ありません。

アプリケーションの構築が正常に完了すると画像のような案内が表示されます。

指示に従いひとまず起動してみましょう。

1
2
cd vue-cli-electron-sample
npm run serve

サーバーが立ち上がり、ブラウザでlocalhost:8080にアクセスするとHello Worldが表示されます。

ここまではVue.jsの説明ですね。このVue.jsアプリケーションをElectron化してみます。

npm run serveコマンドを叩いた場所と同じディレクトリで、vue addコマンドを使ってVue CLIプラグインをインストールします。

1
vue add electron-builder

Electronのバージョンを選択できます。

作りたいアプリケーションで利用する、その他のライブラリとの相性問題等が限りは最新バージョンで問題ないと思います。バージョンを選択するとビルドに必要なパッケージのインストールが始まります。ここでproxyに関連する設定を済ませておかないと、クイックスタートを動かす時と同様インストールに失敗します、ご注意ください。

インストールに成功すると、package.jsonに変更が加わるほか、background.jsが自動生成されます。

background.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
'use strict'

import { app, protocol, BrowserWindow } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
const isDevelopment = process.env.NODE_ENV !== 'production'

// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])

async function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION
}
})

if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
}
}

// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})

app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
createWindow()
})

// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}

ここがVue CLIプラグインの最大の利点です。

package.jsonにelectronに関連するコマンドが追記されます。

npm run serveの代わりにnpm run electron:serveコマンドを実行してみます。

npm run serveコマンドと似ていますが、Electronアプリケーションが起動されます。

表示内容はVue.jsのHello Worldです、これをElectronアプリとして起動することに成功しました。

ではこの状態のアプリケーションをビルドしてみましょう。

1
npm run electron:build

ビルドコマンドを実行するとアプリケーションのビルドが走ります。基本的にはアプリケーションをビルドしている端末のOS向けのアプリケーションがビルドされますが、WindowsでMac向けのアプリケーションをビルドするようなことも可能です。詳しくは公式ガイドをご確認ください。

ビルドに成功すると、dist_electronフォルダ配下に成果物が配置されます。通常のVue.jsアプリケーションのdistフォルダに相当します。

dist_electronフォルダ配下は画像のような状態です。

vue-cli-electron-sample Setup 0.1.0をダブルクリックするとアプリケーションのインストールが開始されます。ちなみに、アプリケーションのバージョン情報はpackage.json記載のバージョンに依存します。

インストールが完了したアプリケーションは一般的なアプリケーション同様に起動することができます。

vue.config.jsに、下記のように、ビルド設定をportableに設定することで、インストール不要な、ダウンロードしたファイルを直接実行、起動できるようなものとしてアプリケーションをビルドする事もできます。

ちょっとしたアプリケーションを毎回インストール、アンインストールするのは手間なので、個人的にはportable形式のアプリが好きです。ここに記載可能な設定は、electron-builderの公式ページに記載されています。

vue.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
win: {
target: [
{
target: 'portable', // 'zip', 'nsis', 'portable'
arch: ['x64'], // 'x64', 'ia32'
},
],
},
}
}
}
}

デフォルトの状態ではアイコンが無いため少々寂しいですが、デスクトップアイコンもvue.config.jsで指定することが可能です。

vue.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
win: {
icon: 'src/assets/icon.png',
target: [
{
target: 'portable', // 'zip', 'nsis', 'portable'
arch: ['x64'], // 'x64', 'ia32'
},
],
},
}
}
}
}

なお最小画像サイズ(256 x 256, macの場合512 x 512px)を下回るサイズの画像を指定した場合エラーが発生してビルドできません、ご注意ください。

アイコンを設定することでそれらしくなってきました。

portable形式でビルドしたものをダブルクリックすると、そのままアプリケーションが起動します。依存しているファイルなどは存在しないため、ビルド成果物の保存場所を移動したり、これだけを他のPCに配布しても問題なく動作します。ただし、何も設定をしないと信頼できないアプリケーションとして扱われてしまいます。

これを回避するためにはアプリケーションの署名が必要になります。仕事で開発する場合は避けては通れない部分ですね。

細かい設定項目はありますが、最小コストとしてはライブラリの導入だけでデスクトップアプリをビルドすることができました。

ReactでElectronアプリをビルドする

Vue.jsではライブラリを導入するだけでアプリケーションのビルドが可能でした。

Reactの場合はツールが自動でやってくれた部分を自力で整える必要があります。逆に言えばReactアプリケーションをElectronアプリとしてビルドできれば、大抵のものはElectronアプリケーションにできるといっても過言ではありません。
Vue.js同様Hello Worldから導入してみます。

React製アプリケーションを手早く構築するために、create-react-appを導入します。

1
npm install create-react-app

インストールに成功したら、新しくアプリケーションを構築します。

1
2
3
mkdir react-electron-sample
cd react-electron-sample
create-react-app react-electron-sample

アプリケーションの構築に成功すると画像のような案内が表示されます。
指示に従ってアプリケーションを起動してみましょう。

1
2
cd react-electron-sample
npm start

無事にHello Worldが起動しました。


Edit src/App.jsの文言を無視するのは忍びないので、Hello Worldらしいメッセージに修正しました。
pタグの文言を修正するだけです。

App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import logo from './logo.svg';
import './App.css';

function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Hello React with Electron
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}

export default App;

ここまででReactのHello Worldは完了です。
これをElectronアプリケーションに拡張します。

まずは愚直にelectronパッケージをインストールします。
npm startコマンドを実行したディレクトリと同じ場所で、下記コマンドを実行します。

1
npm install electron electron-builder --save-dev

Vue.jsの場合ライブラリが自動でやってくれた部分ですが、package.jsonに実行できるコマンド、およびビルドのために必要な設定項目を追記します。
Vue.jsではvue.config.jsにビルドオプションを記載する形を取ることができましたが、Vue CLIプラグインによる拡張が無いためpackage.jsonに直接記載する形になります。

jsonc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

{
"name": "react-electron-sample",
"version": "0.1.0",
"private": true,
"main": "main.js",
"build": {
"extends": null,
"files": [
"build/**/*",
"*.js"
],
"directories": {
"output": "dist_electron"
},
"win": {
"target": "portable"
}
},
"homepage": ".",
// 中略 //
"devDependencies": {
"electron": "^11.1.1",
"electron-builder": "^22.9.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"electron:serve": "electron .",
"electron:build": "react-scripts build && electron-builder",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
}

アプリケーション起動時のエントリポイントの設定に当たる"main": "main.js""main": "electron/starter.js"のように、ディレクトリを分けて整理するような事例など諸説確認しましたが、今回はクイックスタートの雰囲気に合わせようと思います。

加えて、Vue CLIプラグインの場合自動生成されたbackground.jsが担当してくれた部分を手動で書き換えます。

Vue CLIが自動生成したbackground.jsは若干Vue.js向けの拡張が為されているため、electron-quick-startの内容をベースにindex.htmlのパスなどを調整します。

main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Modules to control application life and create native browser window
const electron = require("electron");
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const path = require('path')

function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

// and load the index.html of the app.
mainWindow.loadFile('build/index.html')

// Open the DevTools.
// mainWindow.webContents.openDevTools()
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

上記ファイルはpreload.jsを読み込んでいるため、こちらも同様にディレクトリ直下に作成します。

preload.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}

for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})

package.jsonの修正、main.js, preload.jsの配置が完了したら、いよいよ起動です。

下記コマンドを実行するとElectronアプリケーションが起動します。

1
npm run electron:serve

無事に成功しました。

アプリケーションのビルドは下記コマンドです。

1
npm run electron:build

dis_electron配下にアプリケーションがビルドされました。
package.jsonに何も設定が無いとdistディレクトリに生成されます。

作業の抜け漏れ、typoの確認や、理想のディレクトリ構成を検討して迷うコストを鑑みると体感でVue.jsの3倍くらいの時間がかかる感触でした。

既存のReact製アプリをElectron化したい、Vueより慣れてるフレームワークがある、など事情はあると思いますので、どちらが良いかはチーム構成と趣味次第ですが、エッジケースの挙動や脆弱性の穴埋め等をライブラリ側である程度保証してくれるVue CLIプラグインの利点は大きいかなと感じています。

Vue.js(ブラウザ)でできてElectronアプリでできないこと

無いといっても良いかなというレベル感です。

Webアプリケーションとして一通りの機能を持ったアプリケーションをVue.js x Electronの組み合わせで作りました。最初は技術的実現性を調査しながら実装を進めていく形になりますが、Electronの問題でどうしてもできなかったという機能はありませんでした。少々の調査や努力が必要だった部分はあります。

当初心配だった部分の一例を紹介します。

httpクライアントライブラリ(axiosなど)を用いた外部APIへのアクセスは可能か

→問題なく可能だった

VueRouterを用いた画面遷移は可能か

少々設定が必要だが動いた

ファイルのダウンロード、生成は可能か

→問題なく可能だった

パスワード付きProxy認証を通すことは可能か

→ライブラリ側での担保はされないので自前で実装する必要があった

クライアント証明書を利用することは可能か

→可能、クライアント証明書が端末に複数インストールされているような場合は自前で選択処理を実装する必要があった

初めて触る場合は色々不安になると思いますが、想像以上に大丈夫でした。
これらハマりどころやtipsは今あるもの、今後気が付いたもの含め適宜記事にしたいと考えています。

Electronアプリの開発フロー

上記の通り、Vue.jsとして動かしているときは問題なかったがビルドすると動かなくなった、というケースは少ないです。(0ではありません)

そのため、基本は完全にWeb開発の流れで、Vue.jsのホットリロードを効かせた状態でWebアプリとして開発しています。機能が出来上がった段階でElectronアプリケーションとしてビルドし、ビルドが通り、正常に動作するか確認するような形で開発を行うことが可能です。Electronアプリケーションとして起動しつつ、ホットリロードを行うelectron-reloadのようなパッケージも存在します。万全を期すために、これを導入して常にElectronアプリケーションとして問題ないか検証しながら作業するようなフローにすることも可能かもしれません。導入コストや通常時の動作が重くなる可能性などを考慮し、私のチームでは現在導入していません。

まとめ

ElectronはWebの技術で開発可能な、デスクトップアプリケーションのフレームワークです。

クロスプラットフォームで、フロントエンドライブラリに依存しない、デスクトップアプリケーションの開発プラットフォームがElectronです。ライブラリの導入以外でWeb開発と開発手法が大きく異なるようなことはありません。

記事にするために整理したhello world状態のアプリケーション2種類をプライベートのgithubで公開しています。参考になれば、もしくはこれをベースに何かアプリケーションを作って頂ければ嬉しいです。

余談ですが、昨年のQiita クソアプリ Advent Calendar 2020でもElectronを使った開発事例がありましたね。

お仕事で役立つことはもちろんですが、ちょっとした便利ツールでも、クソアプリ Advent Calendar向けの小ネタでも、いつものWeb開発からちょっと趣向を変えて、デスクトップアプリケーションという選択肢を視野に入れていただければ幸いです。