Help us understand the problem. What is going on with this article?

TypeScriptとElectron

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

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

開発環境の構築

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

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

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

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

各種設定ファイルの作成

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

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

次にwebpack用の設定ファイルwebpack.config.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のメインプロセスとレンダラプロセスをそれぞれmainrendererに分け個別にバンドルできるようにしている。これには目的が2つあり、1つ目は webpackのオプションとして、レンダラプロセスとメインプロセスかを指定しなければならないという点で、もう一つは、レンダラ側でJSX(react)を使うことも想定しているからである。
特に1つ目はtarget:で指定しなければならないので注意していただきたい。
また後述するが、メインプロセス側でBrowserWindow.loadURLなどをするとき、__dirnameなどでパスを指定するが、以下の記述がないと__dirnameが空の状態になる。

__dirname
  node: {
    __dirname: false,
    __filename: false
  },
入出力ファイルとディレクトリ構成

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

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

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

実装作業

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

メインプロセスのエントリーポイント
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環境ではエラーとなったので上記のような記述にしている。

ビルド

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

$ webpack --display-error-details

おまけ Reactの場合

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

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

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

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のメリットをぶっ潰しているだけでなく、インテリセンスや補完がきかなかったりするので、これを読んでいる皆さんにはぜひ型を使う習慣をつけていただきたい。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away