2016年1月10日 星期日

[教學] Electron + React: 使用 JavaScript 建立跨平台桌面應用程式

平常建立桌機用的視窗應用程式,我們都是用 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
就行摟!

相關文件


沒有留言:

張貼留言