使用 clojurescript 和 electron 來打造桌面程式
最近在研究自幹自己的 emacs,於是瞄上了 Electron 。Electron 原名為 Atom-Shell, 也就是 GitHub 開發的 ATOM 編輯器使用的 UI 介面端。Electron 和 NW.js (node-webkit) 一樣是從 Chromium 改過來的,不過目前看起來發展性比較好 (個人觀點), 如果想要使用 node.js 去製作桌面版的圖形介面程式,Electron 和 NW.js 皆是很好的進 入點。
當然,如果你想知道 electron 和 nw.js 的差異,可以參考這篇文章: Technical Differences Between Electron and NW.js
本篇文章會先從 javascript 搭配 electron 開始,再進入到 clojurescript 的世界。
環境建立
首先當然你的系統需要安裝 node.js ,接著我們可以使用以下兩種方式其中一個,將 electron 安裝進系統或是本地專案內
# Install the `electron` command globally
npm install electron-prebuilt -g
# Install as a development dependency
npm install electron-prebuilt --save-dev
從 javascript 世界來學 electron
在學習 clojurescript 的過程中,由於會用到一些 javascript lib 或是功能,學習一些 javascript 是不可避免的,因此讓我們從 javscript 的 electron 版本開始玩吧 :)
(這邊的 javascript 範例編譯自 Electron 官方 Quick Start 文件)
一般來說,一個 electron 專案具有以下結構
your-app/
├── package.json
├── main.js
└── index.html
package.json 定義了這個專案的名稱、版本、依賴於哪些函式庫等資訊,其內容基本上 和你在寫 node.js 程式時是一樣的,而在 main 欄位中指定的腳本則為我們主程式的進 入點。基本的 package.json 內容如下:
(如果 main 欄位並未被指派,則預設會執行 index.js 替代)
{
"name" : "your-app",
"version" : "0.1.0",
"main" : "main.js"
}
main.js 是我們的主程式,並用來建立圖形視窗以及處理事件 (ex: 關掉視窗),我們在此填入以下內容:
var app = require('app'); // Module to control application life.
var BrowserWindow = require('browser-window'); // Module to create native browser window.
// Report crashes to our server.
require('crash-reporter').start();
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
var mainWindow = null;
// Quit when all windows are closed.
app.on('window-all-closed', function() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform != 'darwin') {
app.quit();
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function() {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600});
// and load the index.html of the app.
mainWindow.loadUrl('file://' + __dirname + '/index.html');
// Open the DevTools.
mainWindow.openDevTools();
// Emitted when the window is closed.
mainWindow.on('closed', function() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
});
簡單的來說,在 main.js 裡面,我們透過 browser-window 模組建立了一個大小為 800x600 的視窗,並起打開 chromium 的開發者工具,這個視窗顯示的內容為 index.html ,我們在此填入以下內容:
<!DOCTYPE html>
<html>
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using io.js <script>document.write(process.version)</script>
and Electron <script>document.write(process.versions['electron'])</script>.
</body>
</html>
當你以上的程式完成後,我們就可以直接使用 electron 命令來執行我們的專案,方法如下:
coldnew@Rosia ~/your-app $ electron .
於是我們第一個 electron 程式就這樣執行起來了~

建立你的 clojurescript 專案
雖然我們可以使用 electron-template 、 leintemplatedescjop 等現成的 lein 樣板, 但是這些樣板不是預設使用 grunt 或是就是使用 reagent,對於沒接觸過這些工具的人而 言就是一個麻煩,因此我們還是建立自己的樣板吧。
(本文不討論 om 或是 reagent ,單純就是使用 clojurescript 建立簡單的 electron 專案)
首先先用 lein 建立一個空白的專案
coldnew@Rosia ~/cljs $ lein new electron_test
接著修改產生出來的 project.clj ,我們要加上 cljsbuild 的編譯設定,並指定輸出資 料夾為 app 資料夾,而在編譯時的優化方面,則是使用 :simple 來優化,並將 source-map 輸出到 app 資料夾方便出問題時的除錯
(defproject electron_test "0.1.0-SNAPSHOT"
:description "A simple example to use cljs with electron"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.28"]]
:plugins [[lein-cljsbuild "1.0.6"]]
:cljsbuild
{:builds [{:source-paths ["src"]
:compiler {:output-to "app/main.js"
:source-map "app/main.js.map"
:output-dir "app"
:optimizations :simple
:pretty-print false
}}]})
接著建立 app 資料夾,並且加入 index.html 以及 package.json 檔案
coldnew@Rosia ~/cljs/electron_test $ mkdir -p app
編輯 app/index.html 加上以下內容
<!DOCTYPE html>
<html>
<head>
<title>Hello Clojurescript with Electron!</title>
</head>
<body>
<h1>Hello Clojurescript with Electron!</h1>
Electron Version <script>document.write(process.versions['electron'])</script>.
</body>
</html>
編輯 app/package.json 並加入最基本的資訊
{
"name": "electron_test",
"version": "0.0.1",
"description": "A simeple example for cljs with electron",
"main" : "main.js"
}
編輯 src/electron_test/core.cljs 並加入我們的主程式
(ns electron-test.core)
(def app (js/require "app"))
(def browser-window (js/require "browser-window"))
(.start (js/require "crash-reporter"))
(def main-window (atom nil))
(.on app "window-all-closed"
(fn []
(when-not (= js/process.platform "darwin")
(.quit app))))
(.on app "ready"
(fn []
(reset! main-window (browser-window.
(clj->js {:width 800, :height 600})))
(.loadUrl @main-window (str "file://" js/__dirname "/index.html"))
;;(.openDevTools @main-window)
(.on @main-window "closed"
#(reset! main-window nil))
))
最後,我們就可以使用 cljsbuild 來編譯我們的程式,如果沒出錯的話編譯出來的檔案會出現在 app 資料夾內
coldnew@Rosia ~/cljs/electron_test $ lein cljsbuild once
編譯完成後,執行 electron 就會看到結果囉 ~
coldnew@Rosia ~/cljs/electron_test $ electron app

打包並發佈你的專案
如果想要發佈你的 electron 專案出去,electron-packager 是個很方便的工具,首先我們 先安裝他:
# for use in npm scripts
npm install electron-packager --save-dev
# for use from cli
npm install electron-packager -g
而 electron-packager 基本使用的形式是這樣子的:
electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> --version=<version>
Required options
platform all, or one or more of: linux, win32, darwin (comma-delimited if multiple)
arch all, ia32, x64
version see https://github.com/atom/electron/releases
也就是說,如果我們的專案產生的結果都存放在 app 資料夾,程式的名稱為 test1 , 而我們要針對 Linux amd64 平台使用 electron v0.33.0 產生我們的執行檔的話,則 命令是這樣下的
coldnew@Rosia ~/cljs $ electron-packager app test1 --platform=linux --arch=x64 --version=0.33.0
Downloading electron-v0.33.0-linux-x64.zip
[============================================>] 100.0% of 36.59 MB (184.77 kB/s)
Packaging app for platform linux x64 using electron v0.33.0
Wrote new app to ~/cljs/test1-linux-x64
你會看到 electron-packager 產生了名為 test1-linux-x64 的資料夾,而裡面有以下檔案
coldnew@Rosia ~/cljs $ tree -L 1 test1-linux-x64
test1-linux-x64/
├── LICENSE
├── content_shell.pak
├── icudtl.dat
├── libgcrypt.so.11
├── libnode.so
├── libnotify.so.4
├── locales
├── natives_blob.bin
├── resources
├── snapshot_blob.bin
├── test1
└── version
2 directories, 10 files
其中 test1 就是我們在 Linux 下的執行檔名稱,於是我們只要包裝這個資料夾就可以將其發佈出囉 ~
至於程式碼安全性,實際上 electron-packager 會將剛剛的 app 資料夾放置到 resource 裡面,因此真正要 release 程式時,最好使用 :advanced 編譯來優化以及保 護你的程式碼。
延伸閱讀
[1] Building a desktop application with Electron
[2] Technical Differences Between Electron and NW.js
[3] Meteor 構建桌面應用
[4] Awesome Electron