SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

TypeScript+Electronでデスクトップアプリを作ってみる

最近あちこちでElectronを使ったアプリが出てきたり、入門記事もちらほら見かけるようになってきました。
流行りものには乗っておこうということで、Electronをちょろっと使ってみました。
Electron

JavaScriptでの入門は、良質な記事がネット上に多々あるので、 ここではTypeScriptを使ってElectronアプリを作ってみたいと思います.

サンプルコード一式は、以下に置いておきました。
GitHubからcloneした後、npm installしてnpm startすれば実行できます。

準備

Node.jsがインストールされていて、npmが使える状態を前提としてます。
自分はNode.js v4系を使ってますが、v0.12系でも同じようにできるかと思います。

プロジェクトの作成

まずは適当にフォルダ作ってnpm initしてpackage.jsonを作ります。

electronのインストール

続いてelectronを--save-devでインストールします。

npm install electron-prebuilt --save-dev

TypeScriptコンパイラ

続いてTypeScriptコンパイラもインストールします。
一応、開発環境のバージョンも構成管理できるように、グローバルなtscではなく、プロジェクト内にtscをインストールしておきます。

npm install typescript --save-dev

この、プロジェクト配下にインストールしたtscを使うので、コンパイルなどのコマンドはnode_modules\\.bin\\tscと呼び出します。
グローバルにインストールしたコンパイラを使う場合には、このコマンドをただのtscと読み替えて進めてください。

tsconfig.jsonの作成

TypeScriptのコンパイル時に使う設定をtsconfig.jsonに用意します。
プロジェクトのルート(package.jsonなどがあるのと同じ階層)に、tsconfig.jsonというファイルを以下の内容で作ります。

tsconfig.json

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "sourceMap": true
    },
    "exclude": [
        "node_modules",
        "dist"
    ]
}

設定の内容はおおまかに以下のような感じです。

  • node_modulesは各種外部ライブラリのフォルダなのでコンパイル対象外
  • distというフォルダもコンパイル対象外(まだdistフォルダは作ってませんが、後でリリース物の出力場所としてこの名前のフォルダを作ります。)
  • 「files」プロパティを省略してるので、「exclude」の内容に一致するパス以外のすべての.tsファイルがコンパイル対象になります。

これでnode_modules\\.bin\\tscというコマンドでプロジェクト一式のコンパイルをする準備ができました。

tsdを使って型定義ファイルを用意

tsdのインストール

npmから以下のコマンドでtsdをグローバルにインストールします。
もうグローバルにインストールしてるような場合はここの手順はスキップ。

npm install tsd -g
型定義ファイルの取得

electron用の型定義ファイルを取得します。

DefinitelyTypedのページを見てみると、他のライブラリの型定義ファイルとは異なり、なぜかelectronのディレクトリには、型定義ファイルがやたらたくさんあります。
https://github.com/borisyankov/DefinitelyTyped/tree/master/github-electron
rendererプロセスと、mainプロセス用にわかれてるような感じですが、両方取得するとipcモジュールの定義が重複してエラーになってしまうので、ここではmainプロセス用の型定義のみ取得してます。

これ、うまくやる方法ないのかなぁ。それともmainプロセスとrendererプロセスは別々にコンパイルしたほうがいいのだろうか・・・ この辺はまだまだお悩み中・・・

tsd init
tsd query github-electron-main -rosa install

作ってみる

これで準備が整ったので、ここからelectronを使ったアプリ作成に入ります。

ボタンを押したらalert表示するだけのサンプル

エントリポイントの作成

プロジェクトのルートフォルダに、main.tsというファイルを作ります。
そして、package.jsonに以下のようなプロパティを追加します。

{
  // 省略
  "main": "main.js",
  // 省略
}

package.jsonのmainプロパティに書かれているjsファイルがアプリのエントリポイントとなります。

main.tsは以下のとおり。

import app = require('app');
import BrowserWindow = require('browser-window');
require('crash-reporter').start();

// メインウィンドウの参照をグローバルに持っておく。
var mainWindow: GitHubElectron.BrowserWindow = null;

// すべてのウィンドウが閉じられた際の動作
app.on('window-all-closed', function() {
  // OS X では、ウィンドウを閉じても一般的にアプリ終了はしないので除外。
  if (process.platform != 'darwin') {
    app.quit();
  }
});

app.on('ready', function() {
  // 新規ウィンドウ作成
  mainWindow = new BrowserWindow({ width: 800, height: 600 });

  // index.htmlを開く
  mainWindow.loadUrl('file://' + __dirname + '/index.html');

  // ウィンドウが閉じられたら、ウィンドウへの参照を破棄する。
  mainWindow.on('closed', function() {
    mainWindow = null;
  });
});

BrowserWindowのインスタンスを作り、loadUrlするとウィンドウを開くことができます。

型定義ファイルを使いTypeScriptで書いているので、コード補間も効くので快適にコーディングできます。
electronの各種モジュールやメソッドがうろ覚え状態なので、呼び出せるメソッドの候補などが出てくれるのは特に助かります。
f:id:minami_SC:20151019000726p:plain

表示内容の作成

続いて、main.jsから開かれるindex.htmlとこのページから読み込むindex.jsを作ります。
それぞれのファイルの内容は以下のとおり。
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <script src="index.js"></script>
  </head>
  <body>
    <h1>Hello World!</h1>
    <hr/>
    <button onclick="hello()">Show Message</button>
  </body>
</html>

index.ts

function hello(){
    alert('hello');
}

画面にはHello World!!と表示され、ボタンが一つ表示されているだけです。
このボタンを押すと、index.tsのhelloメソッドが呼ばれ、「hello」とだけ書かれたメッセージボックスが表示されます。

アプリの起動

アプリを起動するには、以下のようにelectron.exeに対し、実行対象のフォルダのパスを渡すことで起動できます。

node_modules\.bin\electron .

package.jsonがあるプロジェクトのルートがカレントディレクトリになってる場合には、「.」の指定でOK。

f:id:minami_SC:20151019000743p:plain こんな風に、ボタンを押したらメッセージボックスが表示されます。

electronのdialogモジュールを使ってダイアログを表示してみる

せっかくなので、もう少し手の込んだことをしてみます。
先ほどはjavascriptのalertメソッドを使ってダイアログの表示をしてましたが、 今度はelectronのdialogモジュールを使ってやってみます。

electronのプロセスについて

dialogモジュールを使う前に、electronのアプリの実行プロセスについてさらっと解説。

electronのアプリは複数のプロセスで動作します。
プロセスは、BrowserProcessとRendererProcessの2種類があります。 エントリポイントとなるmain.tsはBrowserProcessで動作し、BrowserWindow.loadUrlで開かれたウィンドウのhtmlからロードされるindex.tsなどはRendererProcessで動作します。

↓のドキュメントなどにも書いてありますが、BrowserProcessとRendererProcessで使えるモジュールが異なっているので、今書いているスクリプトがどちらのプロセスなのかを意識しておく必要があります。
http://electron.atom.io/docs/v0.34.0/

これからダイアログ表示に利用しようとしているdialogモジュールはBrowserProcess用のモジュールのため、RendererProcessで動作しているindex.tsからはそのままでは利用できません。

RendererProcess側から、BrowserProcessのモジュールを使いたい場合には、require('remote').require(・・・・)という感じで、remoteを経由してrequireします。

index.tsを以下のように修正します。

index.ts

var remote = require('remote');
var app = remote.require('app');
var BrowserWindow = remote.require('browser-window');
var dialog = remote.require('dialog');

function hello(){
    var options = {
        title: 'ダイアログのタイトル',
        type: 'info',
        buttons: ['OK', 'Cancel'],
        message: 'メッセージ',
        detail: 'hello'
    };
    var win = BrowserWindow.getFocusedWindow();
    dialog.showMessageBox(win, options);
}

※補足
github-electron-rendererの型定義ファイルを読み込んでいないので、RendererProcessの方ではTypeScriptでの型情報が利用できてません。。。
github-electron-mainとgithub-electron-rendererの両方を同時に使おうとするとコンパイルエラーになるので。。。 そのため、ここではrequireの結果をvarで受けて使っています。

ここまでの修正で、こんな風にelectronのdialogモジュールを使ったメッセージボックス表示ができました。
f:id:minami_SC:20151019000757p:plain

全体の動作のイメージは↓みたいな感じです。
f:id:minami_SC:20151019000807p:plain

もうちょい扱いやすくしてみる

開発時にもっと便利にコンパイル/実行できるように、npm経由で実行できるようにしたり、VSCodeから簡単に呼び出せるようにしてみます。

この辺は自分の開発スタイルに合わせてお好みの方法を取捨選択してもらえればよいかと。

package.jsonを使って起動

これで、npm run buildとやればビルドでき、npm startとするとアプリを起動できるようになります。
またnpm startでアプリを起動する前にはビルド処理も行うようになります。

package.json

{
  // 省略
  "scripts": {
    "build": "tsc",
    "prestart": "npm run build",
    "start": "electron ."
  },
  // 省略
}

VSCodeから呼び出す

ビルド設定

tasks.jsonを以下のようにしておくと、VSCodeから「Ctrl+Shift+B」でビルドできるようになります。

tasks.json

{
    "version": "0.1.0",
    // The command is tsc. Assumes that tsc has been installed using npm install -g typescript
    "command": "node_modules\\.bin\\tsc",
    // The command is a shell script
    "isShellCommand": true,
    // Show the output window only if unrecognized errors occur.
    "showOutput": "silent",
    "args": ["-p", "."],
    // use the standard tsc problem matcher to find compile problems
    // in the output.
    "problemMatcher": "$tsc"
}
VSCodeから起動する設定

launch.jsonを以下のように書くと、VSCodeからF5で起動できます。
ただし、「OpenDebug process has terminated unexpectedly」というエラーが表示され、デバッガが正常に起動できません。
以下のページでも同じような現象の報告があります。
http://www.mylifeforthecode.com/a-better-way-to-launch-electron-from-visual-studio-code/
https://code.visualstudio.com/Issues/Detail/19279
デバッガの動作は、今後のアップデートとかいろんな情報が出てくるのを待ったほうがいいかな。

launch.json

{
    "version": "0.1.0",
    // List of configurations. Add new configurations or edit existing ones.
    "configurations": [
        {
            // Name of configuration; appears in the launch configuration drop down menu.
            "name": "Launch Electron App",
            // Type of configuration.
            "type": "node",
            // Workspace relative or absolute path to the program.
            "program": "main.js",
            // Automatically stop program after launch.
            "stopOnEntry": false,
            // Command line arguments passed to the program.
            "args": [],
            // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.
            "cwd": ".",
            // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.
            "runtimeExecutable": "node_modules\\.bin\\electron",
            // Optional arguments passed to the runtime executable.
            "runtimeArgs": [],
            // Environment variables passed to the program.
            "env": {},
            // Use JavaScript source maps (if they exist).
            "sourceMaps": false
        },
        {
            "name": "Attach",
            "type": "node",
            // TCP/IP address. Default is "localhost".
            "address": "localhost",
            // Port to attach to.
            "port": 5858,
            "sourceMaps": false
        }
    ]
}

アプリのパッケージング

最後に、アプリをexeファイルとしてパッケージングしてみます。
パッケージングには、electron-pacakgerというモジュールを使います。

以下のコマンドでインストール

npm install electron-packager --save-dev

npmのscriptにパッケージング用のスクリプトを追加

npmのスクリプトでexe化できるようにしてみます。
package.jsonに以下のようなスクリプトを追加します。 package.json

{
  // 省略
  "scripts": {
    "build": "tsc",
    "prestart": "npm run build",
    "start": "electron .",
    "pack": "electron-packager . sample --out=dist --arch=x64 --platform=win32 --version=0.34.0 --overwrite --prune --ignore=dist --ignore=typings"
  },
  // 省略
}

これで、npm run packと実行すると、distフォルダにパッケージングした一式のフォルダが出来上がります。
distフォルダの中身は以下のようになります。sample.exeを実行すると先ほどのアプリが起動します。 f:id:minami_SC:20151019000922p:plain

補足

  • --out
    • 出力先のディレクトリを指定
  • --ignore
    • パッケージに含めないフォルダを指定
    • ただし、このプロパティで特に指定しなくても、node_modules以下のelectron-prebuildやelectron-packagerは除外されます。
  • --prune
    • パッケージにする際に、devDependenciesのフォルダを含めないようにする

ここまでざっと見てみましたが、思ってたより簡単にできました。
最初の準備はちょっと面倒だけど、一度ひな形を作っておけば、サクっと作れそうですね。