Electron + React + Redux

JSで書くデスクトップアプリが熱い

Electron 熱いです。
Atom を始め、Qiita の Kobito や Slack など、
十分に実用できるアプリケーションが登場しはじめています。

なにかと話題のRPGツクールMVは、NW.js(旧node-webkit)ですが、これもJSで書かれていますね。
NW.js は、その他、女の子といちゃこらする系のゲームでも使われています。
JS でデスクトップアプリを書いて、うん千万売り上げるのも、もはや絵空事ではありません。

今回は、Electron と Redux、React の組み合わせで、
デスクトップアプリを作るまでの、簡単なチュートリアルを書きます。

登場人物紹介

Electron

Github が開発してる、JS でデスクトップアプリが書けるすごいやつ
NW.js より活発そうですごい

React

Facebook 製のすごいやつ

Redux

「純関数の組み合わせで State を表現すれば、waitFor いらなくね?」
という尊い思想のもと爆誕したライブラリ
なお Reducer が副作用を持つかは実装者の良心にかかってるもよう

Electron ことはじめ

準備

まずは、適当に package.json を作り、electron-prebuilt をインストールします。
こいつがあれば、コマンドライン上から electron アプリを実行できます。

npm init -y
npm install electron-prebuilt --save-dev

ミニマムのアプリを書く

順を追って理解していくために、ミニマムのアプリを書きます。
まずは、index.js に次のように書きます。

index.js

console.log(好きな女の子の名前);

そして、package.json の scripts に次の行を追加します。
これは、アプリを実行するためのショートハンドです。

package.json

...
"scripts": {
  "start": "electron .", // ←この行
  "test": "echo \"Error: no test specified\" && exit 1"
},
...

これで、次のコマンドでアプリを起動できます。

npm start

コマンドラインに好きな女の子の名前はでましたか?
何もでない場合や、好きじゃない女の子の名前が出る方は、最初からやり直してください。

「ただ node のスクリプトを実行しただけじゃないか!」と憤慨している方は、
Dock やタスクバーを確認して、独立したアプリが立ち上がっていることを確認してください。

ウィンドウを出す

デスクトップアプリらしく、ウィンドウを出します。
ちょっと長いですが、index.js を次のように変更。

index.js

const app = require("app");
const BrowserWindow = require("browser-window");
const ROOT_PATH = `file://${__dirname}`;

app.on("ready", e => {
  const mainWindow = new BrowserWindow({width: 800, height: 600});
  mainWindow.loadURL(`${ROOT_PATH}/index.html`);
});

app.on("window-all-closed", e => {
  app.quit();
});

さらに index.html を用意しておきます。

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Electron App</title>
  </head>
  <body>
    <h1>好きな女の子の名前</h1>
  </body>
</html>

最後に以下のコマンドで実行

npm start

ウィンドウに好きな女の子の名前はでましたか?
img タグを使えば、名前だけじゃなくて、写真も出せるので、挑戦してみてください。

パッケージング

もろちん、デスクトップアプリとして配布することもできます。

パッケージングには、electron-packager というモジュールを使います。

npm install electron-packager --save-dev

そして、package.json の scripts にビルド用のコマンドを追加します。 ↓は、Mac用のビルドの一例です。

package.json

"build": "electron-packager . my-app --platform darwin --arch x64 --version 0.36.0"

そして、以下のコマンドでビルドを実行

npm run build

同じ階層に、my-app-*** というディレクトリができるので、
中の my-app.app を実行して、無事、好きな女の子の名前が出ればOKです。

ipcによる通信

ウィンドウの中には、もちろん JavaScript 書けますが、
ホストのスクリプト(今回は、index.js)と、変数などを共有することは出来ません。

ホストのスクリプトとやりとりするためには、ipcと呼ばれる仕組みを使います。
使うぶんには、ただの EventEmitter なので、あまり深く考えずに使えるはずです。

index.html と index.js 、それぞれで必要なモジュールを読み込み、利用すれば OK です。

index.html(どこかに書く)

<button id="myButton">ボタン</button>
<script>
  const ipcRenderer = require("electron").ipcRenderer;
  const myButton = document.getElementById("myButton");
  var clickCount = 0;
  myButton.addEventListener("click", e => {
    ipcRenderer.send("click-my-button", ++clickCount);
  });
</script>

index.js(どこかに書く)

const ipcMain = require("electron").ipcMain;

ipcMain.on("click-my-button", (sender, e) => {
  console.log(e);
});

ウィンドウのボタンを押せば、
コンソールにボタンを押した回数が表示されるはずです。

React with Electron

React の詳細な説明は省きます。

準備

React を使うのだから、せっかくなら、JSXで書きたいですよね。
そして、せっかくデスクトップアプリとして書くなら、コンパイルとかはしたくない……。

ので、今回は、React がサーバサイドで使われるのと似たような形で、JSXを transform します。
babel でできるので、必要な npm モジュールをインストール。

npm install babel-register --save-dev
npm install babel-plugin-transform-react-jsx --save-dev

次に React もインストール

npm install react --save-dev
npm install react-dom --save-dev

ウィンドウで React を動かす

素直に書けば動きます

index.html

<div id="root-dom"></div>
<script>
  require("babel-register")({plugins: "transform-react-jsx"});
  const React = require("react");
  const ReactDOM = require("react-dom");
  const MyApp = require("./my-app");

  const rootDOM = document.getElementById("root-dom");
  ReactDOM.render(React.createElement(MyApp), rootDOM);
</script>

my-app.jsx

"use strict";

const React = require("react");

class MyApp extends React.Component {
  render() {
    return <h1>React in Electron!</h1>;
  }
}

拍子抜けするぐらい簡単でした。

Redux

Redux を使うからには、ロジックとビューをきっちり分けた状態にしたい!
今回は、ホスト側でのみ Store (Reducer) を扱い、
ウィンドウ側は、レンダリングと、Action の発行だけをするようにします。

じゅんび

おもむろに redux をインストールします。

npm install redux --save-dev

設計方針

  • Store はホストが持つ
  • ウィンドウは、ipc を介して Action を発行する
  • Action を受け取ったホストは、Store を更新し、getState したオブジェクトをウィンドウに渡す。
  • ウィンドウは、受け取ったオブジェクトをレンダリングする

実装

モジュールの読み込みなどは、記述を省略します。

index.html

ホスト側から受け取ったオブジェクトをレンダリングする部分を書きます。

ipcRenderer.on("render", (sender, state) => {
  ReactDOM.render(React.createElement(MyApp, state), rootDOM);
});

my-app.jsx

Action を発行する部分を書きます。

class MyApp extends React.Component {
  _onClick() {
    ipcRenderer.send("dispatch-store", {
      type: "COUNT_UP"
    });
  }
  render() {
    return <div>
      <h1>React in Electron!</h1>
      <p>{this.props.count}</p>
      <button onClick={this._onClick}>Click</button>
    </div>;
  }
}

index.js

Store の生成と、Action を受け取る部分を書きます。

Store の生成

const count = (state, action) => {
  state = state || 0;
  switch(action.type) {
    case "COUNT_UP":
      return state + 1;
  }
  return state;
}
const myStore = redux.createStore((state, action) => {
  state = state || {};
  return {count: count(state.count, action)};
});

Action を受け取る部分

const render = () => {
  mainWindow.webContents.send("render", myStore.getState())
};

ipcMain.on("dispatch-store", (sender, e) => {
  myStore.dispatch(e);
  render();
});

mainWindow.webContents.on("dom-ready", render); // ← DOMの準備ができたら一度レンダリング

これでOKです。
ボタンを押すと数字が増えるので、楽しんでください。

ところで、今回は本筋と関係ないので省きましたが、
ES2015 の Object Destructuring や Default Parameter を使ったほうが、簡潔に書けて良いです。

試しに作った

試しに Jade をコンパイルして保存するエディタを作りました。
https://github.com/hystking/jadedit

↓こういうの jadedit.jpg

おわり

このブログ、シンタックスハイライトほしくないですか?

明日は森さんです。
森さんはすごいです。