Electron:跨平台的視窗應用程式

歷史

很多事情沒有經歷過就無法深刻體會,所以”感同身受”真的很難,但是軟體工程師是一個很特別的職務,尤其是與業務的隔閡,說給同業的朋友聽時,既使他沒遇過同樣的事情,但是也可以感同身受了解你的苦處。

Q業務:『客戶需要一個 Windows 程式。』

  • 攻城獅:『沒問題。』
  • 躬成屍:GAME OVER

Q業務:『客戶的老闆是用 MacBook,所以系統要能支援 Mac,麻煩你加一下。』

  • 攻城獅:『沒問題。』
  • 躬成屍:GAME OVER

Q業務:『客戶的產線主管希望操作員也可以直接重機台的電腦上使用系統,不過他們是用 Linux,你再花點時間改一下就好。』

  • 攻城獅:『沒問題。』
  • 躬成屍:GAME OVER

Q業務:『產線有些嵌入式電腦(ARM),系統裝不起來,麻煩處理一下。』

  • 攻城獅:『沒問題。』
  • 躬成屍:GAME OVER

當事情發生在別人身上時那是喜劇,你可以安慰他”撐過去就是你的”;
當事情發生在自己身上時那就變成悲劇,你可能會…
img

Electron

在 Node.js 興盛起來後全端工程師(Full Stack Developer)的議題又再被炒熱起來,因為透過 Node.js 讓原本侷限在前端的 JavaScript 變成可以開發後端程式,這讓門檻瞬間降低很多。
Node.js 為 JavaScript 提供一些 API 來替 JavaScript 與作業系統甚至硬體設備溝通,開發過 Ionic 或是 Cordova 程式的人應該就會發覺這跟 Cordova 在做的事情幾乎一樣。
在 Cordova 架構下,它前面透過 WebView 來呈顯網頁,後面提供一些 API 來與系統或硬體界接,中間則是讓我們用 JavaScript 撰寫邏輯,往前可以與前端網頁互動,往後可以與系統或硬體溝通,整個架構就像:
HTML(WebView) <=> JavaScript <=> API(Cordova) <=> OS、Hardware
那桌面系統是不是也有相同的方案?
我們開啟 Electron 官方網站 在網站內可以看到目前 Electron 最新版本為 1.7.6 版、Node 為 7.9.0 版、Chromium 為 58.0.3029.110 版、V8為 5.8.283.38 版,也就是說 1.7.6 版的 Electron 內含 7.9.0 版的 Node.js、58.0.3029.110 版的 Chromium,JavaScript 解析引擎為 5.8.283.38 版V8引擎。
img
比對 Cordova 架構就會變成:
HTML(Chromium) <=> JavaScript <=> API(Node.js/V8) <=> OS、Hardware

前置準備

我們需要一個純前端的 Web 程式,這邊我們拿之前練習到 Angular 服務 的程式來當範例。

[**first-app_2017-09-14.zip**](/uploads/first-app_2017-09-14.zip)

若要下載請記得先透過指令 npm install 來重新安裝 package。

透過 Angular CLI 指令 ng build --prod 來建置專案,預設專案會輸出到專案目錄下的 dist 資料夾內。
img

安裝 electron

我們先建立一個資料夾 desktopApp,接著透過 npm init 來建立一個 package.json 檔,entry point 的設定值改成 main.js,因為這是 Electron 官方範本預設程式進入點。
img
接著安裝 electron,指令如下:
npm install -d electron
img
開啟 package.json 並在 scripts 插入新指令 "start": "electron main.js"

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "desktopapp",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron main.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"electron": "^1.7.6"
}
}

再來將剛才建置好的 Angular 專案(dist 資料夾)複製到 desktopApp 資料夾內。
img

main.js

開啟官方在 GitHub 的 electron-quick-start 專案,將 main.js 複製到 desktopApp 資料夾內。
img

執行 electron

我們透過 npm start 來啟動,它會執行剛才加入的指令 electron main.js,它會開啟一個 electron 程式 並自動載入 main.js
img
不過目前畫面一片雪白,按 F12 也無法開啟開發人員工具,Electron 不是內含 Chromium 嗎?怎麼開不起來?
img
檢視 main.js,原來預設開發人員工具沒有開啟,取消註解,重新執行就可以看到開發人員工具。
雖然仍然沒畫面,但是從開發人員工具可以知道是起始頁面(index.html)的路徑錯誤造成的。
img
img
開啟 main.js,修正起始頁面的路徑,順便可以調整預設視窗大小,重新執行。
img
再次失敗畫面仍然沒出現,錯誤訊息顯示 index.html 內的 js 檔與 css 檔找不到,切換到 Elements 頁籤可以發現 index.html 確實被載入了,但是怎麼 js 檔與 css 檔找不到?
img
img
看一下存取路徑怎麼怪怪的。
img
開啟 dist\index.htmlbase tag 的 href 屬性改成 ./
img
重新執行就正常了。
img
最後做一些 main.js 的設定微調。
關閉開發者工具:將 mainWindow.webContents.openDevTools() 註解起來。
隱藏主選單列:在 createWindow 方法內加入 electron.Menu.setApplicationMenu(null);

main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const electron = require('electron')
// Module to control application life.
const app = electron.app
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow

const path = require('path')
const url = require('url')

// 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.
let mainWindow

function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 1200, height: 800})

electron.Menu.setApplicationMenu(null);

// and load the index.html of the app.
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, '/dist/index.html'),
protocol: 'file:',
slashes: true
}))

// Open the DevTools.
// mainWindow.webContents.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
})
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// 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()
}
})

app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow()
}
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

重新執行,整體感覺已經跟一般桌面程式無異。
img

佈署 electron

最後當然是最重要的如何佈署到平台上,首先安裝 electron-packager 套件,指令如下:
npm install -d electron-packager
img
electron-packager 的語法如下:
electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]

sourcedir:表示來源資料夾,. 表示當前來源為資料夾。
appname:輸出的執行檔名稱。
--platform:表示要建置的平台,目前有:linuxwin32darwin,、masall
--arch:表示目前系統架構,選項有:ia32x64armv7larm64all

其他參數可參考 官方文件

Window 64位元版本

package.jsonscripts 插入下列指令:
electron-packager . WinApp --platform=win32 --arch=x64
img
執行 npm run build_win 來建置。
img
編譯後可以看到專案資料夾內多了一個 WinApp-win32-x64 資料夾,資料夾內有一隻 WinApp.exe,執行後就會看到與剛才相同的操作介面。
img
img
我們打開 WinApp-win32-x64\resources\app\ 資料夾其實可以發現程式都在裡面。
img
如果不希望別人可以輕易看到程式碼,可以加上 --asar 參數,讓 electron-packager 幫你封裝起來。
"build_win": "electron-packager . WinApp --platform=win32 --arch=x64 --asar"
重新編譯後就可以看到我們的程式被封裝成 app.asar

如果資料夾已經存在,請先移除,否則請加上 --overwrite 參數來覆寫。
img

img

Linux 64位元版本

package.jsonscripts 插入下列指令:
electron-packager . LinuxApp --platform=linux --arch=x64 --asar,並透過指令 npm run build_linux 編譯。
img
img
將編譯所產生的 LinuxApp-linux-x64 資料夾複製至 Linux 64位元系統(筆者測試環境是 Ubuntu 17 64位元),並執行 LinuxApp,可以看到跟 Winodws 一樣的結果。
img

Linux ARM 版本

package.jsonscripts 插入下列指令:
electron-packager . ARMApp --platform=linux --arch=armv7l --asar,並透過指令 npm run build_arm 編譯。
img
img
將編譯所產生的 ARMApp-linux-armv7l 資料夾複製至 Linux ARM 系統(筆者測試環境硬體是 Raspberry Pi 3,系統是 Raspbian),並執行 ARMApp,可以看到跟 Winodws 一樣的結果。
img

因為筆者沒有 Mac 所以在此不做測試,但是方法應該大同小異。

最後我們來回想最前面的需求:客戶需要一種能在任何桌面系統上執行的程式。
在以往我們可能每個平台都需要額外學習很多技術來支援,甚至每種平台都需要專屬的開發人員,在時程上跟後續維護都會增加很多成本。
Electron 可以幫我們解決不少問題,當然它也有一些限制以及缺點,沒有最好的方案,只有最可行的辦法。

筆者以前主要用 C# 開發 Windows 程式,那時在客戶那 demo 完系統後,最怕遇到客戶說:『你們的系統能不能在瀏覽器上執行。』,現在真的有跨多平台需求時,我們可以對老闆說:『老闆,你找人來開發 Web 平台,剩下平台我一個人搞定。』。

[**desktopApp_2017-09-20.zip**](/uploads/desktopApp_2017-09-20.zip)