なぜTypeScriptでElectronアプリを書いてみるのか


## はじめに

この記事は、前回の記事「[TypeScriptでElectronアプリを書いてみる](http://qiita.com/JunSuzukiJapan/items/4a1a0274a2771cdc0ae2)」の続きです。
前回の記事を読まなくてもだいたいわかると思いますが、読んでおいたほうが理解が深まるかもしれません。 

## なぜTypeScriptでElectronアプリを書いてみるのか？

もちろん型があるとバグが入りにくいというのもある。
もう一つの理由は、１度ベースになるクラスを書いてしまえば、それを継承することにより、次回から楽が出来そうだから。

実際のコードを見てみます。

## アプリケーション

まずは、アプリケーションによって書き換える部分だけ見てみる。

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <title>Hello TypeScript</title>
</head>
<body>
  <h1>Hello TypeScript</h1>
</body>
</html>
```

```typescript:index.ts
/// <reference path="typings/github-electron/github-electron.d.ts" />
/// <reference path="base_browser_window.ts" />
/// <reference path="base_application.ts" />

class MyApplication extends BaseApplication {
}

const app: GitHubElectron.App = electron.app;
const myapp = new MyApplication(app);
```

上記のindex.htmlとindex.tsの２つのファイルを書き換えていくだけでアプリケーションを作成することができる（はず）。


## ベースとなるクラス

ただし、上記の２ファイルだけではビルドできない。
継承元になるクラスがないからである。

なので、以下の２つのファイルを用意する。


```typescript:base_browser_window.ts
/// <reference path="typings/github-electron/github-electron.d.ts" />

const electron = require('electron');
const BrowserWindow: typeof GitHubElectron.BrowserWindow = electron.BrowserWindow;

class BaseBrowserWindow {
	window: GitHubElectron.BrowserWindow;

	constructor(options: GitHubElectron.BrowserWindowOptions, url: string){
		this.window = new BrowserWindow(options);
		this.window.loadURL(url);

		this.window.on('closed', () => { this.onClosed(); });
		this.window.on('close', (event: Event) => { this.onClose(event); });
		this.window.on('page-title-updated', (event: Event) => { this.onPageTitleUpdated(event); });
		this.window.on('unresponsive', () => { this.onUnresponsive(); });
		this.window.on('responsive', () => { this.onResponsive(); });
		this.window.on('blur', () => { this.onBlur(); });
		this.window.on('focus', () => { this.onFocus(); });
		this.window.on('maximize', () => { this.onMaximize(); });
		this.window.on('unmaximize', () => { this.onUnmaximize(); });
		this.window.on('minimize', () => { this.onMinimize(); });
		this.window.on('restore', () => { this.onRestore(); });
		this.window.on('resize', () => { this.onResize(); });
		this.window.on('move', () => { this.onMove(); });
		this.window.on('enter-full-screen', () => { this.onEnterFullScreen(); });
		this.window.on('leave-full-screen', () => { this.onLeaveFullScreen(); });
		this.window.on('enter-html-full-screen', () => { this.onEnterHtmlFullScreen(); });
		this.window.on('leave-html-full-screen', () => { this.onLeaveHtmlFullScreen(); });
		this.window.on('app-command', (event: Event, cmd: string) => { this.onAppCommand(event, cmd); });

		// OS X only
		this.window.on('moved', () => { this.onMoved(); });
	}

	onClosed(){
		this.window = null;
	}

	onClose(event: Event){
	}

	onPageTitleUpdated(event: Event){
	}

	onUnresponsive(){
	}

	onResponsive(){
	}

	onBlur(){
	}

	onFocus(){
	}

	onMaximize(){
	}

	onUnmaximize(){
	}

	onMinimize(){
	}

	onRestore(){
	}

	onResize(){
	}

	onMove(){
	}

	onMoved(){
	}

	onEnterFullScreen(){
	}

	onLeaveFullScreen(){
	}

	onEnterHtmlFullScreen(){
	}

	onLeaveHtmlFullScreen(){
	}

	onAppCommand(event: Event, cmd: string){
	}
}
```


```typescript:base_application.ts
/// <reference path="typings/github-electron/github-electron.d.ts" />

interface CertificateObject {
	data: Buffer,
	issueName: string
}

interface RequestObject {
	method: string,
	url: string,
	referrer: string
}

interface AuthInfoObject {
	isProxy: boolean,
	scheme: string,
	host: string,
	port: number,
	realm: string
}

class BaseApplication {
    mainWindow: BaseBrowserWindow = null;

    windowOptions: GitHubElectron.BrowserWindowOptions = {
        width: 800,
        height: 400,
        minWidth: 500,
        minHeight: 200,
        acceptFirstMouse: true,
        titleBarStyle: 'hidden'
    };
    startUrl: string = 'file://' + __dirname + '/index.html';

    constructor(private app: GitHubElectron.App,
                windowOptions?: GitHubElectron.BrowserWindowOptions,
                url?: string)
    {
    	if(windowOptions !== undefined){
    		this.windowOptions = windowOptions;
    	}
    	if(url !== undefined){
    		this.startUrl = url;
    	}

    	this.app.on('will-finish-launching', () => { this.onWillFinishLaunching(); });
        this.app.on('ready', () => { this.onReady(); });
        this.app.on('window-all-closed', () => { this.onWindowAllClosed(); });
    	this.app.on('before-quit', (event: string) => { this.onBeforeQuit(event); });
    	this.app.on('will-quit', (event: string) => { this.onWillQuit(event);});
    	this.app.on('quit', (event: string, exitCode: number) => { this.onQuit(event, exitCode); });
    	this.app.on('browser-window-blur', (event: string, window: GitHubElectron.BrowserWindow) => {
    		this.onBrowserWindowBlur(event, window);
    	});
    	this.app.on('browser-window-focus', (event: string, window: GitHubElectron.BrowserWindow) => {
    		this.onBrowserWindowFocus(event, window);
    	});
    	this.app.on('browser-window-created', (event: string, window: GitHubElectron.BrowserWindow) => {
    		this.onBrowserWindowCreated(event, window);
    	});
    	this.app.on('certificate-error', (event: string,
    									  webContents: GitHubElectron.WebContents,
					    				  url: string,
					    				  error: string,
					    				  certificate: CertificateObject,
					    				  callback: (verifyCertificate: boolean) => void) => {
			this.onCertificateError(event, webContents, url, error, certificate, callback);
		});
    	this.app.on('select-client-certificate', (event: string,
					    	                      webContents: GitHubElectron.WebContents,
					    	                      url: string,
					    	                      certificateList: CertificateObject[],
					    	                      callback: (certificate: CertificateObject) => void) => {
			this.onSelectClientCertificate(event, webContents, url, certificateList, callback);
		});
    	this.app.on('login', (event: string,
    	 					  webContents: GitHubElectron.WebContents,
					    	  request: RequestObject,
					    	  authInfo: AuthInfoObject,
					    	  callback: (username: string, secret: string) => void) => {
    		this.onLogin(event, webContents, request, authInfo, callback);
    	});
    	this.app.on('gpu-process-crashed', () => { this.onGpuProcessCrashed(); });

    	// OS X only
   		this.app.on('open-file', (event: string, path: string) => { this.onOpenFile(event, path); });
   		this.app.on('open-url', (event: string, url: string) => { this.onOpenURL(event, url); });
   		this.app.on('activate', (event: string, hasVisibleWindows: boolean) => { this.onActivate(event, hasVisibleWindows); });
    }

    onWindowAllClosed(){
        if(process.platform != 'darwin'){
            this.app.quit();
        }
    }

    onReady(){
    	const mythis = this;
    	// 無名クラスを作りBaseBrowserWindowのonClosed関数をオーバーライドする。	
    	const myBrowserWindowClass = class extends BaseBrowserWindow {
    		onClosed() {
    			mythis.mainWindow = null;
    			super.onClosed();
    		}
    	};
    	this.mainWindow = new myBrowserWindowClass(this.windowOptions, this.startUrl);
    }

    onWillFinishLaunching(){
    }

    onBeforeQuit(event: string){
    }

    onWillQuit(event: string){
    }

    onQuit(event: string, exitCode: number){
    }

    onOpenFile(event: string, path: string){
    }

    onOpenURL(event: string, url: string){
    }

    onActivate(event: string, hasVisibleWindows: boolean){
    }

    onBrowserWindowBlur(event: string, window: GitHubElectron.BrowserWindow){
    }

    onBrowserWindowFocus(event: string, window: GitHubElectron.BrowserWindow){
    }

    onBrowserWindowCreated(event: string, window: GitHubElectron.BrowserWindow){
    }

    onCertificateError(event: string,
    				   webContents: GitHubElectron.WebContents,
    				   url: string,
    				   error: string,
    				   certificate: CertificateObject,
    				   callback: (verifyCertificate: boolean) => void)
    {
    }

    onSelectClientCertificate(event: string,
    	                      webContents: GitHubElectron.WebContents,
    	                      url: string,
    	                      certificateList: CertificateObject[],
    	                      callback: (certificate: CertificateObject) => void)
    {
    }

    onLogin(event: string,
    	    webContents: GitHubElectron.WebContents,
    	    request: RequestObject,
    	    authInfo: AuthInfoObject,
    	    callback: (username: string, secret: string) => void)
    {
    }

    onGpuProcessCrashed(){
    }
}
```

上記のbase_application.tsとbase_browser_window.tsの２つのファイルは基本的に書き換える必要はあまりない。

>継承を使うことにより書き換える部分が少なくなるのがTypeScriptを使うメリットだと思う。
>また、GitHubElectron.Appを生で使うと ```on('イベント名', handler)```などのようにイベント名を文字列で指定することになるが文字列というのがなんか気持ち悪いしタイポしたら困るし。
>```onReady(){ //処理 }```のように書けるほうが断然いいと思われる。




## ビルド＆実行

ビルドしてみよう。

### package.json

package.jsonがまだない場合は、

```
$ npm init -y
```

で作成しておく。この作業は１度やれば２回目からは必要ない。

### 型定義ファイル

electron関連の型定義ファイルをインストールしておく。

```
$ npm install tsd -g
$ tsd install github-electron --save
```

この作業も１度だけでよい。

### コンパイル

ビルドは、シェル上で

```
$ tsc --out index.js index.ts
```

とすればできる。
あとは、

```
$ electron .
```

で、アプリが起動する。

## 拡張性

もちろん、拡張性もある。

index.tsにあるclass MyApplicationの中身を自由に書き換えればよい。
この際、イベントハンドラ(onではじまるメソッド）をオーバーライドするだけでアプリの動作を自由に変えられる（はず）。

### オーバーライドの例

index.tsの変更のサンプルをあげておく。
短いコードなので全部のせておく。
参考まで。

```typescript:index.ts
/// <reference path="typings/github-electron/github-electron.d.ts" />
/// <reference path="base_browser_window.ts" />
/// <reference path="base_application.ts" />

class MyApplication extends BaseApplication {
    // オーバーライド
    onWindowAllClosed(){
        console.log("closed");
        super.onWindowAllClosed(); // 継承元のメソッドを呼び出す。
    }
}

const app: GitHubElectron.App = electron.app;
const myapp = new MyApplication(app);
```

## 追記１：バグ？

### thisがthisではない

javascriptのthisは罠が多いとは知っていましたが、あまりjavascript(typescript含む）にくわしくないせいか、踏んでしまいました。。。

base_application.ts内のイベントハンドラ関数内でthisを使うと、期待されるBaseApplication型のオブジェクトではない、何かよくわからないオブジェクトが入っているようです。

```typescript:base_application.ts
    onWindowAllClosed(){
        if(process.platform != 'darwin'){
            this.app.quit();  // <- このthisとか
        }
    }
    
    onReady(){  // この関数内のthisも怪しい
        // 略
    }
```

これがなぜそうなるのか、よくわかってないので、現時点では以下のようにして回避する方法しか思い浮かばない…。

- ハンドラーを呼び出す段階のthisはBaseApplication型のままなので（確認済み）、ハンドラーを呼び出すときに引数でthisを渡してやる。

```typescript:base_application.ts
    constructor(private app: GitHubElectron.App){
    	console.log(this.windowOptions);
    	console.log(this.startUrl);

    	this.app.on('will-finish-launching', this.onWillFinishLaunching);
        this.app.on('ready', () => { this.onReady(this); }); // <- こことか
        this.app.on('window-all-closed', () => { this.onWindowAllClosed(this); }); // <- こことか

		// 略
    }
```

- ハンドラーの引数を増やして、それをthisの代わりに使う。

```typescript:base_application.ts
    onWindowAllClosed(self: BaseApplication){
        if(process.platform != 'darwin'){
            self.app.quit();
        }
    }

    onReady(self: BaseApplication){
    	// 無名クラスを作りBaseBrowserWindowのonClosed関数をオーバーライドする。	
    	const myBrowserWindowClass = class extends BaseBrowserWindow {
    		onClosed() {
    			console.log("myBrowserWindowClass onclosed");
    			self.mainWindow = null;
    			super.onClosed();
    		}
    	};
    	self.mainWindow = new myBrowserWindowClass(self.windowOptions, self.startUrl);
    }
```

こんな感じ。
この方法はありといえばありなんだけど…他にもっと良い方法がありそうな気が…。

~~自分でも調べてみますが、どなかたおわかりになる方がいたらコメントくださると非常に助かります。~~

## 追記２：バグ対策

追記１で書いたバグですが、どうやら、

```ts:base_application.ts
    constructor(private app: GitHubElectron.App){
        // 略
        this.app.on('will-finish-launching', this.onWillFinishLaunching);
        // 略
    }
```

のように、```this.app.on```のコールバックに直接```this.onWillFinishLaunching```（など）を指定していたせいのようです。

上記を、

```ts:base_application.ts
    constructor(private app: GitHubElectron.App){
        // 略
        this.app.on('will-finish-launching', () => { this.onWillFinishLaunching(); });
        // 略
    }
```

のように、一旦無名関数で受けてから、該当のメソッドを呼びだすようにすると、thisの問題が起きなくなる気がします。

すべてのコードをこの考えで書き換えました。（２０１６元旦）