用 Electron 打造跨平臺前端 App

前言

現現在,用 HTML、JavaScript、CSS、Node.js 寫桌面應用早已不是什麼新鮮的事了,做爲一名前端,可以使用本身熟悉的語言,快速實現本身想要的桌面應用,是件很讓人興奮的事。
目前常見的有 NWheXElectron。今天,就來簡單的上手一下 Electron。javascript

Electron 是什麼?

Electron 是一款能夠利用 Web技術 開發跨平臺桌面應用的框架,最初是 Github 發佈的 Atom 編輯器衍生出的 Atom Shell,後改名爲 Electron。css

Electron 能作什麼?

Electron 內置了 Chromium 內核 和 Node,所以可使用 HTML 和 CSS 來實現應用的 GUI 界面,用 JavaScript 調用豐富的原生 API 實現桌面應用。你也能夠將 Electron 看做是一個由 JavaScript 控制的一個小型的 Chrome 內核瀏覽器。html

因爲內置的 Chromium 內核 和 Node, 所以咱們不須要關心前端的兼容問題,你甚至能夠寫 -webkit- only 的代碼; 也不須要關心一些須要編譯的 Node 模塊兼容問題,由於 Node 版本是固定的。所以,用 Electron 來編寫跨平臺應用程序是很是合適的。前端

或許你還不知道,Visual Studio Codewordpressslack 等客戶端都是基於 Electron 開發的。java

下面,先快速上手一下。node

快速入門

相信你看到這裏都是對 Node 有必定了解的,故這裏再也不對 Node 的安裝進行描述。python

咱們有以下目錄結構:linux

electron-quick-start/
    ├── package.json
    ├── main.js
    └── index.html

package.json 跟常規 Node 程序一致,將 main.js 做爲 程序的啓動入口文件,基本內容以下:git

{
  "name"    : "electron-quick-start",
  "version" : "1.0.0",
  "main"    : "main.js",
  "scripts" : {
    "start" : "electron main.js"
  },
  "devDependencies": {
    "electron-prebuilt": "^1.2.0"
  }
}

咱們用 index.html 做爲咱們的程序界面,簡單的界面代碼以下:github

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

接着是最重要的入口文件 main.js 的編寫了,其內容以下:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
let mainWindow;

function createWindow () {
  //建立一個 800x600 的瀏覽器窗口
  mainWindow = new BrowserWindow({width: 800, height: 600});

  //加載應用的界面文件
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  //打開開發者工具,方便調試
  //mainWindow.webContents.openDevTools();

  mainWindow.on('closed', function () {
    mainWindow = null;
  });
}

app.on('ready', createWindow);

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', function () {
  if (mainWindow === null) {
    createWindow();
  }
});

最後,執行:

npm install && npm start

運行結果以下圖:

clipboard.png

當程序啓動時,Electron 調用在 package.json 中定義的 main.js 文件並執行它。這個過程當中,Electron 會建立一個主進程,主進程調用 BrowserWindow 模塊建立瀏覽器窗口,每一個瀏覽器窗口都有本身獨立的渲染進程,渲染進程負責渲染 HTML 文件,以做爲程序的 GUI 界面。

主進程管理全部頁面和與之對應的渲染進程。每一個渲染進程都是相互獨立的,而且只關心他們本身的網頁。

clipboard.png

至此,相信你對 Electron 的運行過程已有必定了解了,下面,我將介紹一下我是如何將咱們的前端工做流程(tmt-workflow) 封裝成桌面應用(WeFlow)的。

應用實踐

現狀

tmt-workflow : 是一個基於 Gulp(v4.0),經過約定必定的項目結構和配置文件實現高效、跨平臺(Mac & Win)、可定製的前端工做流程。

其擁有 4 個任務(gulp task) :

  1. 開發任務(gulp build_dev)

  2. 生產任務(gulp build_dist)

  3. 部署任務(gulp ftp)

  4. 打包任務(gulp zip)

運行時須要先安裝(npm install) ,再執行相應任務命令,也能夠配合 WebStorm 等編輯器的 gulp 任務管理器 使用。

目標

利用現有的 tmt-workflow, 包裝成一個 可視化 界面,不須要安裝(npm install) ,直接下載打開便可使用。具體擁有:

  • 可視化的項目管理(新建、打開、配置、刪除)

  • 可視化的全局項目配置

  • 可視化的任務執行(開發、生產編譯、FTP 部署、Zip 打包)

  • 可視化的 log 日誌反饋

設計效果預覽

clipboard.png

主要由幾部分組成:

  1. 第一次打開時的歡迎頁

  2. 主窗體,由項目列表和任務列表組成,選擇具體項目執行任務流程

  3. 全局設置頁

  4. 項目設置頁

  5. 關於

實現

核心: 如何將 gulp 程序轉換

咱們知道,gulp 的任務執行必需在命令行下執行,如: gulp build_dist ,這裏的 gulp 是一個命令,是一個全局的 cli。執行時依賴於項目下的 node_modules

基於 gulp 程序的以上特色,咱們的思路以下:

思路 1: 若是咱們什麼都不改變的話,直接把 tmt-workflow 這個 gulp 工做流封裝,那可能的思路就是:

當點擊可視化的任務按鈕執行時,

  1. 先進入所要執行的項目的目錄

  2. 再調用子進程執行 gulp 命令:

let exec = require('child_process').exec;
exec('gulp build_dist', {'cwd': 'projectPath'});

這樣子,任何 gulp 流程都不須要改動,直接在其上面套一個殼,這個殼提供一下可視化的交互,而後幫你執行相應的 gulp 任務。

思路貌似挺好的,但跟咱們的目標有點衝突,咱們之因此要封裝打包,爲的就是省去用戶安裝,讓用戶打開即能用。而這個思路的執行方式須要在用戶的項目目錄下面執行 gulp 任務,那程序依賴的依然是用戶已安裝的 node_modules,而安裝的過程有些模塊(如圖形模塊)須要本地編譯,而編譯又依賴於用戶系統的 node 版本和相關環境(如 win 下須要 python2.7.3 和 VS2010),這有時候是一個漫長又痛苦的過程。這就是爲何要省去安裝的緣由了。

因此,咱們有了思路 2。

思路 2: 將 gulp 工做流程序node_modules 一塊兒打包進 Electron ,當點擊可視化的任務按鈕執行時:

  1. 獲取項目的路徑

  2. 將整個項目傳進 Electron 裏面打包的工做流執行一遍

  3. 將編譯後的文件輸出

觀察咱們的 gulp 任務寫法,都有一個固定的結構,以下:

//編譯 less
function compileLess() {
    gulp.src(paths.src.less)
        .pipe(less())
        .pipe(gulp.dest(paths.dist.css))
}

//註冊 build_dist 任務
gulp.task('build_dist', gulp.series(
    delDist,
    compileLess,
    ...
));

就是利用 gulp.src 讀取資源,而後通過一系列處理以後再用 gulp.dest 輸出。而後再經過 gulp 註冊一個 gulp 任務,便可用 gulp 命令調用執行。若是能夠把 gulp 從這個過程當中去掉,換成普通的程序,則就能夠不須要命令行調用,也就能夠依賴於當前 Electron 打包的 node_modules ,實現封裝的目的。

經過觀察 gulp 的實現咱們能夠看到以下代碼:

var vfs = require('vinyl-fs');

function Gulp() {
  Undertaker.call(this);

  // Bind the functions for destructuring
  this.watch = this.watch.bind(this);
  this.task = this.task.bind(this);
  this.series = this.series.bind(this);
  this.parallel = this.parallel.bind(this);
  this.registry = this.registry.bind(this);
  this.tree = this.tree.bind(this);
  this.lastRun = this.lastRun.bind(this);
}

Gulp.prototype.src = vfs.src;
Gulp.prototype.dest = vfs.dest;

咱們發現,gulp.src 和 gulp.dest 其實是 vinyl-fs 模塊的實現。而原來 gulp 任務註冊的 同步(gulp.parallel) 和 異步(gulp.series) 處理,咱們也能夠直接用 async 來替代,所以,咱們稍微改動能夠變成:

const async = require('async');
const vfs = require('vinyl-fs');

//編譯 less
function compileLess(cb) {
    vfs.src(paths.src.less)
        .pipe(less())
        .pipe(vfs.dest(paths.dist.css))
        .on('end', cb);
}

async.series([
    function (next) {
        compileLess(next);
    }
], function (error) {
    if (error) {
        throw new Error(error);
    }
});

這個樣子,就跟 gulp 無關了,但相關編譯模塊都還直接用的原來基於 gulp 的模塊,因此,只須要稍加改動,就能夠利用現有的 gulp 工做流快速實現 GUI 程序。

解決了核心的 gulp 流程轉換,剩下的就是一些邏輯交互處理、配置功能、數據存儲、菜單欄和快捷鍵功能等的實現了。下面對整個項目的相關實現進行介紹。

項目結構
WeFlow/
    ├── about.html              //關於界面
    ├── app.html                //主界面
    ├── assets/                 //資源目錄
    │   ├── css
    │   ├── img
    │   └── js
    ├── main.js                 //應用入口文件
    ├── package.json
    ├── src/                    //源文件目錄
    │   ├── _tasks/
    │   ├── app.js
    │   ├── common.js
    │   ├── createDev.js
    │   └── menu.js
    ├── templates/              //模版目錄
    │   └── project.zip
    └── weflow.config.json      //配置文件
數據存儲

WeFlow 須要對用戶的一些操做進行記錄(新建或打開了多少項目)進行存儲,以便下次打開時還原。
Weflow 是一個本地程序,故數據不須要存儲在雲端,只須要存儲在用戶本地便可。因此直接使用 localStorage 來存儲數據,WeFlow 構造的數據對象以下:

{
    "name": "WeFlow",
    "workspace": "/Users/littledu/WeFlow_workspace",
    "projects": {
        "project": {
            "path": "/Users/littledu/WeFlow_workspace/project",
            "devPath": "/Users/littledu/WeFlow/src/_tasks/tmp_dev/0c0876c4232f1de240f519f0920f2d60.js",
            "pid": 0
        }
    }
}

整個程序運行的過程當中都是基於此對象進行操做。打開程序時,會讀取此數據,進行界面內容填充。當項目位置或開發狀態變更時,也更新數據存儲進 localStorage。

菜單欄和快捷鍵功能設計

圖片描述

menu 模塊是一個主進程模塊,能夠用來建立原生菜單,每一個菜單有一個或幾個菜單項 menu items,而且每一個菜單項能夠有子菜單。

Electron 有一個 global-shortcut 模塊專門用來設置(註冊/註銷)各類自定義操做的快捷鍵。但經過 menu 模塊也能夠綁定快捷鍵,代碼以下:

const electron = require('electron');
const remote = electron.remote;
const Menu = remote.Menu;

var template = [
    {
        label: '文件',
        submenu: [
            {
                label: '新建項目',
                accelerator: 'CmdOrCtrl+N',
                click: function (item, focusedWindow) {
                    newProjectFn();
                }
            },
            {
                label: '打開項目…',
                accelerator: 'CmdOrCtrl+O',
                click: function (item, focusedWindow) {
                    let projectPath = remote.dialog.showOpenDialog({ properties: [ 'openDirectory' ]});
                    if(projectPath && projectPath.length){
                        openProject(projectPath[0]);
                    }
                }
            }
        ]
    }
];

menu 是主進程模塊,但在這裏想給快捷鍵綁定渲染進程中的功能。故調用了 remote 模塊進行渲染進程和主進程通訊。

遇到的問題

1. 瀏覽器自動刷新監聽功能沒法中斷(browser-sync@2.13.0 以前)

tmt-workflow 使用 browser-sync 實現開發任務的自動刷新功能。常規狀況下使用結束時,經過 cmd+c 或 ctrl+c 中斷。然而封裝後再也不是經過命令行方式調用,故沒法經過命令行來中斷。 browser-sync 也沒有提供 API 中斷。故 WeFlow 中的 開發任務 跟其餘的任務不一樣,解決方式是:

用子進程 child_process.fork 來執行開發任務的 dev.js,將返回的 PID 保存,便可經過這個 PID 來中斷對應的子進程,達到中止開發任務的目的。

原理代碼以下:

let childProcess = require('child-process');
function runDevTask(devPath){
    let child = childProcess.fork(devPath, {silent: true});

    child.stdout.on('data', function (data) {
        logReply(data.toString());
    });

    child.stderr.on('data', function (data) {
        logReply(data.toString());
    });

    child.on('close', function (code) {
        if (code !== 0) {
            logReply(`child process exited with code ${code}`);
        }
    });
}

function killChildProcess(pid){
    try {
        if(process.platform === 'win32'){
            childProcess.exec('taskkill /pid ' + pid);
        }else{
            process.kill(pid);
        }
    }
}

2. windows 下打包 EXE 後不能使用 process.stdout

官方認爲,Electron 實現的都是 GUI 程序,因此理論上不須要這種輸出功能。雖然在調試階段並不影響,但打包的時候記得去掉,要否則會報錯。

打包

electron-packager 能夠用來打包 Electron 應用。生成各個平臺的最終可運行文件,如 .app.exe

使用命令:

electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]
  • <sourcedir>: 項目的位置

  • <appname>: 應用名

  • --platform=<platform>: 打包的系統(darwin、win3二、linux)

  • --arch=<arch>: 系統位數(ia3二、x64)

  • --icon=<icon>: 指定應用的圖標(Mac 爲 .icns 文件,Windows 爲 .ico 或 .png)

  • --out <out>: 指定輸出的目錄

  • --version=<version>: 指定編譯的 electron-prebuilt 版本

例子:

electron-packager ./ WeFlow --platform=darwin --arch=x64 --icon=./assets/img/WeFlow.icns --overwrite --out ./dist --version=0.37.8

咱們能夠直接在 package.jsonscript 字段中添加腳本,以下:

"scripts": {
    "build:all": "electron-packager . --all --overwrite",
    "build:mac": "electron-packager ./ WeFlow --platform=darwin --arch=x64 --icon=./assets/img/WeFlow.icns --overwrite --out ./dist --version=0.37.8",
    "build:win64": "electron-packager ./ WeFlow --platform=win32 --arch=x64 --icon=./assets/img/WeFlow.png --overwrite --out ./dist --version=0.37.8",
    "build:win32": "electron-packager ./ WeFlow --platform=win32 --arch=ia32 --icon=./assets/img/WeFlow.png --overwrite --out ./dist --version=0.37.8 --app-version=1.0.0"
}

注意:不要認爲一個系統能夠完成全部系統的打包

若是你引用了一些原生模塊(如 lwip),它是必需根據目標系統編譯生成 .node 文件。遇到這種狀況,則沒法在一個系統上面打包另外一個系統的可執行程序。更好的作法是利用 AppVeyorTravis 來爲各平臺實現打包自動化。能夠經過相應官網進行了解。

electron-packager 打包後的文件能夠看到源代碼,想更進一步打包能夠用 electron-builder

下載體驗地址

參考文檔

相關文章
相關標籤/搜索