112
121

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScriptとElectron

Last updated at Posted at 2018-06-27

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

112
121
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
112
121

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?