やってみよう。
まずWebアプリのフロントエンドを作る
ElectronアプリはフロントエンドのみのWebアプリのようなものです。
なので、まずそんなものを作ってみましょう。
今回はフレームワークとしてReagentを使います。ClojureScriptのフレームワークとしてはOmが一番人気のようですが、軽量で使いやすいという噂のReagentの方が小さなスタートとしてはやりやすいかなと思い選んでみました。Reactの薄いラッパーでもあるので、Reactの知識を活かしやすいという点もあります。
雛形を作る
まず適当なディレクトリを作ります。
$ mkdir sample
$ cd sample
次に、leiningenのテンプレート機能で雛形を作ります。
$ lein new reagent-frontend exercise
とりあえずコンパイルしてみましょう。
$ cd exercise
$ lein cljsbuild once
で、適当な1行サーバとかで、 public/index.html
を開けば画面が表示されるはずです。
これを起点とします。
Electronアプリ化する
では、このWebアプリをElectronアプリにしてみましょう。
まず、electronが無ければ入れておきます。
$ npm i -g electron
Reagentアプリの1つ上のディレクトリに移動し、package.jsonとmain.jsとを設置します。
{
"name" : "reagent-electron",
"version" : "0.1.0",
"main" : "main.js"
}
const { app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url')
let win
function createWindow () {
win = new BrowserWindow({width: 800, height: 600})
win.loadURL(url.format({
pathname: path.join(__dirname, 'exercise/public/index.html'),
protocol: 'file:',
slashes: true
}))
// デバグ用
win.webContents.openDevTools()
win.on('closed', () => {
win = null
})
}
app.on('ready', createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (win === null) {
createWindow()
}
})
それから徐にelectronコマンドを叩きます。
$ electron .
すると小窓が開き、Electronアプリとして先程のWebアプリが起動します。
これだけでデスクトップアプリができるなんて、凄いですね。
動きのあるアプリを作ってみる
ただ文字を表示するだけでは面白くないので、動きを付けてみましょう。
他のWebフレームワークを使って散々「足し算アプリ」を作ったので、今回は……「足し算アプリ」を作ってみます。
入力が2箇所あって、その2つに数値を入れると合計値を出してくれるという斬新なアプリケーションです。
アプリの構成
上で見たように、アプリ部分は単なるWebフロントエンドとして作る事ができます。
(ファイル選択のようなネイティブの機能を利用する場合はちょっと工夫が必要ですが、今回は省略します。)
なので、まずは「足し算アプリ」を単なるReagentアプリとして作ります。
まず、core.cljsを次のように変更します。
(ns exercise.core
(:require [reagent.core :as r]
[exercise.home :as home]))
(defn mount-root []
(r/render [home/home-page] (.getElementById js/document "app")))
(defn init! []
(mount-root))
このファイルではrootコンポーネントのマウントのみを行います。メインとなるコンポーネントは別ファイル(home.cljs)に切り出しました。
src
└─ exercise
├─ core.cljs
└─ home.cljs
では、本体となるhome.cljsを見ていきましょう。
homeコンポーネント
home.cljsのコードは次のとおりです。
(ns exercise.home
(:require [reagent.core :as r]))
(def state (r/atom {:state {:x 0 :y 0}}))
(defn- update-state [key val]
(swap! state #(assoc-in %
[:state key]
(if (js/isNaN val) 0 val))))
(defn- make-input [key value]
[:input {:type "text"
:value value
:on-change #(update-state key (-> %
.-target
.-value
(js/parseInt 10)))}])
(defn- calc-app [{{:keys [x y]} :state}]
[:div
[:h2 "Calc App"]
[:div
[make-input :x x]
"+"
[make-input :y y]
"="
(+ x y)]])
(defn home-page []
[calc-app @state])
見ていきましょう。
コンポーネントとレンダリング
reagentのコンポーネントは、元のReactのコンポーネントと同じように、単なる関数とする事ができます(create-class関数を使って、ライフサイクル系の関数を含んだComponentクラスのオブジェクトを作る事もできます)。
この際、レンダリングするHTMLはHiccup式の内部DSLで記述します。
;; これとか
[:input {:type "text"
:value value
:on-change #(update-state key (-> %
.-target
.-value
(js/parseInt 10)))}]
;; これ
[:div
[:h2 "Calc App"]
[:div
[make-input :x x]
"+"
[make-input :y y]
"="
(+ x y)]]
説明不要なくらいわかりやすいDSLですね。
コンポーネントは3つ作りました、
まず入力フィールドを作るmake-inputコンポーネント。
(defn- make-input [key value]
[:input {:type "text"
:value value
:on-change #(update-state key (-> %
.-target
.-value
(js/parseInt 10)))}])
次にアプリ本体部分のcalc-appコンポーネント。
(defn- calc-app [{{:keys [x y]} :state}]
[:div
[:h2 "Calc App"]
[:div
[make-input :x x]
"+"
[make-input :y y]
"="
(+ x y)]])
最後に、アプリをレンダリングするだけのhome-pageコンポーネント
(defn home-page []
[calc-app @state])
rootコンポーネントからはhome-pageコンポーネントが呼び出されるので、ここがエントリーポイントになります。
ステート
reagentは状態管理にClojureのatomという機能を利用します。
atomは、その名の通り状態をアトミックに管理できる機能で、値を取り出す時は @
、置き換える時は reset!
、更新する時は swap!
などを使います。これらの処理が一意に成されるので、安心して並列処理中で使う事ができるのですね。
reagentでは、clojureのもともとのatom関数ではなく、独自の(同名の)atom関数を使ってatomを作ります。何故独自に用意されているかというと、コンポーネントの更新要求をatomに対する更新(上記のreset!関数とswap!関数)に紐付けるためです。
つまり、コンポーネントの中でatomの値を( @
で取り出して)使っている場合、その使用しているatomが更新されたら、自動的にそのコンポーネントも更新、つまり再レンダーされます。setState関数が呼ばれるのと同じですね。
今回は、その名もstateというatomを用意して、状態を管理することにしました。
(def state (r/atom {:state {:x 0 :y 0}}))
atomの中身は単なるマップです。
状態としては、足し合わせる2つの値に、それぞれxとyという名前を付けて持っています。
これがどのように使われているのでしょうか。まず、home-pageコンポーネントに於いて、
(defn home-page []
[calc-app @state])
stateの中身を取り出して、それをcalc-appコンポーネントに引数として渡しています。
これで、stateが更新されたら自動的にcalc-appコンポーネントを再レンダリングするようにしています。
calc-appコンポーネントはstateを受け取り、レンダリングに使用します。
(defn- calc-app [{{:keys [x y]} :state}]
[:div
[:h2 "Calc App"]
[:div
[make-input :x x]
"+"
[make-input :y y]
"="
(+ x y)]])
分配束縛を用い、変数にstate内のデータを直接束縛しています。
xとyとは、それぞれInput要素の値として使われる他、足し合わせて合計も表示しています。stateが更新される度に、この値は自動的に書き換わり、「現在の状態」を画面に表示します。
stateの更新はどのように行われるでしょうか。
Input要素を作るmake-inputコンポーネントを見ます。
(defn- make-input [key value]
[:input {:type "text"
:value value
:on-change #(update-state key (-> %
.-target
.-value
(js/parseInt 10)))}])
on-change属性に渡した無名関数の中で、stateを更新しています。stateの更新の為にupdate-state関数を定義しているので、フィールドが更新された後の値を数値に変換して渡しています。
update-state関数を見ましょう。
(defn- update-state [key val]
(swap! state #(assoc-in %
[:state key]
(if (js/isNaN val) 0 val))))
swap!関数でstateを更新しているのが見えますね。swap!関数は一種のmap関数で、「atomから値を取り出し、それを引数に取った関数で加工して、またatomの中に戻す」という一連の処理をアトミックに行います。
その他にも上記のように、現在のatomの状態にかかわらず単に値を書き換えるreset!関数などもあります。
Electronアプリとして動かす
では、新しいコードをElectronアプリとして動かしてみます。
とはいえ、やる事は変わりません。
コンパイルして、
$ lein cljsbuild once
electronコマンドを叩くだけです。
$ electron .
無事、立ち上がったでしょうか。
FigwheelとElectron Packager
開発にあたって役立つ2つのツールを軽く紹介しておきます。
FigwheelはClojureScriptのコードを監視し、変更があった場合コンパイルし、さらにホットローディングまで行ってくれるツールです。基本的にブラウザアプリの開発で利用するものですが、Electronの開発でも利用できます。
なお、単に変更を監視して自動的にコンパイルしたいだけなら、
$ lein cljsbuild auto
でも行う事ができます。
Electron PackagerはElectronアプリをバンドルされた実行ファイルにパッケージングする為のツールです。
アプリを配布可能な形式(Windowsならexeファイル、MacOSならappファイル)にする方法については、公式のREADMEを参照してください。
まとめ
ElectronとClojureScriptで、簡単なデスクトップアプリを作ってみました。
簡潔で力強いClojureを利用する事で、複雑なアプリケーションをストレス少なく開発する事ができるはずです。
またClojureScriptは、JavaScriptのオブジェクトをわかりやすく操作する事ができるので、JavaScriptで書かれたサンプルコードであっても容易に翻訳する事ができます。
今回紹介できたElectronやReagentの機能は本当に極僅かなので、是非とも本家のドキュメントを読んで、色々な機能を試してみてください!