23
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Electronを使ってMacとWindowsで動くアプリを作ってみる

Last updated at Posted at 2022-04-04

Electronとは?

screen.jpg

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が追加されます。

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 ."を追加すれば動くようです。

package.json
{
  "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ファイルを指定する処理を追加していきます。

index.js
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!のテンプレートを用意します。

index.html
<!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のタイトルが表示されました。
electron.jpg
意外とあっさり起動できましたね。
続けて、メインの処理を書いていきます。

HTMLを編集してみる

ブラウザと同じようにindex.htmlファイルがElectronのアプリ上で描画されるため、全てのHTMLタグはもちろん、CSSでデザインを整えたり、JavaScriptを実行することもできます。

試しに、以下のようなHTMLを用意しました。

index.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がきちんと反映されています。
css_check.jpg

保存ボタンをクリックすると、JavaScriptが実行され、無事にアラートが表示されました。
js_check_.jpg

テキストの保存機能を作ろうかと迷いましたが、先にパッケージ化できることをテストしてみます。

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ディレクトリにファイルが保存されます

output.jpg
名称やアイコン画像を変更していませんので、デフォルトの表示となっていますが、書き出しに成功しました。
生成されたファイル- out/electron-darwin-x64/electron.appを実行してみると、見事に作成したアプリが開きました。

css_check.jpg

容量を確認してみると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を使ってホワイトリスト形式で指定します。
以下の例は、レンダラーからメインプロセスの処理(ウィンドウのタイトルの変更)を呼び出す場合の書き方です。

main.js
  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)
  })
preload.js
const { contextBridge, ipcRenderer } = require('electron')

// 表示側に開放するAPIを定義
contextBridge.exposeInMainWorld('electronAPI', {
    setTitle: (title) => ipcRenderer.send('set-title', title)
})
renderer.js
// 表示側で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

23
20
0

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
  3. You can use dark theme
What you can do with signing up
23
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?