Electronでアプリケーションを一つ作った時のメモ

この記事を書いたひと: @t4traw 2019年3月11日

ちょっとお仕事でElectronを触る機会があり、今後も使う事があると思うので、自分のためのTip集というか覚書。WEB系の技術のみでネイティブみたいなアプリケーションが作れるのは感動しましたね。

自分はMacなので、Windowsで開発となると微妙に違う部分があるかもしれません。自分の場合はMacで開発してWindows用のアプリケーション(ポータブルなexe)をビルドしました。また、nodejsのインストールはすでにしてある物とします。

まず必要なファイルの準備

とりあえず、initをします。余談ですが、最近このyオプションを知りました。なぜもっと早くしっておかなかったのか😭

$ npm init -y

package.jsonができたので、さっそくelectronをインストールします。

$ npm i electron -D

で、次にsrcディレクトリを作って、その中に『main.js』『index.html』『package.json』を作成します。「これってわざわざsrcディレクトリにする必要あるん?ルートに置くじゃだめなの?」と思っていましたが、最終のビルドをする時にこれが原因で躓いたので、ひとまず最初は無難に作成しておくことをオススメします。

$ mkdir src
$ touch src/main.js src/index.html src/package.json

ひとまずこのファイル構成でhello worldができます。

メインプロセスとレンダラープロセスがある

Electronにはメインプロセスとレンダラープロセスがあって、メインは名前の通りアプリケーションの核となるプロセスで、いわゆるNodejsな空間、レンダラーは1つの画面毎に生成されるプロセス(自分の中ではView用のjsと解釈)。

具体的に何が違うかっていうとアクセスできるAPIに違いがあります。

Electron Documentation Docs / API

これはChromeのAPI拡張を開発する時にも同じだったので、すんなり受け入れる事ができました。

とりあえず簡単なhello world

ひとまず簡単なhello worldをします。

src/main.js
const {app, BrowserWindow} = require('electron')
const url = require('url')
const path = require('path')

let appWindow

function initApp () {
  appWindow = new BrowserWindow({
    width: 800,
    height: 600
  })
  appWindow.loadURL(
    url.format({
      pathname: path.join(__dirname, 'index.html'),
      protocol: 'file:',
      slashes: true
    })
  )
}

app.on('ready', initApp)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (appWindow === null) {
    initApp()
  }
})

基本的な部品をrequireして、Windowを1つ作るだけ。次に実際のview。

src/index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Electron Helloworld</title>
</head>

<body>
<h1>Hello world</h1>
</body>
<script>

</script>
</html>

で、最後にメインプロセスはmain.jsですよとpackage.jsonに書きこきます。

src/package.json
{
  "main": "main.js",
  "name": "electron_helloworld",
  "version": "0.0.1",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
}

あ、あと最終的にビルドする時に必要なので、こちらに同じバージョンのelectronをインストールしておきます。

$ npm --prefix ./src install electron-store ./src -D

ルートディレクトリに戻り、これで実際に実行します。

$ npx electron src

無事起動しました。

メインプロセスとレンダラープロセスでデータのやり取り

メインプロセスとレンダラープロセスでのデータのやり取りはipcを通じて行います。

レンダラープロセスである要素をクリックしたら特定の文字列を返すようにしてみます。そうですね……。数字が入力されたらオラオラする感じにしてみましょうか。

こんな機会でしか使えないのでcsshakeをインストールします。

プログラムの中で使うものはsrcディレクトリ内でインストールしておきます。

$ npm --prefix ./src install css-shake ./src

とりあえずレンダラープロセスにeventを追加します。

src/index.html
<link rel="stylesheet" href="./node_modules/csshake/dist/csshake.min.css">
<style>
  span {
    float: left;
  }
</style>

<!-- 中略 -->

<script>
const { ipcRenderer } = require('electron')

function submitNum (num) {
  ipcRenderer.send('submitNum', num)
}

ipcRenderer.on('renderOra', (event, args) => {
  newNode = document.createElement('span')
  newNode.classList.add('shake-hard')
  newNode.classList.add('shake-constant')
  newNode.innerText = args
  document.getElementById('app').appendChild(newNode)
})
</script>

次にメインプロセス側を書きます。受け取った数字回数分、小分けでレンダラープロセスに送信します。

src/main.js
function sleep (milsec) {
  return new Promise(resolve => setTimeout(resolve, milsec))
}

async function renderOra (args) {
  for (i = 0; i < args; i++) {
    appWindow.webContents.send('renderOra', 'オラ')
    await sleep(100)
  }
}

ipcMain.on('submitNum', function (event, args) {
  renderOra(args)
})

これで起動すると……

いい具合にオラオラしていますね。

ローカルにデータを保存する(localStorageみたいなキャッシュ)

ブラウザでいうlocalStorageみたいなキャッシュを保存するには、electron-storeがシンプルで便利でした。

sindresorhus/electron-store

const Store = require('electron-store')
const store = new Store()

store.set('foo', 'bar')
console.log(store.get('foo'))
// => bar

json = {
  'num': 123
}
store.set('hoge', json)
console.log(store.get('hoge'))
// => {"num": 123}

console.log(store.path)
// => /Users/t4traw/Library/Application Support/electron_helloworld/config.json

フルスクリーン&kioskモードで起動する

フルスクリーンかつ操作を受け付けないkioskモードも簡単にできました。また、kioskモードにするとアプリの終了などが大変なので、ショートカットキーを設定。

appWindow = new BrowserWindow({
  width: 800,
  height: 600,
  kiosk: true,
  'fullscreen': true,
  frame: false
})

let appQuit = globalShortcut.register('ctrl+q', function () {
  app.quit()
})

あとカーソルを非表示に(これはcssだけど)。

* {
  cursor: none;
}

tmpディレクトリの取得方法

ファイルなどのダウンロードに一時的なディレクトリ(tmpディレクトリとかテンポラリっていうやつ)が必要になります。そのときは

app.getPath('userData')

などで実行環境(WinやMac)など意識せずにパスの取得ができます。

ビルドはelectron-build

ビルドに関してはelectron-packagerとかasarとかありますが、他人に配布するという用途ではelectron-buildを使えばOKです。

electron-userland/electron-builder

$ npm i electron-build -D

で、srcディレクトリの中にビルド用のスクリプトを作成します。

src/build-win.js
'use strict';

const builder = require('electron-builder');
const fs = require('fs');
const packagejson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));

builder.build({
  platform: 'win',
  config: {
    'appId': `com.example.${packagejson.name}`,
    'win': {
      'target': 'portable',
      "icon": "icon.ico",
    },
  },
});

今回はサクっとexe単体で使えるようにしたかったので、portableに。あとicoファイルを指定する事でアイコンの設定などができます。

ビルドする時に同梱したいパッケージなどがsrc/node_modulesに入っていないとダメなので注意してください。

$ node src/build-win

これでdistディレクトリの中にexeが生成されていると思います。


ひとまず自分が気になった事を走り書きしてみました。今回書いたコードをリポジトリにまとめておきました。といってもただ「オラオラ」してるだけのモノですがw

t4traw/electron_helloworld

それでは。