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
↓こういうの
おわり
このブログ、シンタックスハイライトほしくないですか?
明日は森さんです。
森さんはすごいです。