Electron+React+Babel+Gulp環境構築

## はじめに

JSONな設定ファイルを編集する環境としてExcelを使ったけど、
ExcelからJSON吐き出すのが面倒だった

JSONが扱いやすい環境としてWebアプリを調べたところElectronが作りやすそうだったのでお試し.

Web界隈は環境の変化が激しいせいか導入が難しかった

## つまずいたこと

- React公式チュートリアルはtype="text/babel"でjsx変換してる
    - 実行前に変換済みファイル吐いたほうがミス探しやすいと思う
- gulp以外の変換ツールだとrequire('electron')で怒られることがあった。
    - 正しい設定あると思うけど正解探すのが大変だった
- TypeScript使ったほうが作りやすいけど設定がさらに複雑に
    - gulpなにそれ状態からElectron+React+Babel+Gulp+TypeScriptとか死ねる
    - 一通り基本設定が出来てからTypeScript対応したほうが無難っぽい

## 開発環境

Windows10 + Git Bash

## 使用ツール

- Electron
    - node.js + Chromiumなアプリ開発環境
- React
    - Facebook製MVCのView関連ライブラリ
- Photon
    - Electronアプリに綺麗な見た目を
- Babel
    - React向けファイル変換
- Gulp
    - ファイル変換の自動化

## 環境構築

### node.jsインストール

windowsでのnode.js環境構築はnodistを使った。
インストーラーがあるので楽ちん

### Electronインストール

```
$ npm install -g electron
```

### photon

npmリポジトリツリーにいないようなのでgithubURLで

```
$ npm install https://github.com/connors/photon
```

### React

```
$ npm install --save-dev react react-dom
```

### Babel

```
$ npm install -g babel-cli
$ npm install --save-dev babel-preset-es2015
```

ファイル指定変化はこんな感じ
```
$ babel before.js -o after.js
```

ファイル修正のたびにbabelコマンドを打つのは大変なのでgulpを使う

### gulp

-g付きでgulpコマンドが使えるように

```
$ npm install -g gulp-babel
```

プロジェクトフォルダにも

```
$ npm install --save-dev gulp
```

## サンプルアプリ

起動時にconfig.jsonを読み込んでUIに反映する

## アプリフォルダ構成

```
app/
 |--build/              ※変換後jsファイル置き場
 |--data/
 |   |--config.json
 |--lib/
 |   |--main.js         ※Electronアプリメイン
 |   |--fileUtil.js     ※ファイル操作関連
 |--node_modules/       ※ローカルインストールしたnode.jsモジュール群
 |   |--photon/
 |   |--react/
 |   |--react-dom/
 |   |--...
 |--view/               ※Electronアプリ 描画群
 |   |--index.html
 |   |--index.js        ※UI構築メイン　babelで変換される
 |   |--component.js    ※UI構築サブ   babelで変換される
 |--.babelrc            ※babel設定
 |--gulpfile.js         ※gulp設定
 |--package.json        ※electronアプリ設定
```

## アプリ構築

### .babelrcの設定

```
$ cat .babelrc
{ "presets": ["react"] }
```

### gulpfile.jsの設定

```
$ cat gulpfile.js
var gulp = require('gulp');
var babel = require('gulp-babel');

gulp.task('babel', function () {
  return gulp.src('./view/*.js') //view/以下の.jsを変換対象に
    .pipe(babel())
    .pipe(gulp.dest('./build')); //build/に出力
});

gulp.task('watch', function () {
  gulp.watch(src, ['babel']);
});

gulp.task('default', ['babel']);
```

### package.jsonの設定

```
$ cat package.json
{
  "name": "electron",
  "version": "1.0.0",
  "description": "",
  "main": "lib/main.js",
  "scripts": {
    "start": "electron .",
  },
  "keywords": [],
  "author": "",
  "dependencies": {
    "react": "^0.14.0",
    "react-dom": "^0.14.0"
  },
  "devDependencies": {
    "babel-plugin-transform-react-jsx": "^6.8.0",
    "gulp": "^3.9.1",
    "gulp-babel": "^6.1.2"
  }
}

```

### lib/main.js

本家サンプルのまんま

```
const {app, BrowserWindow} = require('electron')

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow () {
  // Create the browser window.
  win = new BrowserWindow({width: 800, height: 600})

  // and load the index.html of the app.
  win.loadURL(`file://${__dirname}/../view/index.html`)

  // Open the DevTools.
  win.webContents.openDevTools()

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow()
  }
})
```

### lib/fileUtil.js

```
const fs = require("fs");

//JSONファイルをテキストのまま取得
exports.readJsonPlain = (path, callback) => {
  fs.readFile(path, 'utf8', (err, data) => {
    if(!err)
    {
      callback(data);
    }
    else {
      console.log(err);
    }
  });
}

//JSONファイルをオブジェクト化して取得
exports.readJson = (path, callback) => {
  fs.readFile(path, 'utf8', (err, data) => {
    if(!err)
    {
      callback( JSON.parse(data) );
    }
    else {
      console.log(err);
    }
  });
}
```

### view/index.html

```
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- photon css読み込み -->
    <link rel="stylesheet" type="text/css" href="../node_modules/photon/dist/css/photon.css" />
    <title>Electron Sample</title>
  </head>
  <body>
  <div class="window">
    <!-- ツールバー -->
    <header class="toolbar toolbar-header">
    </header>

    <div class="window-content">
      <div class="pane-group">
        <!-- 左側サイドバー idで要素特定してReactで挿入する-->
        <div class="pane pane-sm sidebar" id="sidebar-left">
        </div>

        <!-- 右側サイドバー -->
        <div class="pane pane-sm sidebar" id="sidebar-right">
        </div>
      </div>
    </div>
   </div>

   <script src="../node_modules/react/dist/react.js"></script>
   <script src="../node_modules/react-dom/dist/react-dom.js"></script>
   <!-- 変換後jsを読みこませるのでbuild/以下のjsを指定 -->
   <script src="../build/index.js"></script>
  </body>
</html>

```

### view/index.js

```
//ファイル読み込みはメインプロセス側で動作させてみる
const remote = require('electron').remote;
const fileUtil = remote.require('./fileUtil');

const React = require('react');
const ReactDOM = require('react-dom');
//変換後jsを読み込むのでbuild/以下のファイルを指定
const SidebarLeft = require('../build/sidebar-left');

document.addEventListener( "DOMContentLoaded", () => {
  //file path はアプリルートフォルダから相対で。
  //`file://${__dirname}/../data/config.json`はエラーになった
  fileUtil.readJson(`data/config.json`, (data) => {
    const sidebar = document.getElementById('sidebar-left');
    ReactDOM.render(
      <SidebarLeft data={data}/>,
      sidebar );
  });
});

```

### view/sidebar-left.js

```
const React = require("react");

var SidebarLeft = React.createClass({
  render: function() {
    var nodes = [];
    for(var v in this.props.data)
    {
      nodes.push(
        <h1>{v}</h1>
      );
    }
    return (
      <div className="test">{nodes}</div>
    );
  }
});

module.exports = SidebarLeft;

```

## 参考にしたURL

- https://facebook.github.io/react/docs/tutorial.html
- http://qiita.com/koki_cheese/items/054fcf3fde5abd12c334
- http://babeljs.io/docs/plugins/preset-react/
- http://mae.chab.in/archives/2547
- https://developer.mozilla.org/ja/docs/Web/Guide/