はじめに
最近、Electron(旧atom-shell) を触って遊んでいました。公式のドキュメントが充実しており、Web開発の経験があれば、Electron を使い手軽にクロスプラットフォームなデスクトップアプリケーションを作成することができます。今回は、JavaScript の代わりに ClojureScript を使いこの Electron を使ったアプリ開発をしてみたいと思います。
方法としては、Electron の公式ドキュメントにあるQuick Startの動作確認から入り、徐々に ClojureScript に置き換えつつ、 ClojureScript で Electron アプリを開発する環境を整えていきたいと思います。
全体の流れ
まずは、全体の流れをざっと確認しておきます。
- Step0: Quick Start の動作確認
- Step1: Main Process を ClojureScript に置き換え (dev環境)
- Step2: Main Process を ClojureScript に置き換え (production環境)
- Step3: Renderer Process を ClojureScript に置き換え (dev環境)
- Step4: Renderer Process を ClojureScript に置き換え (production環境)
- Step5: Renderer Process に Figwheel を設定
- Step6: Main Process に Figwheel を設定
- Step7: SCSS の設定
- Step8: externs の設定
最終的に完成したコードは github においてあります。また、各 Step が完了した段階のコードは対応するタグとして作成してあります。
$ git clone https://github.com/snufkon/cljs-hello-electron
$ git checkout -b step0 step0
各 Step におけるあまり重要でない変更点の解説は省きたいと思いますので、上記のようにタグをチェックアウトしてソースを確認してみてください。
Step0: Quick Start の動作確認
まずは、Quick Startに説明されている Electron の最小構成で動作を確認しておきます。
必要なファイルを配置
$ mkdir cljs-hello-electron
$ touch cljs-hello-electron/{package.json,main.js,index.html}
で必要なファイルを作成します。main.js
, index.html
については Quick Start のページに表示されているコードをそのままコピーしてください。 package.json
については以下のように記述しておきます。
{
"name" : "hello-electron",
"version" : "0.1.0",
"main" : "main.js"
}
動作確認
Electron アプリをテスト起動させるため、npm で electron コマンドをインストールしておきます。
$ npm install -g electron
無事に electron コマンドのインストールが完了したら、cljs-hello-electron
ディレクトリに移動し、
$ electron .
を実行することで、作成したアプリケーションの起動が確認できると思います。
Step1: Main Process を ClojureScript に置き換え (dev環境)
Electron では package.json
の main
に記載し実行するスクリプトを Main Process と呼びます。この Main Process がアプリケーション全体の管理を行います。また、Main Process から生成される各ウィンドウ(BrowserWindowインスタンス)で表示を行うスクリプトを Renderer Process と呼びます。
Step1 として Main Process にあたる main.js
を ClojureScript からビルドする環境を整えたいと思います。
先に完成後のディレクトリを示すと以下のようになります。
cljs-hello-electron
├── app
│ └── dev
│ ├── index.html
│ ├── js
│ └── package.json
├── project.clj
└── src
└── cljs
└── main
└── hello_electron
└── core.cljs
Step0 でディレクトリ直下に配置していた、index.html
, package.json
は app/dev
に移動します。
また、main.js
は Leiningen によりビルド行い app/dev/js/main/main.js
に出力するように設定します。そのため、package.json
の main
で指定している js の相対パスを以下のように変更します。
{
"name" : "hello-electron",
"version" : "0.1.0",
"main" : "js/main/main.js" // <-- 変更
}
project.clj の追加
leiningen で使用する project.clj
は以下となります。
(defproject hello-electron "0.1.0-SNAPSHOT"
:description "Hello Electron form ClojureScript"
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.293"]]
:plugins [[lein-cljsbuild "1.1.4"]]
:clean-targets [:target-path "app/dev/js/main"]
:cljsbuild
{:builds
{:dev-main {:source-paths ["src/cljs/main"] ;; Main Process に関するソースを配置する場所
:compiler {:target :nodejs ;; Main Process 側は target に nodejs を指定
:main "hello-electron.core" ;; エントリーポイント
:output-to "app/dev/js/main/main.js" ;; package.json ではこの main.js を指定する
:output-dir "app/dev/js/main"
:optimizations :none}}}})
lein cljsbuild once dev-main
で src/cljs/main
以下に配置した cljs のソースをビルドし、開発用の main.js
を出力させます。この段階では、src/cljs/main
以下には main.js
をそのまま cljs に置き換えた src/cljs/hello_electron/core.cljs
のみ配置してあります。
main.js を core.cljs へ変更
(ns hello-electron.core
(:require [cljs.nodejs :as nodejs]))
(nodejs/enable-util-print!)
(defonce path (js/require "path"))
(defonce url (js/require "url"))
(defonce electron (js/require "electron"))
(defonce app (.-app electron))
(defonce BrowserWindow (.-BrowserWindow electron))
(def win (atom nil))
(defn create-window
[]
(reset! win (BrowserWindow. (clj->js {:width 800 :height 600})))
(.loadURL @win (.format url (clj->js {:pathname (.join path (js* "__dirname") "../../../index.html")
:protocol "file"
:slashes true})))
(.openDevTools (.-webContents @win))
(.on @win "closed" (fn []
;; (println "window: closed")
(reset! win nil))))
(defn -main [& args]
(.on app "ready" (fn []
;; (println "app: ready")
(create-window)))
(.on app "window-all-closed"
(fn []
;; (println "app: window-all-closed")
(when (not= (.-platform nodejs/process) "darwin")
(.quit app))))
(.on app "activate"
(fn []
;; (println "app: activate")
(when-not @win
(create-window)))))
(set! *main-cli-fn* -main)
main.js
の中身をそのまま cljs に置き換えました。
(set! *main-cli-fn* -main)
では Node.js 上で実行する際にコマンドライン引数を受け取る関数を指定しています。
参考: Running ClojureScript on Node.js
動作確認
以下で、ビルドおよびアプリケーションの動作確認をすることができます。
$ lein cljsbuild once dev-main
$ electron app/dev
Step0 の js版 と同様のアプリケーションが起動することを確認してください。
Step1: Main Process を ClojureScript に置き換え (production環境)
dev 環境の設定に引き続き、Main Process の production 環境の設定を行います。
設定完了後のディレクトリ構成は以下のようになります。
cljs-hello-electron
├── app
│ ├── dev
│ │ ├── index.html
│ │ ├── js
│ │ └── package.json
│ └── prod
│ ├── index.html <-- app/dev からコピー
│ ├── js
│ └── package.json <-- app/dev からコピー
├── project.clj
└── src
└── cljs
├── dev-main <-- dev-main ビルド時のみ読み込ませるファイルを配置
│ └── hello_electron
│ └── config.cljs
├── main
│ └── hello_electron
│ └── core.cljs
└── prod-main <-- prod-main ビルド時のみ読み込ませるファイルを配置
└── hello_electron
└── config.cljs
project.clj への追記
上記のディレクトリ構造に対して、production 環境をビルドする設定を追加した project.clj
は以下となります。
(defproject hello-electron "0.1.0-SNAPSHOT"
:description "Hello Electron form ClojureScript"
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.293"]]
:plugins [[lein-cljsbuild "1.1.4"]]
;; ビルドを行うコマンドと、ビルド結果を削除するコマンドを定義しておく
:aliases {"clean-dev" ["clean"]
"clean-prod" ["with-profile" "prod" "clean"]
"clean-all" ["do"
["clean-dev"]
["clean-prod"]]
"build-dev" ["do"
["clean-dev"]
["cljsbuild" "once" "dev-main"]]
"build-prod" ["do"
["clean-prod"]
["cljsbuild" "once" "prod-main"]]
"build-all" ["do"
["build-dev"]
["build-prod"]]}
;; dev, prod 用の profile を追加
:profiles {:dev {:clean-targets [:target-path "app/dev/js/main"]}
:prod {:clean-targets [:target-path "app/prod/js/main"]}}
:cljsbuild
{:builds
{:dev-main {:source-paths ["src/cljs/main"
"src/cljs/dev-main"] ;; <-- 追加
:compiler {:target :nodejs
:main "hello-electron.core"
:output-to "app/dev/js/main/main.js"
:output-dir "app/dev/js/main"
:optimizations :none}}
;; 追加
:prod-main {:source-paths ["src/cljs/main"
"src/cljs/prod-main"]
:compiler {:target :nodejs
:output-to "app/prod/js/main/main.js"
:output-dir "app/prod/js/main"
:optimizations :simple}}}})
production ビルド用の prod-main
を追加しました。dev-main
との違いは
-
:source-paths
に指定するディレクトリを変更 -
:main
の記述を削除 -
:optimizations
に:simple
を指定
といった点です。
Node.js 上では :optimizations
を :advanced
にしても :simple
や :none
を設定した場合とほとんど違いが生じないため、:optimizations
には :simple
を指定しています。
参考: Running ClojureScript on Node.js
dev, prod 用の config.cljs を用意
src/cljs/dev-main/hello_electron/
, src/cljs/prod-main/hello_electron
に config.cljs
を配置します。
環境別に config.cljs
を用意するのは :optimizations
の違いにより __dirname
で取得できるパス位置が dev-main
でのビルドと prod-main
のビルドで異なるためです。
(ns hello-electron.config)
(defonce path (js/require "path"))
(defonce url (js/require "url"))
(def index-url (->> {:pathname (.join path (js* "__dirname") "../../../index.html") ;; <-- prod とパスが異なる
:protocol "file"
:slashes true}
clj->js
(.format url)))
(ns hello-electron.config)
(defonce path (js/require "path"))
(defonce url (js/require "url"))
(def index-url (->> {:pathname (.join path (js* "__dirname") "../../index.html") ;; <-- dev とパスが異なる
:protocol "file"
:slashes true}
clj->js
(.format url)))
core.cljsを修正
src/cljs/main/hello_electron/core.cljs
を上記の config.cljs
を利用する形に変更します。
(ns hello-electron.core
(:require [cljs.nodejs :as nodejs]
[hello-electron.config :as config])) ;; <-- 追加
...
(defn create-window
[]
(reset! win (BrowserWindow. (clj->js {:width 800 :height 600})))
(.loadURL @win config/index-url) ;; <-- 変更
(.openDevTools (.-webContents @win))
(.on @win "closed" (fn []
;; (println "window: closed")
(reset! win nil))))
...
:source-paths
の設定により core.cljs
で使われる config.cljs
は
-
dev-main
でビルドした場合src/cljs/dev-main/hello_electron/config.cljs
-
prod-main
でビルドした場合:src/cljs/prod-main/hello_electron/config.cljs
となります。
動作確認
aliases
に設定した、build-prod
コマンドでビルドを行います。
$ lein build-prod
app/prod/js/{main,front}/main.js
が作成されていることを確認してください。
確認ができたら、
$ electron app/prod
で アプリケーションが起動させ、Step2 の dev 環境と同じ結果なることを確認してください。
Step3: Renderer Process を ClojureScript に置き換え (dev環境)
index.html
で行っている Renderer Process 上の表示を js で行うようにします。また、その js を cljs をビルドすることで生成させる環境を設定します。
script タグを追加
<script type="text/javascript" src="js/front/main.js"></script>
まずは、index.html
に js を読み込ませるための script タグを追加します。
project.clj に dev-front ビルドの設定を追加
次に、project.clj にビルド用の設定を追加します。
:dev-front {:source-paths ["src/cljs/front"]
:compiler {:main "hello-electron.core"
:output-to "app/dev/js/front/main.js"
:output-dir "app/dev/js/front"
:asset-path "js/front"
:optimizations :none}}
ビルドに利用するファイルは src/cljs/front
以下に配置し、ビルド結果は app/dev/js/front
以下に出力させます。
表示を行う ClojureScript のコードを記述
表示元となる cljs は以下のように記述しておきます。
(ns hello-electron.core)
(let [node (-> "h1" js/document.getElementsByTagName (aget 0))]
(set! (.-innerHTML node) "Hello ClojureScript World!"))
単純に index.html
に <h>
タグで表示されているテキストを "Hello ClojureScript World!" に差し替えるコードとなります。
動作確認
$ lein build-dev
で開発環境用のビルドを行い、 app/dev/js/{main,front}/main.js
が作成されることを確認します。
確認できたら、
$ electron app/dev
で dev 環境のアプリケーションを起動し、画面に "Hello ClojureScript World!" と表示されることを確認してください。
Step4: Renderer Process を ClojureScript に置き換え (production環境)
Renderer Process 側に production ビルド用の環境を整えます。
script タグを追加
dev と同様 index.html
に script タグを追加します。
project.clj に prod-front ビルドの設定を追加
次に、project.clj
にビルド用の設定を追加します。
:prod-front {:source-paths ["src/cljs/front"]
:compiler {:output-to "app/prod/js/front/main.js"
:output-dir "app/prod/js/front"
:optimizations :advanced}}
dev-front
の設定との違いは以下になります。
-
:main
の設定を削除 -
:asset-path
の設定を削除 -
:optimization
を:advanced
に設定
動作確認
$ lein build-prod
で production 環境用のビルドを行い、 app/prod/js/{main,front}/main.js
が作成されることを確認します。
確認できたら
$ electron app/prod
で production 環境のアプリケーションを起動し、Step3 と同じ結果となることを確認してください。
Step5: Renderer Process に Figwheel を設定
現状の設定では、開発時に Renderer Process 側のコードを変更した場合、その動作を確認するためには Step3 の動作確認に記載した
$ lein build-dev
$ electron app/dev
を毎回行う必要があり煩雑です。
lein cljsbuild auto dev-front
でソースを監視した状態でビルドし、また、アプリケーション上で画面をリロードすることにより、上記のようにターミナル上で毎回コマンド実行を繰り返す必要はなくなりますが、やはり煩雑です。
そこで、lein-figwheelを使い、ソースの変更を検知し、画面を自動更新させる設定を行います。
project.clj に figwheel の設定を追加
まず project.clj
に figwheel の設定を追加します。
[lein-figwheel "0.5.8"]
を :plugins
に
:figwheel-front {:figwheel {:server-port 3500
:server-logfile "figwheel-front.log"}}
を :profiles
に追加します。Main Process 側に設定する figwheel と使用するポートを分けるため、各々にプロファイルを用意し、そこに使用するポートを記載しておきます。
figwheel-front
プロファイルを使用して、figwheel を実行するための alias
も設定しておきます。
"figwheel-front" ["with-profile" "figwheel-front" "figwheel" "dev-front"]
を :aliases
に追加。
最後に dev-front
の設定に :figwheel true
を追加しておきます。
:dev-front {:source-paths ["src/cljs/front"]
:figwheel true ;; <-- 追加
:compiler {:main "hello-electron.core"
:output-to "app/dev/js/front/main.js"
:output-dir "app/dev/js/front"
:asset-path "js/front"
:optimizations :none}}
動作確認
まず、aliases
に設定した figwheel-front
を実行し、figwheel を Renderer Process 上で走らせておきます。
$ lein figwheel-front
次に、別のターミナルを開き dev 環境でアプリケーションを起動します。
$ electron app/dev
この状態で src/cljs/front/hello_electron/core.cljs
に変更を加えてみます。
"Hello ClojureScript World!" の文字列を "Hello Figwheel" などの適当な文字列に変更し、変更がアプリケーションの表示に反映されることを確認してください。
もしファイルの更新が検知されずに、画面の再描画が実行されない場合は project.clj
に以下を追加して確認してみてください。(step5 タグの project.clj
には設定済み)
:figwheel {:hawk-options {:watcher :polling}}
Step6: Main Process に Figwheel を設定
Main Process 側も現状の設定では、動作確認が煩雑なため figwheel の設定を追加します。
wsモジュールをインストール
Node.js 上で figwheel を使うためには ws
というモジュールが必要です。
cljs-hello-electron のディレクトリ直下に
{
"name": "hello-electron",
"version": "0.0.0",
"description": ""
}
を作成し
$ npm install ws --save-dev
でインストールしておきます。
project.clj へ記述を追加
project.clj
で変更する箇所は Renderer Process に figwheel を設定した際とほぼ同じです。
aliases
に以下を追加。
"figwheel-main" ["with-profile" "figwheel-main" "figwheel" "dev-main"]
profiles
に以下を追加。
:figwheel-main {:figwheel {:server-port 3500
:server-logfile "figwheel-main.log"}}
:figwheel-front {:figwheel {:server-port 3600 ;; <-- ポート変更した
:server-logfile "figwheel-front.log"}}
また、dev-main
には
:figwheel {:on-jsload "hello-electron.core/reload"}
を追加します。ソース変更を figwheel が検知した際に、hello-electron.core/reload
関数を実行するように設定しています。
core.cljs を Reloadable なコードへ修正
現状のコードでは、ソースファイルの変更を検知しても既に作成済みのウィンドウに対して何も変化は起こりません。コードを以下のように修正し、ソース変更を検知したらウィンドウを作成し直すように記述します。
(ns hello-electron.core
(:require [cljs.nodejs :as nodejs]
[hello-electron.config :as config]))
(nodejs/enable-util-print!)
(defonce electron (js/require "electron"))
(defonce app (.-app electron))
(defonce BrowserWindow (.-BrowserWindow electron))
(defonce win (atom nil))
(defn- create-window
[]
(reset! win (BrowserWindow. (clj->js {:width 800 :height 600 :show false})))
(.loadURL @win config/index-url)
(.openDevTools (.-webContents @win))
(.on @win "closed" (fn []
;; (println "window: closed")
(reset! win nil)))
(.on @win "ready-to-show" (fn []
;; (println "window: ready-to-show")
(.showInactive @win))))
(defn- destroy-window
[]
(.destroy @win)
(reset! win nil))
(defn- remove-listeners
[]
(.removeAllListeners app "ready")
(.removeAllListeners app "activate")
(.removeAllListeners app "window-all-closed"))
(defn- add-listeners
[]
(.on app "ready" create-window)
(.on app "activate" (fn []
;;(println "app: activate")
(when-not @win
(create-window))))
(.on app "window-all-closed"
(fn []
;; (println "app: window-all-closed")
(when (not= (.-platform nodejs/process) "darwin")
(.quit app)))))
(defn -main [& args]
(add-listeners))
(defn reload
[]
(remove-listeners)
(add-listeners)
(destroy-window)
(create-window))
(set! *main-cli-fn* -main)
元のコードを reloadable なコードに変更しました。
アプリケーション起動時は以下の流れでウィンドウが作成されます。
-
-main
関数実行 -
add-listeners
関数実行- app オブジェクトのイベントリスナーを登録しておく
- Electron の初期化完了で
ready
イベントを受信 -
add-listeners
で登録したcreate-window
関数を実行
また、ソースファイルの変更後は以下の流れでウィンドウを作成します。
-
reload
関数実行 -
remove-listeners
関数で、現状のアプリケーションに登録されているリスナーを全て削除 -
add-listenrs
関数でリスナーを登録し直し -
destroy-window
関数で現状のウィンドウを削除 -
create-window
関数でウィンドウを作り直し
動作確認
Step5 と同様、まずは Main Process 上で figwheel を走らせておきます。
$ lein figwheel-main
次に、別のターミナルを開きアプリケーションを起動します。
$ electron app/dev
アプリケーションが起動したら、src/cljs/main/hello_electron/core.cljs
に変更を加えてみます。
ウィンドウサイズを :width 500
などの適当な値に変更し、現状のウィンドウが破棄され、指定したサイズで新しいウィンドウが作成されることを確認してください。
Step7: SCSS の設定
Quick Start の index.html
では css が指定されていませんが、実プロジェクトでは必要になってくると思います。
そこで、
- scss から css への変換
- css の変更検知からの画面更新
をできるように設定しておきます。
タグを追加
app/{dev,prod}/index.html
に
<link rel="stylesheet" href="css/style.css">
を追加しておきます。
scss から css への変換するための設定
lein-sassyプラグインを使い、scss を css へ変換させます。
まず、project.clj
に以下の設定を追加します。
[lein-sassy "1.0.7"]
を :plugins
に追加。
:sass {:src "src/scss"
:dst "app/dev/css"
:style :nested}
を dev
プロファイルに追加。
:sass {:src "src/scss"
:dst "app/prod/css"
:style :compressed}
を prod
プロファイルに追加。
変換元となる scss は src/scss/style.scss
に配置しておきます。
body {
background-color: #ccc;
}
cssの変更による画面更新の設定
Renderer Process 側の fighweel に css 監視の設定を追加します。
project.clj
の figwheel-front
を以下のように設定します。
:figwheel-front {:figwheel {:server-port 3600
:server-logfile "figwheel-front.log"
:css-dirs ["app/dev/css"]}} ;; <-- 追加
動作確認
まず、scss から css の変換が正しく行われるか確認しておきます。
$ lein sass once
で src/scss/style.scss
から app/dev/css/style.css
が作成されること
また、
$ lein with-profile prod sass once
で src/scss/style.scss
から app/prod/css/style.css
が作成されることを確認してください。
次に Renderer Process 側の figwheel と合わせて動作確認します。
$ lein build-dev
$ lein figwheel-front
で figwheel を起動します。次に別のターミナルで
$ lein sass watch
を実行し scss の変更を監視させておきます。
最後にまた別のターミナルでアプリケーションを起動します。
$ electron app/dev
この状態で style.scss
の background-color: #ccc;
を background-color: #green;
などの適当な値に変更し、起動中のアプリケーション画面の背景が更新されることを確認してください。
Step8: externs の設定
現状のコードでは特に必要ありませんが、こちらも css 同様に実プロジェクトでは必要になってくると思います。
lein-externsプラグインを利用し、externs ファイルを作成したいと思います。
project.clj への変更
[lein-externs "0.1.6"]
を :plugins
に追加。
"externs-prod" ["externs" "prod-front" "app/prod/js/externs_front.js"]
を :aliases
に追加。
:prod-front {:source-paths ["src/cljs/front"]
:compiler {:output-to "app/prod/js/front/main.js"
:output-dir "app/prod/js/front"
:optimizations :advanced
:externs ["app/prod/js/externs_front.js"]}} ;; <-- 追加
prod-front
に :externs
の記述を追加。
動作確認
$ lein externs-prod
で app/prod/js/externs_front.js
が作成されることを確認してください。
production, dev 環境での動作確認
長くなってしまいましたが、ざっと設定が完了しました。最後に、production 環境、dev 環境での動作確認方法を記載しておきます。
production 環境
production 環境の動作確認は以下で実行できます。
$ lein build-prod
$ electron app/prod
dev 環境
dev 環境の動作確認は以下で実行できます。
$ lein build-dev
$ electron app/dev
また、figwheel を利用した開発時の設定は以下の手順で行います。
$ lein figwheel-main
で Main Process 用の figwheel を起動。
別のターミナルで
$ lein figwheel-front
を実行し、Renderer Process 用の figwheel を起動。
また、別のターミナルを開き
$ lein sass watch
を実行し、lein-sassy プラグインにより scss の変更を監視。
最後に別のターミナルで
$ electron app/dev
を実行し、アプリケーションを起動します。
この状態で各ファイルに変更を加えると以下の更新処理が走ります。(冒頭 gif 画像の状態)
- src/main 以下のファイル変更: ウィンドウ再作成
- src/front 以下のファイル変更:ウィンドウ画面更新
- src/scss 以下のファイル変更: ウィンドウ画面更新
おわりに
駆け足となりましたが、Electron の Quick Start から開始し、徐々に ClojureScript に移行しながら、Electron アプリを開発する方法を紹介しました。開発環境として、figwheel を Main Process 側、Renderer Process 側の両方に設定したため、Reloadable なコードを書いていくことが要求されますが、そうすることにより、ビルド、アプリケーション起動といった煩雑な作業をなしく、効率よく開発をすすめていける環境が構築できたと思います。
また、パッケージを作成する方法については紹介していませんが、electron-packagerをインストールしておけば、
$ electron-packager app/prod --platform=darwin --version=1.4.7
で簡単にパッケージを作成することができると思います。(Macの場合)
今回は、ClojureScript で Electron アプリを開発する一から作成しましたが、@kara_d さんが作成している descjopというテンプレートを利用する方法もあります。 Main Process 側への figwheel の設定はありませんが、Window 環境での開発を考慮した設定などが記載されています。実際に開発を行う方は合わせて、確認してみることをお勧めします。
実際にアプリ開発を進めていくと、今回説明した構成や設定では、問題が出てくることもあると思いますが、ひとまず、開発を始める際の足掛かりとしていただければと思います。
参考資料
- Electron
- ClojureScript