平常建立桌機用的視窗應用程式,我們都是用 C++, VB6 或是 C#。但是現在的產品,除了要提供網頁版,手機 APP,Windows / Mac 桌面的客戶端,不然使用者會很不方便。但是使用的語言不同,我們便需要使用不同的技術重複寫好多次。
Github 推出了
Electron 這個開源的專案,它的出現讓使用 JavaScript 來寫桌面應用程式變為可能。Electron 可以視為使用 io.js 控制的修改過的瀏覽器,利用網頁技術來做為畫面的輸出,但同時又可以存取本機資源 (沒有 sendbox)。
如此一來,網頁和桌面應用程式就沒有任何差別,只要瀏覽器可以跑的地方我們的程式幾乎都可以執行 (取決於是否用到特殊的 extension)。而因為是用網頁做為視窗畫面的輸出,所有網頁現成的工具如 jQuery, React, bootstrap 等,也馬上可以用在桌面應用程式上了!
目前 Github 的
Atom 編輯器,
Slack 桌面端,
微軟的 Visual Studio Code 都是建構在 Electron 上面。
Electron 簡介
Electron 是 Github 推出的 Atom 編輯器的底層,也是 Github 的一個開源項目。
我們可以將 Electron 視為是修改過的 Chromium 瀏覽器提供 Node.js API (Chromium Content API),讓我們可以寫 JavaScript 來控制視窗的行為,而 Electron 也是修改自 Chromium multi-process 架構。以下投影片來自
Electron × React — 前端開發者高速跨界桌面開發 。
他每開啟一個視窗,可以想像成開啟 Chrome 瀏覽器的一個 tab,一個 tab 是一個獨立的 process。每一個視窗都可以有自己的 WebKit,有自己的 DOM,執行自己的 JavaScript 來處理畫面,因此我們可以在這裡面套用 React 方便我們做頁面的互動。
一個應用程式可能會有很多個視窗,每個視窗都是獨立的 process,因此在 Electron 裡面,還會有一個幕後的 process 稱為 main process 來管理這些視窗 process,視窗 process 又叫做 renderer process。
接下來,我們來撰寫一個只有一個視窗的應用程式,並且作為範例。
撰寫第一個 Electron + React 應用程式
Electron 基本操作
我們應用程式的檔案結構如下:
your-app/
├── package.json
├── main.js
├── webpack.config.js
└── app/
├── mainWindow.html
└── mainWindow.jsx
其中 package.json 格式跟 Node 模組的 package.json 一樣。其中最重要的是 main 欄位指定的檔案會成為 Electron 開始程式的檔案,這會啟動 main process。package.json 範例如下:
package.json
{
"name": "electron-example",
"version": "0.1.0",
"main": "main.js"
}
我們也指定程式開啟的時候讀取同一層目錄的 main.js 這個檔案。
註:如果 package.json 沒有指定 main 欄位,Electron 預設會使用 index.js 這個檔案。
main.js 是用來建立視窗,處理系統事件。我們範例的 main.js 如下:
main.js
'use strict';
const electron = require('electron');
// app: 控制應用程式生命週期的模組
const app = electron.app;
// BrowserWindow: 建立系統原生視窗 (native window) 的模組
const BrowserWindow = electron.BrowserWindow;
// 保留一個全域的物件關聯以避免 JavaScript 物件 GC 機制造成視窗自動關閉
let mainWindow;
function createWindow () {
// 建立 browser window
mainWindow = new BrowserWindow({width: 800, height: 600});
// 載入 mainWindow.html 作為畫面
mainWindow.loadURL('file://' + __dirname + '/app/mainWindow.html');
// 開啟開發者工具
mainWindow.webContents.openDevTools();
// 當 browser window 被關閉時,會送出 'closed' 訊號,並執行相關的 callback
mainWindow.on('closed', function() {
// 將此 window 物件解除關聯。
// 如果你的應用程式支援多視窗,通常會將這些物件存在一個陣列裡面。
// 現在就是刪除對應的視窗物件的時機。
mainWindow = null;
});
}
// 當 Electron 完成初始化並且可以開始建立視窗的時候,
// 會發送 'ready' 訊號,並執行對應的 callback
// 我們指定收到 'ready' 訊號時,執行 createWindow()
app.on('ready', createWindow);
// 當所有視窗都關閉時,結束應用程式 ( app.quit() )
app.on('window-all-closed', function () {
// OS X 的使用習慣是當所有視窗關閉的時候,上方的 menu bar 仍然維持開啟
// 此時應用程式還沒有完全關閉,除非使用者強制按 Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', function () {
// OS X 通常在應用程式已經起來了,但是所有視窗關閉的時候,還可以重新建立主視窗
if (mainWindow === null) {
createWindow();
}
});
而 app/mainWindow.html 是你想要在視窗中顯示的內容
app/mainWindow.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My Electron-React app</title>
</head>
<body>
<div id="content">
Hello World!!
</div>
</body>
</html>
執行程式
安裝套件
$ npm install electron-prebuilt
如果有使用 Windows 的使用者碰到困難,可以參考我之前寫的這篇:
執行
$ ./node_modules/.bin/electron .
其中第二個路徑是 package.json 所在的目錄
加入 React
首先,將需要的套件加進來。以下是我們的 package.json 檔案。
package.json
{
"name": "electron-example",
"version": "0.1.0",
"main": "main.js",
"dependencies": {
"react": "^0.14.5",
"react-dom": "^0.14.5"
},
"devDependencies": {
"babel-core": "^6.3.26",
"babel-loader": "^6.2.1",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"electron-packager": "^5.2.0",
"electron-prebuilt": "^0.36.2",
"electron-rebuild": "^1.0.2",
"webpack": "^1.12.9",
"webpack-target-electron-renderer": "^0.3.0"
}
}
需要的工具說明如下:
- webpack: 我們將使用 webpack 幫助我們處理壓縮 js / css 等,我們只處理 renderer 用到的 js。
- babel: 我們將使用 JSX 來撰寫 React,他會幫助我們將 JSX 翻譯為 JavaScript ,中間透過 babel-loader 讓 webpack 執行 babel 進行翻譯。
- webpack-target-electron-renderer: 由於 webpack 會嘗試處理 require 語句,而其中 electron 是原生的 node extension,處理的時候會壞掉。這個 package 會告訴 webpack 跳過那些 electron extension。
我們的 webpack設定檔 webpack.config.js 如下:
webpack.config.js
var webpack = require('webpack');
var webpackTargetElectronRenderer = require('webpack-target-electron-renderer');
var config = {
entry: {
mainWindow: ['./app/mainWindow.jsx']
},
output: {
path: './app/built',
filename: '[name].js'
},
module: {
loaders: [
{
test: /\.jsx$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['react', 'es2015'],
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2015'],
}
}
]
},
plugins: [
new webpack.ExternalsPlugin('commonjs',['fs']),
new webpack.IgnorePlugin(/vertx/)
]
}
config.target = webpackTargetElectronRenderer(config);
module.exports = config;
webpack 的使用不在本文範圍內,如果不熟的人可以去搜尋教學文件。
這裡我們做重點說明:
- entry: 原則上一個 window 會有獨立的 js 檔案,因為我們目前只有 mainWindow 這個視窗,因此只有一個項目。
- output: 輸出的目錄會在 ./app/built/,並搭配 entry 欄位的名稱。因此上面這個範例輸出檔案為 ./app/built/mainWindow.js
接下來,我們的 React 程式碼如下:
app/mainWindow.jsx
'use strict';
import React from "react";
import ReactDOM from "react-dom";
var MainWindow = React.createClass({
getInitialState: function() {
return {
message: '',
};
},
handleTextChange: function(event) {
this.setState({message: event.target.value});
},
render: function() {
return (
<div>
Hello world!!
<hr/>
<input type="text" onChange={this.handleTextChange} />
<p><strong>你輸入的是</strong></p>
<p>
<span>{this.state.message}</span>
</p>
</div>
);
}
});
ReactDOM.render(
<MainWindow/>,
document.getElementById('content')
);
我們的範例是畫面中會有一個 textbox,在 textbox 輸入任何字會同步顯示在頁面上。
而 renderer 用到的 mainWindow.html 也要讀取新的 js 才會動喔。因此將 mainWindow.html 改為如下:
app/mainWindow.html
<html>
<head>
<title>My Electron-React app</title>
</head>
<body>
<div id="content">
</div>
</body>
<script src="./built/mainWindow.js"></script>
</html>
程式的部分就都完成了。
編譯與執行
執行 webpack 產生新的 app/built/mainWindow.js
$ ./node_modules/.bin/webpack
然後執行 Electron
$ ./node_modules/.bin/electron .
為了方便未來容易使用,我們將指令放到 package.json
"scripts": {
"start": "./node_modules/.bin/electron ./",
"electron-rebuild": "./node_modules/.bin/electron-rebuild",
"webpack": "./node_modules/.bin/webpack"
}
之後只需要
$ npm run webpack && npm start
就行摟!
相關文件