React Tutorial Example (Electron)

[React](https://facebook.github.io/react/) や [Redux](http://redux.js.org/) は 主に Webアプリケーション のライブラリなのですが
[Electron](http://electron.atom.io/) を利用すると *ディスクトップ アプリケーション* を作成することができます。
[前回](http://qiita.com/ogomr/items/e0001c2744781f004873) のアプリケーションを *ディスクトップ アプリケーション* にしましょう。

# Learning

* Electron で *ディスクトップ アプリケーション* を作成する。
* Electron の *メニュー* と *IPC(プロセス間通信)* を利用する。

# Environment

* node: v6.9.2
* npm: v4.0.5
* yarn: v0.18.0

# Comment Box Form

* 完成される [Source Code](https://github.com/ogom/react-comment-box-example/tree/electron) のファイルリストです。
	* `main.js` と `server.js` の２ファイルが追加されます。

```bash
$ tree -a -I node_modules
.
├── .babelrc
├── app
│   ├── actions
│   │   └── index.js
│   ├── components
│   │   ├── Comment
│   │   │   └── index.js
│   │   ├── CommentBox
│   │   │   ├── index.js
│   │   │   └── style.css
│   │   ├── CommentForm
│   │   │   └── index.js
│   │   ├── CommentList
│   │   │   └── index.js
│   │   └── global.css
│   ├── containers
│   │   └── App
│   │       ├── index.js
│   │       └── style.css
│   ├── index.js
│   ├── reducers
│   │   └── index.js
│   └── store
│       └── index.js
├── test
│   └── app
│       ├── actions
│       │   └── index.test.js
│       ├── components
│       │   ├── Comment
│       │   │   └── index.test.js
│       │   └── CommentBox
│       │       └── index.test.js
│       └── reducers
│           └── index.test.js
├── index.html
├── main.js
├── package.json
├── server.js
├── webpack.config.js
└── yarn.lock
```

*Let's hands-on*

## Setup application

* `git clone` コマンドでアプリケーションをダウンロードします。
* `yarn` コマンドで依存するモジュールをインストールします。

```bash
$ git clone https://github.com/ogom/react-comment-box-example.git
$ cd react-comment-box-example
$ git checkout jest
$ yarn
```

*API Server* は [React Tutorial Example (Express)](http://qiita.com/ogomr/items/b90c68764c3c75cf501e) をご利用ください。

# Electron

* Electron のモジュールをインストールします。

```bash
$ yarn add --dev electron
```

## main.js

* `main.js` ファイルに Electron の設定をします。
	* `index.html` ファイルを参照しています。

```javascript:main.js
import { app, BrowserWindow } from 'electron'

let mainWindow

function createWindow () {
  mainWindow = new BrowserWindow({width: 800, height: 600})
  mainWindow.loadURL(`file://${__dirname}/index.html`)
  mainWindow.webContents.openDevTools()
  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow()
  }
})
```

* `package.json` ファイルに `main` と `scripts` の設定します。
* Electron でも `babel` を利用するので `babel-register` を `require` に設定します。

```diff:package.json
   "name": "react-comment-box-example",
   "version": "1.0.0",
   "private": true,
+  "main": "main.js",
   "scripts": {
-    "start": "webpack-dev-server -d --progress --colors",
+    "start": "electron --require babel-register .",
     "test": "jest",
     "test:watch": "npm test -- --watch"
   },
```

* `yarn start` コマンドで Electron が起動します。

```
$ yarn start
```

![electron_main](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/electron/electron_main.png)

`bundle.js` ファイルがロードできないエラーが発生しています。 ( *Failed to load resource: net::ERR_FILE_NOT_FOUND* )
なので、`bundle.js` ファイルを参照できるように webpack の設定をしましょう。

## server.js

* `server.js` ファイルに webpack の設定をします。
	* `webpack.config.js` ファイルを参照しています。
	
```javascript:server.js
import webpack from 'webpack'
import WebpackDevServer from 'webpack-dev-server'
import config from './webpack.config'

const options = {
  publicPath: config.output.publicPath,
  hot: true,
  inline: true,
  historyApiFallback: true,
  stats: {
    colors: true,
    hash: false,
    timings: true,
    chunks: false,
    chunkModules: false,
    modules: false
  }
}

const compiler = webpack(config)
const server = new WebpackDevServer(compiler, options)

server.listen(4000)
```

* `main.js` ファイルに `server.js` をインポートします。
	* Electron の実行で webpack も実行されるようになります。

```diff:main.js
+import server from './server'
 import { app, BrowserWindow } from 'electron'

 let mainWindow
```

* `webpack.config.js` ファイルに entry と output の設定をします。

```diff:webpack.config.js
 var webpack = require('webpack')
+var path = require('path')

 module.exports = {
-  entry: './app/index',
+  entry: [
+    'webpack-dev-server/client?http://localhost:4000/',
+    'webpack/hot/dev-server',
+    path.join(__dirname, './app/index')
+  ],
   output: {
+    path: path.join(__dirname, './dist'),
+    publicPath: 'http://localhost:4000/dist/',
     filename: 'bundle.js'
   },
```

* `index.html` ファイルの `bundle.js` ファイルの参照先を変更します。

```diff:index.html
   </head>
   <body>
     <div id='content'></div>
-    <script src='/bundle.js'></script>
+    <script src='http://localhost:4000/dist/bundle.js'></script>
   </body>
 </html>
```

![electron_server](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/electron/electron_server.png)

`bundle.js` ファイルがロードできるので **Comment Box** が表示されました。
それに *Hot Module Replacement* の機能も使えるので開発が捗ります。

## Menu

* ディスクトップ アプリケーションのメニューは簡単に作成することができます。
* `Comment` のメニューに `Clear` のサブメニューを作成します。
	* アプリケーションを終了する `quit` のメニューもあると開発が捗ります。

```diff:main.js
 import server from './server'
-import { app, BrowserWindow } from 'electron'
+import { app, BrowserWindow, Menu, dialog } from 'electron'

 let mainWindow

 function createWindow () {
   mainWindow = new BrowserWindow({width: 800, height: 600})
   mainWindow.loadURL(`file://${__dirname}/index.html`)
   mainWindow.webContents.openDevTools()
   mainWindow.on('closed', () => {
     mainWindow = null
   })
+
+  Menu.setApplicationMenu(
+    Menu.buildFromTemplate(
+      [
+        {
+          label: 'App',
+          submenu: [
+            {
+              role: 'quit'
+            }
+          ]
+        },
+        {
+          label: 'Comment',
+          submenu: [
+            {
+              label: 'Clear',
+              click(item, focusedWindow) {
+                dialog.showMessageBox({
+                  type: 'info',
+                  message: 'Message!',
+                  detail: 'message detail.',
+                  buttons: ['OK']
+                })
+              }
+            }
+          ]
+        }
+      ]
+    )
+  )
 }
```

* メニューをクリックすると *Message!* のダイアログが表示されます。

![electron_menu](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/electron/electron_menu.png)

## IPC

* Electron のメニューとブラウザを連携するには *IPC* を利用します。
* *IPC* に `store.dispatch(action)` を設定すると `Menu -> Redux -> React` で連携が可能です。
	* さらに `ipcRenderer.send('ipc::getState' store.getState())` で **State** をメニューに送信することもできます。

```diff:app/index.js
 import React from 'react'
 import ReactDOM from 'react-dom'
 import { Provider } from 'react-redux'
+import { ipcRenderer } from 'electron'

 import App from './containers/App'
 import configureStore from './store'

 const store = configureStore()

 ReactDOM.render(
   <Provider store={store}>
     <App />
   </Provider>,
   document.getElementById('content')
 )
+
+ipcRenderer.on('ipc::dispatch', (e, action) => {
+  store.dispatch(action)
+})
```

* **Actions** はブラウザの `app/actions/index.js` ファイルをインポートすると *DRY* ですね。

```diff:main.js
+import * as Actions from './app/actions'
 import server from './server'
 import { app, BrowserWindow, Menu, dialog } from 'electron'
```

* 先ほどのメニューに `send('ipc::dispatch', Actions.showComments([]))` を設定します。

```diff:main.js
               label: 'Clear',
               click(item, focusedWindow) {
                 dialog.showMessageBox({
                   type: 'info',
                   message: 'Message!',
                   detail: 'message detail.',
                   buttons: ['OK']
                 })
+                focusedWindow.webContents.send('ipc::dispatch', Actions.showComments([]))
               }
             }
           ]
```

* `webpack.config.js` ファイルに `target: 'electron-renderer'` を設定します。
	* `server.js` ファイルに設定した項目は除去します。

```diff:webpack.config.js
-  devServer: {
-    hot: true,
-    port: 4000,
-    inline: true,
-    historyApiFallback: true
-  }
+  target: 'electron-renderer'
 }
```

* これでメニューをクリックして *Message!* のダイアログの `OK` をクリックするとコメントがクリアされます。

![electron_ipc](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/electron/electron_ipc.png)

*Congrats!*

----

リリースをするためには `electron-packager` などでパッケージを作成するなど、いくつかの手順がありますが
Webアプリケーション のテクニックで ディスクトップ アプリケーション が作成できるのは便利ですね。
また、モバイル アプリケーションは [React Native](https://facebook.github.io/react-native/) を利用すると作成できますよ。

React やその周辺のテクノロジーは変化が活発で、発展が楽しみなプロダクトです。
([React Tutorial](https://facebook.github.io/react/tutorial/tutorial.html) も *Comment Box* から *Tic tac toe* に変更されました :satisfied:)

*Enjoy React*