TypeScriptとElectron

以前に、TypeScriptを使ってElectronアプリケーションを作るためのノウハウを記事にしたが、だいぶ時間が立ってしまったので、改めてここに記載しておこうと思う。

というのも、アレだけ時間が立っておきながら、未だに **新しいTypeScriptの記法やシキタリに則ったハウツーがネットに存在しない** という状態がよろしくないと思ったからである。
未だにtypingsを使えとか言うのはどうなのとかね

## 開発環境の構築

### パッケージのインストール

Node JSがマシンにインストールされていることが前提。ここのインストール方法がわからないとか言うお方は回れ右して調べてほしい。

基本は `npm install` で導入していく。

```console:パッケージのインストール
$ npm init
$ npm install --save-dev typescript webpack webpack-cli ts-loader
$ npm install --save electron
```

* typescript
    * TypeScriptのトランスパイラ `tsc` とかのパッケージ
* webpack/webpack-cli
    * ご存知バンドラ。最近`webpack`と`webpack-cli`に別れたのでどっちも必要
* ts-loader
    * webpackからTypeScriptをロードするためのプラグイン
* electron
    * 本命

### 各種設定ファイルの作成

`tsc`はオプションでTypeScriptファイルのトランスパイル方法を指定できるが、今回は
`webpack` -> `ts-loader` -> `tsc` という感じで呼ばれるので、予め設定ファイルを作成しておく。設定ファイルは暗黙的にカレントディレクトリの`tsconfig.json`が実行時に読み込まれる。と言っても、次のコマンドを実行するだけでほっておいて良い。

```console:tsc用の設定ファイル作成
$ tsc --init
```

次に`webpack`用の設定ファイル`webpack.config.js`を作っておく。
ココらへんは各プロジェクトで違うと考えられるが、おすすめなのは次のような設定

```js:webpack.config.js
const path = require('path');

var main = {
  mode: 'development',
  target: 'electron-main',
  entry: path.join(__dirname, 'src', 'index'),
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist')
  },
  node: {
    __dirname: false,
    __filename: false
  },
  module: {
    rules: [{
      test: /.ts?$/,
      include: [
        path.resolve(__dirname, 'src'),
      ],
      exclude: [
        path.resolve(__dirname, 'node_modules'),
      ],
      loader: 'ts-loader',
    }]
  },
  resolve: {
    extensions: ['.js', '.ts']
  },
};

var renderer = {
  mode: 'development',
  target: 'electron-renderer',
  entry: path.join(__dirname, 'src', 'renderer', 'index'),
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist', 'scripts')
  },
  resolve: {
    extensions: ['.json', '.js', '.jsx', '.css', '.ts', '.tsx']
  },
  module: {
    rules: [{
      test: /\.(tsx|ts)$/,
      use: [
        'ts-loader'
      ],
      include: [
        path.resolve(__dirname, 'src'),
        path.resolve(__dirname, 'node_modules'),
      ],
    }]
  },
};

module.exports = [
  main, renderer
];
```

上記は、electronのメインプロセスとレンダラプロセスをそれぞれ`main`と`renderer`に分け個別にバンドルできるようにしている。これには目的が２つあり、１つ目は **webpackのオプションとして、レンダラプロセスとメインプロセスかを指定しなければならない**という点で、もう一つは、**レンダラ側でJSX(react)を使うことも想定している**からである。
特に１つ目は`target:`で指定しなければならないので注意していただきたい。
また後述するが、メインプロセス側で`BrowserWindow.loadURL`などをするとき、`__dirname`などでパスを指定するが、以下の記述がないと`__dirname`が空の状態になる。

```js:__dirname
  node: {
    __dirname: false,
    __filename: false
  },
```

##### 入出力ファイルとディレクトリ構成

上記の`webpack.config.js`で想定するディレクトリ構成は、プロジェクトディレクトリをルートとして、メインプロセスのソースファイルは`src/`直下、レンダラプロセス用のソースファイルは`src/renderer`にあることを想定している。また、プロジェクトディレクトリをルートとして、`dist`直下にメインプロセス、`dist/scripts`にレンダラプロセスのファイルが出力される。
`dist/`以下にはレンダラプロセス用のHTMLファイルも置かれ、レンダラプロセスのJavaScriptファイルが次のようにHTML内で指定されることを想定している。

```html:レンダラプロセスのHTMLファイル
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <div id="contents"></div>

    <!-- ここでJavaScriptファイルを読み込む -->
    <script type="text/javascript" src="scripts/index.js"></script>
</body>
</html>
```

ついでに`package.json`の`main`の値を変更しておく。

```json:package.json
"main": "dist/index.js"
```

ここまでであらかたの導入は完了である。

## 実装作業

では早速、`src`ディレクトリ以下にメインプロセス用のファイルを作成する。

```ts:メインプロセスのエントリーポイント
import { BrowserWindow, app, App, } from 'electron'

class SampleApp {
    private mainWindow: BrowserWindow | null = null;
    private app: App;
    private mainURL: string = `file://${__dirname}/index.html`

    constructor(app: App) {
        this.app = app;
        this.app.on('window-all-closed', this.onWindowAllClosed.bind(this))
        this.app.on('ready', this.create.bind(this));
        this.app.on('activate', this.onActivated.bind(this));
    }

    private onWindowAllClosed() {
        this.app.quit();
    }

    private create() {
        this.mainWindow = new BrowserWindow({
            width: 800,
            height: 400,
            minWidth: 500,
            minHeight: 200,
            acceptFirstMouse: true,
            titleBarStyle: 'hidden'
        });

        this.mainWindow.loadURL(this.mainURL);

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

    private onReady() {
        this.create();
    }

    private onActivated() {
        if (this.mainWindow === null) {
            this.create();
        }
    }
}

const MyApp: SampleApp = new SampleApp(app)
```

よく見るやつでしょう？
でもちゃんと型を指定したりしてるので評価してほしい。ともするとスッと`any`にして知らんぷりしてるサンプルコードを見かける。

注意していただきたいのは、環境によっては `import * as Electron from 'electron'`や、`import Electron from 'electron'`としておくとコンパイル時にエラーになる場合がある。少なくともOSX環境ではエラーとなったので上記のような記述にしている。

## ビルド

もうここまで来たらいいでしょう。必要なレンダラプロセスのファイルいつもどおりを作った後。
プロジェクトディレクトリ直下で次のようにすれば出来上がる

```console
$ webpack --display-error-details
```

## おまけ Reactの場合

レンダラプロセスでreactを使いたい場合、以下のようにパッケージをインストールしておく。未だに`typings`とかは古いので入れなくて良い。

```console:React用パッケージのインストール
$ npm install --save react react-dom
$ npm install --save-dev @types/react @types/react-dom
```

レンダラプロセス用のソースディレクトリ`src/renderer`に`index.tsx`として作ると、以下のような感じになる。

```jsx:index.tsx

import React from 'react'
import ReactDOM from 'react-dom'

interface MyComponentProps {
    // 必要なプロパティを記述
}

interface MyComponentStates {
    // 必要なプロパティを記述
}

// クラスの場合
class MyComponent extends React.Component<MyComponentProps, MyComponentStates> {
    constructor(props: MyComponentProps) {
        super(props)
        this.state = {
            // MyComponentStates と一致する必要あり
        }
    }
    public render(): React.ReactNode {
        return (/* 必要なコンポーネントを記述 */)
    }
}

interface MyStatelessComponentProps {
    // 必要なプロパティを記述
}

// 関数の場合
function MyStatelessComponent(props: MyStatelessComponent): JSX.Element {
    return ( /* 必要なコンポーネントを記述 */ )
}

// レンダリング
ReactDOM.render(/*コンポーネント*/, document.getElementByName('contents');
```

## 編集後記

ElectronやReactをTypeScriptで作っている例はよく見るが、TypeScriptがany型を許容しているせいもあってか、そこらへんをおざなりにしている例も多い。
TypeScriptのメリットをぶっ潰しているだけでなく、インテリセンスや補完がきかなかったりするので、これを読んでいる皆さんにはぜひ型を使う習慣をつけていただきたい。