Electron アプリをつくる時に便利なパッケージ

この記事は Electron アドベントカレンダー2016 の13日目の記事です.

本記事では,僕が Electron アプリをつくる上で便利だったり,ほしかったのでつくったりしたパッケージを7つほど紹介します.

  1. electron-about-window
  2. electron-dl
  3. electron-in-page-search
  4. electron-window-state
  5. menubar
  6. node-auto-launch
  7. electron-mocha

electron-about-window

electron-about-window は 'このアプリについて' ウィンドウを簡単にクロスプラットフォームにつくるためのパッケージです.下記のように関数を1つインポートして呼び出すだけで「このアプリについて」ウィンドウを生成することができます.(example

import openAboutWindow from 'about-window';
openAboutWindow({ icon_path: 'path/to/icon.png' });

アイコンファイルへのパスはどうしても必要になってしまうため手で指定する必要がありますが,その他のバージョンやデスクリプションは package.json から引っ張ってきてくれるので特に指定する必要はありません.

例えば Shiba だと下記のようなウィンドウが生成されます.

f:id:rhysd:20161211191411p:plain

ウィンドウのライフサイクルなどはすべてライブラリ側でハンドルされるので,特にライブラリユーザ側が気にする必要はありません.

TypeScript も対応済みです.

electron-dl

Electron アプリ中で何かのファイルをローカルにダウンロードしたい時,やり方は2通りあります.

  1. Chrome のネイティブなダウンロード機能を Electron が提供する API で利用する
  2. ダイアログのみネイティブな API (openSaveDialog) を使って,ダウンロードや保存は Node.js の API で頑張る

できれば 1. でやりたいところですが,Electron のダウンロード関連の API は色々なところに API が分散していて使いにくいです.例えばダウンロードを開始する downloadURLWebContents に,ダウンロードの再開は Session に,ダウンロード後の Dock の挙動(macOS)は app.dock にあったりします.また,Chrome のダウンロードはダイアログを出すかどうかの制御や一時中断・再開,など一通りのことができるので複雑です.これは Electron が ChromiumC++ API を割とそのまま JavaScriptAPI として見せているためです.

electron-dl はこれらの使いづらい API を裏に隠し,簡単にファイルのダウンロードを行えるようにしてくれます.

import {download} from 'electron-dl';

const win = new BrowserWindow();

const opts = {
    // 保存先ディレクトリ.ダイアログを開く場合はこのディレクトリが初期値になる
    directory: '/path/to/some/direcotry',

    // 'save as' ダイアログを出すかどうか.出さない場合は上記の 'directory' プロパティ
    // で指定する.何も指定しなければデフォルトのダウンロードディレクトリ(~/Downloads など)
    // が使われる.
    saveAs: true
};

// https://example.com/some/file をダウンロードする(opts は省略可)
download(win, 'https://example.com/some/file', opts)
    .then(dl => {
        console.log('Download successfully completed', dl.getSavePath());
    })
    .catch(err => {
        console.error('Download failed', err.message);
    });

これだけでダウンロードが中断された場合の面倒などを含めたファイルのダウンロードをすべて行ってくれます.また,macOS のダウンロード中の挙動(Dock アイコンへのプログレスバーの表示や完了時の Dock アイコンのバウンス)もやってくれます.なおダウンロード完了後に return される値(上記コードの dl)は DownloadItem インスタンス です.

electron-in-page-search

Electron には Chrome のページ内検索を実行する API があり,WebContents インスタンス.findInPage() メソッドや found-in-page イベントを組み合わせて使えます.ですが,検索窓が別のネイティブなウィンドウで実装されていること前提になっていたり,BrowserWindow<webview> で挙動が違ったりと色々落とし穴があります.これはダウンロード周りと同様に Electron が ChromiumC++ API を割とそのまま JavaScriptAPI として見せているためです.この辺は以前メモしたりしました.

Electron で Chrome のページ内検索機能を使う

そこで,それらの落とし穴や検索の状態を気にせずページ内検索をアプリに組み込むためのモジュールとしてつくられたのが electron-in-page-search です.

これは exampleスクリーンショットです.ボタンを押すと検索窓が現れ,そこに検索ワードを入れて Enter キーや 次/前 検索ボタンで検索できます.検索窓のスタイルは CSS などで細かく制御できます.

import searchInPage from 'electron-in-page-search';
import {remote} from 'electron';

const inPageSearch = searchInPage(remote.getCurrentWebContents());

document.getElementById('some-button').addEventListener('click', () => {
    inPageSearch.openSearchWindow();
});

前日の joe-re さんの記事にもあったように,Electron のネイティブな API のテストは色々面倒ですが,electron-in-page-search はすでに各プラットフォーム(OS X, Linux, Windows)でテスト済みなので安心できます.

TypeScript も対応済みです.

electron-window-state

アプリがウィンドウを生成する時に,毎回同じサイズ・位置で生成するのではなく以前ユーザが作成したウィンドウの位置を覚えておいてほしい事があります.ブラウザウィンドウのサイズはレンダラプロセス作成前に知る必要があるので,アプリディレクトリに JSON ファイルなどで保存しておく必要があります.

Electron アプリのウィンドウサイズ&ポジションを復元する

electron-window-state はその辺りをやってくれるパッケージです.

const windowStateKeeper = require('electron-window-state');
let win;

app.on('ready', () => {
  // Load the previous state with fallback to defaults
  const state = windowStateKeeper({
    defaultWidth: 1000,
    defaultHeight: 800
  });

  win = new BrowserWindow({
    x: state.x,
    y: state.y,
    width: state.width,
    height: state.height
  });

  state.manage(win);
});

ウィンドウの状態を保存・復帰したり,初期値をハンドルしてくれるので,気にする必要がなくなります.

menubar

menubar はメニューバーに常駐して,メニューアイテムがクリックされた時にミニウィンドウを表示するようなアプリをつくるための BrowserWindow の wrapper です.

menubar の様子

メニューアイテムの表示やウィンドウ位置の制御,表示のトグルなどを管理してくれるので,十数行でメニューウィンドウを扱うアプリが作れます.

import * as menubar from 'menubar';

const mb = menubar({
    index: '/path/to/index.html',
    icon: '/path/to/icon.png'
});

mb.on('ready', () => {
    // app.on('ready') 相当.アプリが立ち上がった後の処理
    mb.showWindow();
});
mb.on('after-create-window', () => {
    // ウィンドウが作成された後の処理
});

OS X, Linux, Windows に対応していますが,LinuxUbuntu などのメジャーなディストリビューション向けです.また,Windows でタスクバーの位置を左にするなどの設定をしている場合は対応していないようです.

node-auto-launch

上記のメニューバーに常駐するアプリなどを実装すると,OS 起動時にアプリも一緒に起動してほしいと思います.起動時に自動でアプリをスタートするにはそれぞれの OS ごとに設定する(OS X なら Launch Agent, Windows ならレジストリ登録など)必要がありますが,node-auto-launch ではこれらの処理を wrap して,クロスプラットフォームに OS 起動時のアプリ自動実行を行ってくれます.

const AutoLaunch = require('auto-launch');

const launcher = new AutoLaunch({
    name: 'Your App Name',
    path: '/path/to/YourApp',
});

launcher.isEnabled().then(enabled => {
    if (enabled) {
        // すでに自動起動アプリとして登録済み
        return;
    }
    launcher.enable();
});

GitHub の通知を流してくれるメニューバーアプリ gitify などが利用しています.

electron-mocha

Electron アプリの単体テストを書く時,テスト対象が Electron のモジュールに依存していると Node.js では単体テストが実行できません.

そこで,Electron 上で mocha単体テストを実行できるのが electron-mocha です.メインプロセス側でもレンダラプロセス側でもテストを実行できます.

Electron アプリだけでなく,DOM API に依存した単体テストなんかもレンダラプロセス側では実行できるので,DOM API に依存したテストを行いたい時一般にも使えます.

describe('', function () {
    before(function () {
        this.elem = document.querySelector('.target');
    });

    it('', function () {
        this.elem.click();
        assert.equal(this.elem.innerText, 'Clicked');
    });
});

のようなテストが特に jsdom などを使わずに普通に動きます.

まとめ

Electron 1.0 が出てからだいぶ時間が経ち,それなりにエコシステムも育ってきました.今回は私が実際に導入してみて便利だったものを紹介しましたが,awesome-electron にはさらにたくさんのツールやライブラリが掲載されています.