使用 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-templatelein_template_descjop 等現成的 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/ <b>
├── LICENSE
├── content_shell.pak
├── icudtl.dat
├── libgcrypt.so.11
├── libnode.so
├── libnotify.so.4
├── locales <b>
├── natives_blob.bin
├── resources <b>
├── snapshot_blob.bin
├── test1 <g>
└── version

2 directories, 10 files

其中 test1 就是我們在 Linux 下的執行檔名稱,於是我們只要包裝這個資料夾就可以將其發佈出囉 ~

至於程式碼安全性,實際上 electron-packager 會將剛剛的 app 資料夾放置到 resource 裡面,因此真正要 release 程式時,最好使用 :advanced 編譯來優化以及保 護你的程式碼。