以前に、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
とかのパッケージ
- TypeScriptのトランスパイラ
- webpack/webpack-cli
- ご存知バンドラ。最近
webpack
とwebpack-cli
に別れたのでどっちも必要
- ご存知バンドラ。最近
- ts-loader
- webpackからTypeScriptをロードするためのプラグイン
- electron
- 本命
各種設定ファイルの作成
tsc
はオプションでTypeScriptファイルのトランスパイル方法を指定できるが、今回は
webpack
-> ts-loader
-> tsc
という感じで呼ばれるので、予め設定ファイルを作成しておく。設定ファイルは暗黙的にカレントディレクトリのtsconfig.json
が実行時に読み込まれる。と言っても、次のコマンドを実行するだけでほっておいて良い。
$ tsc --init
次にwebpack
用の設定ファイル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
に分け個別にバンドルできるようにしている。これには目的が2つあり、1つ目は webpackのオプションとして、レンダラプロセスとメインプロセスかを指定しなければならないという点で、もう一つは、レンダラ側でJSX(react)を使うことも想定しているからである。
特に1つ目はtarget:
で指定しなければならないので注意していただきたい。
また後述するが、メインプロセス側でBrowserWindow.loadURL
などをするとき、__dirname
などでパスを指定するが、以下の記述がないと__dirname
が空の状態になる。
node: {
__dirname: false,
__filename: false
},
入出力ファイルとディレクトリ構成
上記のwebpack.config.js
で想定するディレクトリ構成は、プロジェクトディレクトリをルートとして、メインプロセスのソースファイルはsrc/
直下、レンダラプロセス用のソースファイルはsrc/renderer
にあることを想定している。また、プロジェクトディレクトリをルートとして、dist
直下にメインプロセス、dist/scripts
にレンダラプロセスのファイルが出力される。
dist/
以下にはレンダラプロセス用のHTMLファイルも置かれ、レンダラプロセスのJavaScriptファイルが次のように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
の値を変更しておく。
"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
とかは古いので入れなくて良い。
$ npm install --save react react-dom
$ npm install --save-dev @types/react @types/react-dom
レンダラプロセス用のソースディレクトリsrc/renderer
に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のメリットをぶっ潰しているだけでなく、インテリセンスや補完がきかなかったりするので、これを読んでいる皆さんにはぜひ型を使う習慣をつけていただきたい。