Electronとは?
Electronは、HTML、CSS、JavaScriptを使って、MacとWindowsの両方で動くデスクトップアプリを作ることができるフレームワークです。
ElectronはChromiumとNode.jsがベースとなっており、Chromeで動くページであればそのままデスクトップアプリ化させることも可能です。
作成したデスクトップアプリは、MacのAppStoreやMicrosoftのストアで公開することも可能です。
Electronを使うメリット
- Webの言語でデスクトップアプリが作れる
- JavaScriptの資産が使える(React、Vueなども)
- 同じコードで、Mac版 Windows版が作れる
Electronを使うデメリット
- パッケージ後の容量が大きい
- ネイティブアプリよりも実行速度が遅い
Electoronが使われているアプリ
- VSCode
- Atom
- Facebook Messenger
- Slack
- Notion
環境構築
Electoronを使うには、Node.jsとnpmをインストールする必要があります。
今回はnodebrewを使ってインストールしてみます。
nodebrewのインストール
Homebrewでnodebrewをインストールします。
$ brew install nodebrew
次に、nodebrewを使って、安定版のNode.jsをインストールします。
インストール先のディレクトリがないとエラーとなってしまうため、~/.nodebrew/srcにディレクトリを作成してから、インストールを実行します。
$ mkdir -p ~/.nodebrew/src
$ nodebrew install stable
nodebrew lsでインストールされたバージョン確認します。
$ nodebrew ls
v16.14.2
current: none
currentがnoneとなっているので、バージョンを選択します。
$ nodebrew use v16.14.2
もう一度、nodebrew lsを実行します。
# nodebrew ls
v16.14.2
current: v16.14.2
currentに選択したバージョンが表示されました。
Pathを通します
$ vim ~/.zshrc
以下を追加します。
export PATH=$HOME/.nodebrew/current/bin:$PATH
.zshrcを再読み込みします。
$ source ~/.zshrc
nodeとnpmコマンドが使えるようになります。
$ node -v
v16.14.2
$ npm -v
8.5.0
Electronを動かしてみる
Electronのインストール
Electronのソースコードを置くディレクトリを作成して、npmパッケージを初期化します。
$ mkdir electron && cd electron
$ npm init
対話形式で色々と聞かれますが、とりあえず全てEnterでデフォルトで進めます。
$ npm install --save-dev electron
npm initを実行すると、プロジェクトのルートディレクトリに、package.jsonが追加されます。
{
"name": "electron",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
},
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^18.0.1"
}
}
package.jsonの"scripts"の欄に、Electronの起動時に必要なコマンドを追加します。
"start": "electron ."
を追加すれば動くようです。
{
"name": "electron",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "electron ." # この一文を追加
"test": "echo \"Error: no test specified\" && exit 1",
},
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^18.0.1"
}
}
index.jsを用意する
package.jsonのmainのフィールドに選択したファイルが最初に読み込まれるjsファイルです。
今回は、初期化時にindex.js
が設定されています。
ルートディレクトリにindex.jsを作成します。
このファイルに、起動時にウィンドウを開いたり、読み込むhtmlファイルを指定する処理を追加していきます。
const { app, BrowserWindow } = require('electron')
const createWindow = () => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
index.jsのmainWindow.loadFile('index.html')
を変更すれば、メインのwindowとして開かれるhtmlの置き場所を変更できます。
今回はデフォルトのまま進めますので、ルートディレクトリにindex.html
を用意します。
index.htmlを用意する
お馴染みのHello World!のテンプレートを用意します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
アプリを起動してみる
これで、Electoronを動かす準備が完成しました🎉
以下のコマンドで起動してみましょう。
$ npm start
そうすると、Electronのウィンドウが開き、無事Hello Worldのタイトルが表示されました。
意外とあっさり起動できましたね。
続けて、メインの処理を書いていきます。
HTMLを編集してみる
ブラウザと同じようにindex.htmlファイルがElectronのアプリ上で描画されるため、全てのHTMLタグはもちろん、CSSでデザインを整えたり、JavaScriptを実行することもできます。
試しに、以下のようなHTMLを用意しました。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>メモ帳</title>
<style>
h1 {
text-align: center;
font-size: 18px;
margin-top: 23px;
}
textarea {
width: 340px;
height: 380px;
margin: 0 auto;
display: block;
border: 2px dashed #ccc;
border-radius: 16px;
padding: 16px;
resize: none;
}
button {
width: 370px;
display: block;
margin: 16px auto 0;
border-top: none;
border-left: none;
border-right: none;
border-bottom: 4px solid #00000012;
background: #5ca0e6;
color: white;
font-weight: bold;
height: 34px;
border-radius: 6px;
cursor: pointer;
}
</style>
<script>
function onClickHandler() {
alert("保存機能はまだできてません!");
}
</script>
</head>
<body>
<h1>メモ帳</h1>
<textarea>きょうはハンバーグをたべました。</textarea>
<button onclick="onClickHandler()">保存</button>
</body>
</html>
ファイルの変更を適用するには、再度npm start
する必要があります。
$ npm start
アプリが開くと、htmlの変更が反映されました。
html内に追加したCSSがきちんと反映されています。
保存ボタンをクリックすると、JavaScriptが実行され、無事にアラートが表示されました。
テキストの保存機能を作ろうかと迷いましたが、先にパッケージ化できることをテストしてみます。
MacOS用アプリ、Windows用アプリとして書き出す
上記の段階でもローカルでの実行はできますが、他人に配布して起動できるようにするには、.appファイルや、.exeファイルとして書き出す必要があります。
今回は、Electron Forgeを使ってみます。
Electron Forgeをインストール
Electron Forgeを開発用のdependencyとしてインストールします。その後、importコマンドを実行することで、初期設定が完了です。
プロジェクトのルートディレクトリで以下を実行します。
$ npm install --save-dev @electron-forge/cli
$ npx electron-forge import
アプリを書き出す
npm run make
コマンドで、書き出しが始まります。
$ npm run make
outディレクトリにファイルが保存されます
名称やアイコン画像を変更していませんので、デフォルトの表示となっていますが、書き出しに成功しました。
生成されたファイル- out/electron-darwin-x64/electron.app
を実行してみると、見事に作成したアプリが開きました。
容量を確認してみると196MBと、ペラペラのアプリにしては意外と大きくなりました。
Mac版のChromeをダウンロードしてみるとファイルサイズは約180MBでしたので、Chromiumを使ったElectronのファイルサイズが大きくなってしまうのは仕方がなさそうです。
Electronで使える便利なAPI
autoUpdater
アプリを自動でアップデートするのに使える。
clipboard
クリップボード(コピーやペースト)の機能が使える。
const { clipboard } = require('electron')
clipboard.writeText('Example string', 'selection')
console.log(clipboard.readText('selection'))
crashReporter
クラッシュレポート(アプリのフリーズや突然終了などのレポート)をリモートのサーバーに送れる。
const { crashReporter } = require('electron')
crashReporter.start({ submitURL: 'https://your-domain.com/url-to-submit' })
dialog
システムダイアログ、ファイルを開いたり、保存したり、アラートを出したりできる。
const { dialog } = require('electron')
console.log(dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] }))
globalShortcut
グローバル(キーボードフォーカスが当たっていない状態も含む)でのキーボード入力を検知できる。
const { app, globalShortcut } = require('electron')
app.whenReady().then(() => {
const ret = globalShortcut.register('CommandOrControl+X', () => {
console.log('CommandOrControl+X is pressed')
})
})
inAppPurchase
MacのAppStoreで使える課金機能。
Menu
上部のアプリケーションメニューを設定できる。
const { app, Menu } = require('electron')
const isMac = process.platform === 'darwin'
const template = [
// { role: 'appMenu' }
...(isMac ? [{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}] : []),
// { role: 'fileMenu' }
{
label: 'File',
submenu: [
isMac ? { role: 'close' } : { role: 'quit' }
]
},
// { role: 'editMenu' }
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
...(isMac ? [
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [
{ role: 'startSpeaking' },
{ role: 'stopSpeaking' }
]
}
] : [
{ role: 'delete' },
{ type: 'separator' },
{ role: 'selectAll' }
])
]
},
// { role: 'viewMenu' }
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
// { role: 'windowMenu' }
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
...(isMac ? [
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' }
] : [
{ role: 'close' }
])
]
},
{
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://electronjs.org')
}
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
Notification
OSの通知機能が使える。通知をクリックした後の動作も指定可能。
powerMonitor
バッテリーが使われているか、ACアダプターで節電されているか、などが分かる。
systemPreferences
システム環境設定の値を取得できる。ダークモードやアクセントカラーの設定など。
const { systemPreferences } = require('electron')
console.log(systemPreferences.isDarkMode())
TouchBar
(macのみ)タッチバーの表示をカスタムできる。
Electronのセキュリティ面
Electronは、JavaScriptを使ってOS側に近い処理(ローカルデバイスへの接続、通知、ローカルのファイルシステムへのアクセス等)を書けます。これは非常に便利な一方で、危険な側面もあります。
例えば、悪意のあるJavaScriptが埋め込まれたサイトを脆弱性のあるElectronアプリで開くと、レンダラー側からNode.jsを経由してOSレベルの危険の操作(個人情報の流出や、ウィルスの埋め込みなど)が行われる可能性があります。
そのため、Electron v12からは、レンダラーからNode.jsが実行できないような仕組み(Context Isolation)が搭載されており、レンダラーからNode.jsの機能を使う場合には、「contextBridge」というモジュールを使う必要があります。
contextBridge
preload.js内で、レンダラーに開放したいAPIをcontextBridgeを使ってホワイトリスト形式で指定します。
以下の例は、レンダラーからメインプロセスの処理(ウィンドウのタイトルの変更)を呼び出す場合の書き方です。
const mainWindow = new BrowserWindow({
webPreferences: {
// preload.jsを読み込み
preload: path.join(__dirname, 'preload.js')
}
})
// ウィンドウのタイトルを変更
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
const { contextBridge, ipcRenderer } = require('electron')
// 表示側に開放するAPIを定義
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
// 表示側でAPIを呼び出し
window.electronAPI.setTitle('こんにちは')
レンダラーからメインプロセス(一方向)
ipcMain.on() // main.jsで使用
ipcRenderer.send() // preload.jsで使用
レンダラーからメインプロセス(双方向)
ipcMain.handle() // main.jsで使用
ipcRenderer.invoke() //preload.jsで使用
メインプロセスからレンダラー(一方向)
BrowserWindow.webContents.send // main.jsで使用
ipcRenderer.on // preload.jsで使用
Electronについて理解ができたら
Electronについての概要をざっくり掴めたところで、実践的にデスクトップアプリを作ってみたいと思います。
次の記事では、 実際にReactとElectoronを使ってMacとWindowsで動くToDoアプリを作成します。
関連記事
よかったら、こちらの記事も読んでみてください。
reactでポケモン風RPGゲームを作ってみよう!戦闘画面編
https://qiita.com/udayaan/items/38680c63ed034503eac0
Electron + Reactでデスクトップアプリを作ろう!
https://qiita.com/udayaan/items/2a7c8fd0771d4d995b69
話題の最新フロントエンドフレームワーク「Astro」を使ってみた
https://qiita.com/udayaan/items/24ecb2f5f4608fc1df4c