HTML5Experts.jp

Electronプログラミング入門 — インストールからミニブラウザ構築まで

Electron

Electronとは、デスクトップクライアントを作るためのフレームワークです。クロスプラットフォームで動作することをサポートしているため、Electronで作ったアプリケーションはMac、Windows、Linuxの環境でも動作します。Atomと呼ばれる GitHub社製のエディタがあります。 ElectronはAtomを作る際にフレームワークとして作られました。以前はAtomShellと呼ばれていましたが、Electronとして名前を変更し、2016年にはversion 1.0がリリースされるまでに成長しました。

ElectronはJavaScript / HTML / CSSを使ってクライアントアプリケーションを作成します。中のアーキテクチャはChromiumとNode.jsで作られており、Web開発の技術を使ってデスクトップアプリケーションを構築することが可能です。

ElectronはCheng Zhao氏 (以降zcbenz)が開発したフレームワークですが、zcbenz氏は実際Electronの開発前にNW.js(旧 NodeWebkit) と呼ばれるフレームワークのコントリビューターでした。ElectronはNW.jsと非常によく似たフレームワークですが、いくつか技術的に異なるポイントが有ります。決定的な違いは、Chromiumの組み込み方の違いです。ElectronはライブラリとしてChromiumを組み込んでいるのに対して、NW.jsはChromiumをforkしたプロジェクトを使っています。Chromiumは非常に進化が早いプロダクトなので、forkして持つよりもアップデートを考えると効率的です。

とはいえ、NW.jsとElectronの技術的な部分以外は違いはそこまでありません。Atom開発当初にNW.jsが多少不安定だったために新しいプロダクトとして立ち上げたという側面もあるそうです。詳しくはAtomShellとNodeWebkitの違いに詳しく記載されています。

Electronの特徴

いくつか特徴があるので紹介します。

Electronのランタイム

Electron そのものはただのランタイムライブラリです、Node.jsにおけるnodeコマンドのようなもので、electronコマンドでエントリポイントとなるJavaScriptを実行します。

electronコマンドは公式サイトからダウンロードすることもできますが、Node.jsのパッケージモジュールである、npmコマンドを使ってインストールすることも可能です。

$ npm install electron -g

※ 以前までは npm install electron-prebuiltからインストールする必要がありましたが、最近はnpm install electronでinstallできるようになりました。

これでelectronコマンドが有効になるので、そのコマンドを使ってデスクトップアプリケーションを起動させます。アプリケーションを実際に書くのは後述します。

Electron / Browser間でモジュールを共有

Electronアプリを構築すると、シームレスにブラウザからNode.jsのコードを呼ぶことが可能です。そのため、下記のようなコードを実行することもできます。 

// script tag から
<script>
// 自分のローカルファイル読みこんだり
const fs = require(‘fs’);
fs.readFile(‘foo/bar/baz’, (err, data) => {
  console.log(data);
});
</script>

<script> // 外部プロセスを呼んだり const cp = require(‘child_process’); cp.exec(‘ls -l’, (err, stdout) => { console.log(stdout); }); </script>

ただし、この方法を使った場合、DOM-based XSSが発生すると、任意のコマンドだったり、ファイルが操作できてしまう結果になるため、プロダクションでElectronを活用する場合は注意が必要です。

参考資料: Electronの倒し方

Hello World

一旦Electronを起動してみましょう。Electronを動かすだけなら Node.jsはbuilt-inされているので不要ですが、npmがある方が便利なのでNode.jsをインストールしておきましょう。Node.jsは 公式サイトからダウンロードできます。Macであれば、brewでもインストール可能です。

Node.jsがインストールされたらElectronをインストールしてみましょう。下記の通りです。

$ npm install electron -g

適当なフォルダを作成し、package.jsonmain.jsindex.htmlを作成します。 package.jsonは下記の通りに作成します。

{
  "name": "electron-intro",
  "version": "0.0.1",
  "main": "main.js"
}

main.jsを記述します。

const {app, BrowserWindow} = require('electron');

// window objectがGCされないようにするために、globalに定義する let win;

function createWindow () { win = new BrowserWindow({width: 800, height: 600});

win.loadURL(file://${__dirname}/index.html);

win.on('closed', () => { // windowがクローズされたら null にして削除 win = null; }); }

app.on('ready', createWindow);

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

app.on('activate', () => { if (win === null) { createWindow(); } });

最後に表示するためのindex.htmlを記述します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Electron!</title>
  </head>
  <body>
    <h1>Hello Electron!</h1>
    Node version: <script>document.write(process.versions.node)</script>,
    Chrome version: <script>document.write(process.versions.chrome)</script>,
    Electron version: <script>document.write(process.versions.electron)</script>.
  </body>
</html>

これだけでHello Worldは一旦完成です。 下記のようなファイル構成になっていることを確認してください。

.
├── index.html
├── main.js
└── package.json

Electronを起動させてみましょう。

$ electron .

下記のようなウィンドウが出たら完成です。

Hello, World実行結果

PhotonKitを使ってミニマムブラウザを作る

これだけだと味気ないので、PhotonKitを使ってミニマムブラウザを作ってみましょう。ブラウザを作ると言っても、 Chromiumを内包しているElectronであれば、Chromiumの機能を借りてくるだけなのでそこまで難しくはないです。

PhotonKitはCSSフレームワークの1つです。BootstrapやMaterial Design Liteのようなclass setを持っています。Mac のクライアントのようなアプリケーションを作るためのデザインテンプレートになっています。

Photon

まずはpackage.jsonを作りましょう。

{
  "name": "electron-mini-browser",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "electron index.js"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "electron": "^1.3.5"
  }
}

npm startでElectronを起動できるようにしておくことと、dependencieselectronをインストールしておきましょう。下記の方法でpackage.jsonに記述しつつ、インストールさせることが可能です。 

$ npm install electron --save

次にindex.jsを作成します。

'use strict';
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

let mainWindow = null;

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

app.on('ready', function() { mainWindow = new BrowserWindow({ width: 800, height: 600, }); mainWindow.loadURL(file://${__dirname}/index.html);

mainWindow.on('closed', function() { mainWindow = null; }); });

実際のページ(index.html)を作成します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="./css/photon.css">
    <script src="./js/main.js"></script>
    <title>Hello Electron!</title>
  </head>
  <body>
    <div class="window">
    <header class="toolbar toolbar-header">
    <h1 class="title">Hello Electron!</h1>
    <div class="toolbar-actions">
     <!-- リロードボタン -->
     <button id="reload" class="btn btn-default">
       <span class="icon icon-arrows-ccw icon-text"></span>
       Reload
     </button> 
     <!-- 戻るボタン -->
     <button id="back" class="btn btn-default">
       <span class="icon icon-left icon-text"></span>
       Back
     </button> 
     <!-- 進むボタン -->
     <button id="forward" class="btn btn-default">
       <span class="icon icon-right icon-text"></span>
       Forward
     </button> 
     <!-- URL バー -->
     <input type="text" id="urlbar" class="form-control" placeholder="URL" value="https://github.com">
     <!-- お気に入りボタン -->
     <button id="favorite" class="btn btn-default">
       <span class="icon icon-star icon-text"></span>
       Favorite
     </button> 
    </div>
    </header>
      <div class="window-content">
        <div class="pane-group">
          <div class="pane-sm sidebar">
            <!-- お気に入りリスト -->
            <ul id="fav-list" class="list-group">
            </ul>
          </div>
            <!-- Webページ表示領域 -->
          <webview class="pane" id="webview" src="https://www.github.com/" autosize="on" style="height:100%;"></webview>
        </div>
      </div>
    <footer class="toolbar toolbar-footer">
    <h1 class="title">Footer</h1>
    </footer>
    </div>
  </body>
</html>

ページの中のボタンに動きを与えるため、js/main.jsを作成します。

document.addEventListener('DOMContentLoaded', () => {
  const webview = document.getElementById('webview');
  const reloadButton = document.getElementById('reload');
  const backButton = document.getElementById('back');
  const forwardButton = document.getElementById('forward');
  const favoriteButton = document.getElementById('favorite');
  const urlbar = document.getElementById('urlbar');
  const favList = document.getElementById('fav-list');

// webview表示の時にurlbarの値を変える webview.addEventListener('load-commit', ({ url, isMainFrame }) => { if (isMainFrame) { urlbar.value = url; } });

// urlbarでEnterキーを押したら遷移する urlbar.addEventListener('keypress', (e) => { if (e.key === 'Enter') { webview.setAttribute('src', urlbar.value); } });

// 更新ボタンをクリックしたらwebviewをリロードする reloadButton.addEventListener('click', () => { webview.reload(); });

// 戻るボタンをクリックしたらwebviewを戻る backButton.addEventListener('click', () => { webview.goBack(); });

// 進むボタンをクリックしたらwebviewを進ませる forwardButton.addEventListener('click', () => { webview.goForward(); });

// お気に入りボタンをタップしたらリストにURLを追加する favoriteButton.addEventListener('click', () => { const listItem = document.createElement('li'); const listContent = document.createElement('p'); listItem.setAttribute('class', "list-group-item"); listItem.setAttribute('data-url', urlbar.value); listContent.textContent = urlbar.value; listItem.appendChild(listContent); favList.appendChild(listItem); listItem.addEventListener('click', () => { const url = listItem.getAttribute('data-url'); webview.setAttribute('src', url); }); }); });

最後にPhotonKitのCSSとFontセットを ダウンロードしておきます。 下記のようなディレクトリ構成になります。

.
├── css
│   ├── photon.css
│   └── photon.min.css
├── fonts
│   ├── photon-entypo.eot
│   ├── photon-entypo.svg
│   ├── photon-entypo.ttf
│   └── photon-entypo.woff
├── index.html
├── index.js
├── js
│   └── main.js
└── package.json

全てを終えたら、npm startコマンドで起動します。下記のようなブラウザが出現できたら完成です。

Electron+Photonで作ったミニブラウザ

プラットフォーム/ライブラリの特徴

項目 説明
対応プラットフォーム Windows, Linux, OS X
コードベースは(ほぼ)完全に統一できるか? ほぼ統一できる
UIを記述する言語 JavaScript, HTML, CSS
UIはネイティブかウェブか 基本はウェブだが、OSネイティブの機能(タスクバー等)は一部利用できる
パフォーマンス 基本ウェブなのでウェブページと同等、ただし自分でオフラインキャッシュの仕組みを持てるので、改善可能
ネイティブな機能を呼び出せるか? Node.jsからOSの機能を呼び出せる。

以下に、上の表を補足します。

対応プラットフォーム

Electronの公式サポートはWindows, Linux, OS Xの3つのみです。今のところはデスクトップアプリケーションのためのフレームワークなので、モバイル対応は全く考えられていません。

コードベースは (ほぼ) 完全に統一できるか?

ほぼ統一できます。ただし、OS X用の機能であったり、Windows専用の機能は用意されていて、プラットフォームによって呼び出し可能なAPIや受信可能なEventが若干異なります。クロスプラットフォームでOSの専用の機能を利用する上で、気になる方は一度APIを確認すると良いでしょう。

UIを記述する言語

Hello Worldやミニブラウザを作って分かる通り、HTML/JS/CSSで書きます。ネイティブ部分の呼び出しもNode.jsなので、 JavaScriptになります。

JavaScriptのレイヤはNode.js部分とChromium部分でv8(JavaScript Engine)が動くので、ほぼ同一の動きをします、ただし、Node.jsとChromiumでv8のバージョンは若干違う可能性があります。基本的な部分は変わりませんが、ES2016の対応状況を見てもらえれば分かる通り、新しいJavaScriptの機能面で違いはあります。

ES2016の対応状況

UIはネイティブかウェブか

基本はウェブです。デスクトップアプリケーションとして、タスクトレイに常駐させたい場合やOSネイティブのダイアログボックスを使いたい場合は専用のAPIがあるので、それを利用して機能を作ることも可能です。

パフォーマンス

Electronは基本的にウェブなので、ネイティブのデスクトップアプリと比較するとそこまで高速ではありません。

Electronのアプリを起動させると、Chromium用のプロセスが3つ起動し、Node.js用のメインプロセスが1つ起動します。合計4プロセスが常駐することになります。言ってしまえばブラウザそのものを起動しつつ、バックグラウンドにNode.jsを起動しているのと同様です。富豪的な方法で実現していると言えるでしょう。

そのため、描画パフォーマンス等はブラウザの機能がそのまま利用できますが、実行効率が良いモデルとはいえません。パフォーマンスにシビアなアプリケーションを作る場合は Electron ではなく、ネイティブの機能を使って作る方が良いでしょう。

ネイティブな機能を呼び出せるか?

ElectronからNode.jsを呼び出せばファイル操作や外部プロセスコールといった基本的な機能は呼び出せます。また、C言語等で書かれたネイティブライブラリもNode.jsアドオンがあれば呼び出すことが可能です。ただし、ネイティブライブラリに関しては、通常のnpmでインストールするだけでは利用できないことがあります。Nodeのバージョンがelectronのbuilt-inで保持しているバージョンと異なる場合にうまく利用できないことが多いです。ネイティブライブラリを利用したい場合は下記のガイドを確認することを推奨します。

ネイティブライブラリ利用ガイド

まとめ

Electronの概要とアプリ構築の方法、ウェブ用プラットフォームを構築する上での共通質問項目を記述しました。Electron はこれまでのAngularJSやReact等で構築されたSingle Page Applicationを活用して、デスクトップアプリケーションを構築するのに非常に向いています。

まだv1.0がリリースされて日が浅いこともあり、そこまでノウハウが溜まっているわけではありません。特にセキュリティやパフォーマンスなどの改善は今進んでいますが、ユーザーからのノウハウも溜める必要があると感じています。

もしまだ触っていない方がいらっしゃるのであれば、electronicaなどのチュートリアルもあるので是非やってみてください。

また今回のNode学園祭では、Electron作者のzcbenzもゲストスピーカーとして登壇する予定です。Electronの今後の話が聞けると思います。

一緒にElectronを盛り上げていきましょう。