Electron & React & Redux & TypeScript アプリ作成ワークショップ をやってみた1


概要

以下をやってみた記録。良記事に感謝。

環境

Node.jsとnpmのバージョン
$ node -v
v10.13.0
$ npm -v
6.4.1

npmプロジェクトの作成

フォルダとpackage.json作成
$ mkdir electron-react-app
$ cd electron-react-app
$ npm init
package.json
{
  "name": "electron-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

gitで作業を記録することにする。
以降、記事には記載しないが、適時コミットしてワークショップを進める。

gitの初期設定
$ git init
$ git add .
$ git commit -m "npm init"
$ git remote add origin (your remote repo)
$ git push -u origin master
日本語対策
$ set LANG=ja_JP.UTF-8
.gitignore
node_modules/
/dist/

必要なライブラリをインストールする

各ライブラリの詳細は、元記事参照。

各種ライブラリインストール
$ npm install --save react react-dom redux react-redux styled-components
$ npm install --save-dev electron typescript tslint webpack webpack-cli ts-loader tslint-loader
$ npm install --save-dev @types/react @types/react-dom @types/redux @types/react-redux

ここから二日目の内容です。

TypeScript コンパイラ・オプションファイルの作成

tsconfig.json作成
$ "./node_modules/.bin/tsc" --init
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "sourceRoot": "./tsx",
    "inlineSourceMap": true,
    "inlineSources": true
  },
  "include": [
    "./ts/**/*"
  ]
}

tslint 設定ファイル の作成

tslint.json作成
$ "./node_modules/.bin/tslint.cmd" --init
tslint.json
{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "quotemark": [
            true,
            "single",
            "jsx-double"
        ]
    },
    "rulesDirectory": []
}

webpack.config.js の作成

2か所タイポがあったのは秘密(以下は修正済み)。

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

module.exports = {
    // node.js で動作することを指定する
    target: 'node',
    // 起点となるファイル
    entry: './ts/index.tsx',
    // webpack watch したときに差分ビルドができる
    cache: true,
    // development は、source map fileを作成。再ビルド時間の短縮などの設定となる
    mode: 'development', // "production" | "development" | "none"
    // ソースマップのタイプ
    devtool: 'source-map',
    // 出力先設定 __dirname は node ではカレントディレクトリのパスが格納される変数
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'index.js'
    },
    // ファイルタイプ毎の処理を記述する
    module: {
        rules: [{
            // 正規表現で指定する
            // 拡張子 .ts または .tsx の場合
            test: /\.tsx?$/,
            // ローダーの指定
            // TypeScript をコンパイルする
            use: 'ts-loader'
        }, {
            // 拡張子 .ts または .tsx の場合
            test: /\.tsx?$/,
            // 事前処理
            enforce: 'pre',
            // TypeScript をコードチェックする
            loader: 'tslint-loader',
            // 定義ファイル
            options: {
                configFile: './tslint.json',
                // airbnb という JavaScript スタイルガイドに従うには下記が必要
                typeCheck: true,
            },
        }],
    },
    // 処理対象のファイルを記載する
    resolve: {
        extensions: [
            '.ts',
            '.tsx',
            '.js', // node_modules のライブラリ読み込みに必要
        ]
    },
};

HTML の作成

index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Electronチュートリアル</title>
</head>

<body>
    <div id="contents"></div>
    <script src="dist/index.js"></script>
</body>

</html>

main.js の作成

main.js
const {
    app,
    BrowserWindow
} = require('electron')

// レンダープロセスとなるブラウザ・ウィンドウのオブジェクト。
// オブジェクトが破棄されると、プロセスも終了するので、グローバルオブジェクトとする。
let win

function createWindow() {
    // ブラウザウィンドウの作成
    win = new BrowserWindow({
        width: 800,
        height: 600
    })
    // index.htmlをロードする
    win.loadFile('index.html')
    // 起動オプションに、"--debug"があれば開発者ツールを起動する
    if (process.argv.find((arg) => arg === '--debug')) {
        win.webContents.openDevTools()
    }
    // ブラウザウィンドウを閉じたときのイベントハンドラ
    win.on('closed', () => {
        // 閉じたウィンドウオブジェクトにはアクセスできない
        win = null
    })
}

// このメソッドは、Electronが初期化を終了し、
// ブラウザウィンドウを作成する準備ができたら呼び出される。
// 一部のAPIは、このイベントが発生した後にのみ使用できる。 
app.on('ready', createWindow)

// 全てのウィンドウオブジェクトが閉じたときのイベントハンドラ
app.on('window-all-closed', () => {
    // macOSでは、アプリケーションとそのメニューバーがCmd + Qで
    // 明示的に終了するまでアクティブになるのが一般的なため、
    // メインプロセスは終了させない
    if (process.platform !== 'darwin') {
        app.quit()
    }
});

app.on('activate', () => {
    // MacOSでは、ドックアイコンがクリックされ、
    // 他のウィンドウが開いていないときに、アプリケーションでウィンドウを
    // 再作成するのが一般的です。
    if (win === null) {
        createWindow()
    }
});

コンパイル確認用スクリプトの記述

ts/index.tsx
import React from 'react';
import ReactDom from 'react-dom';

const container = document.getElementById('contents');

ReactDom.render(
    <p>こんにちは、世界</p>,
    container,
);

コンパイルの確認

webpack実行とelectron起動
$ "./node_modules/.bin/webpack"
$ "./node_modules/.bin/electron" ./

npm script を利用する

package.json
diff --git a/package.json b/package.json
index a98d36d..f69de75 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,8 @@
   "description": "",
   "main": "main.js",
   "scripts": {
+    "build": "webpack",
+    "start": "electron ./",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author": "",
webpack実行とelectron起動
$ npm run build
$ npm start

ここから三日目の内容です。

child_state の作成

ts/IUser.ts
export default interface IUser {
    name: string;
}

export const initUser: IUser = {
    name: '',
};

component の作成

ラベル付きテキストボックスの作成

ts/components/TextBox.tsx
import React from 'react';

// 親コンポーネントから渡されるプロパティを定義する
interface IProps {
    // ラベル文字列
    label: string;
    // テキストボックスのタイプ
    type: 'text' | 'password';
    // テキストボックスに表示する値
    value: string;
    // 値の確定時にその値を親プロパティが取得するためにコールバック関数を提供する
    onChangeText: (value: string) => void;
}

export class TextBox extends React.Component<IProps, {}>{
    // DOMエレメントをレンダリングする
    public render() {
        // ラベルが設定されていない場合は、label を出力しない
        const label = (!!this.props.label) ?
            <label>{this.props.label}</label> :
            null;
        return (
            <span>
                {label}
                <input name="username" type={this.props.type} value={this.props.value}
                    onChange={this.onChangeText}></input>
            </span>
        );
    }

    // 値を変更したら、store.dispatch で action を reducer に渡して、state を更新する。
    // state が更新されたら component の prop が更新され、再レンダリングされ、テキストボックスの内容が変更される。
    private onChangeText = (e: React.ChangeEvent<HTMLInputElement>) => {
        this.props.onChangeText(e.target.value);
    }
}

続きは次回。

感想

  • Electronはもちろんだけど、周辺ツールについても、とても勉強になる
  • TypeScriptの書き方も同上

以上