現現在,用 HTML、JavaScript、CSS、Node.js 寫桌面應用早已不是什麼新鮮的事了,做爲一名前端,可以使用本身熟悉的語言,快速實現本身想要的桌面應用,是件很讓人興奮的事。
目前常見的有 NW、heX、Electron。今天,就來簡單的上手一下 Electron。javascript
Electron 是一款能夠利用 Web技術 開發跨平臺桌面應用的框架,最初是 Github 發佈的 Atom 編輯器衍生出的 Atom Shell,後改名爲 Electron。css
Electron 內置了 Chromium 內核 和 Node,所以可使用 HTML 和 CSS 來實現應用的 GUI 界面,用 JavaScript 調用豐富的原生 API 實現桌面應用。你也能夠將 Electron 看做是一個由 JavaScript 控制的一個小型的 Chrome 內核瀏覽器。html
因爲內置的 Chromium 內核 和 Node, 所以咱們不須要關心前端的兼容問題,你甚至能夠寫 -webkit- only
的代碼; 也不須要關心一些須要編譯的 Node 模塊兼容問題,由於 Node 版本是固定的。所以,用 Electron 來編寫跨平臺應用程序是很是合適的。前端
或許你還不知道,Visual Studio Code 、wordpress 和 slack 等客戶端都是基於 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
運行結果以下圖:
當程序啓動時,Electron 調用在 package.json 中定義的 main.js 文件並執行它。這個過程當中,Electron 會建立一個主進程,主進程調用 BrowserWindow
模塊建立瀏覽器窗口,每一個瀏覽器窗口都有本身獨立的渲染進程,渲染進程負責渲染 HTML
文件,以做爲程序的 GUI 界面。
主進程管理全部頁面和與之對應的渲染進程。每一個渲染進程都是相互獨立的,而且只關心他們本身的網頁。
至此,相信你對 Electron 的運行過程已有必定了解了,下面,我將介紹一下我是如何將咱們的前端工做流程(tmt-workflow) 封裝成桌面應用(WeFlow)的。
tmt-workflow : 是一個基於 Gulp(v4.0),經過約定必定的項目結構和配置文件實現高效、跨平臺(Mac & Win)、可定製的前端工做流程。
其擁有 4 個任務(gulp task) :
開發任務(gulp build_dev)
生產任務(gulp build_dist)
部署任務(gulp ftp)
打包任務(gulp zip)
運行時須要先安裝(npm install) ,再執行相應任務命令,也能夠配合 WebStorm 等編輯器的 gulp 任務管理器 使用。
利用現有的 tmt-workflow, 包裝成一個 可視化
界面,不須要安裝(npm install)
,直接下載打開便可使用。具體擁有:
可視化的項目管理(新建、打開、配置、刪除)
可視化的全局項目配置
可視化的任務執行(開發、生產編譯、FTP 部署、Zip 打包)
可視化的 log 日誌反饋
主要由幾部分組成:
第一次打開時的歡迎頁
主窗體,由項目列表和任務列表組成,選擇具體項目執行任務流程
全局設置頁
項目設置頁
關於
gulp
程序轉換咱們知道,gulp 的任務執行必需在命令行下執行,如: gulp build_dist
,這裏的 gulp
是一個命令,是一個全局的 cli
。執行時依賴於項目下的 node_modules
。
基於 gulp 程序的以上特色,咱們的思路以下:
思路 1: 若是咱們什麼都不改變的話,直接把 tmt-workflow 這個 gulp 工做流封裝,那可能的思路就是:
當點擊可視化的任務按鈕執行時,
先進入所要執行的項目的目錄
再調用子進程執行 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 ,當點擊可視化的任務按鈕執行時:
獲取項目的路徑
將整個項目傳進 Electron 裏面打包的工做流執行一遍
將編譯後的文件輸出
觀察咱們的 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.json
的 script
字段中添加腳本,以下:
"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 文件。遇到這種狀況,則沒法在一個系統上面打包另外一個系統的可執行程序。更好的作法是利用 AppVeyor 和 Travis 來爲各平臺實現打包自動化。能夠經過相應官網進行了解。
electron-packager 打包後的文件能夠看到源代碼,想更進一步打包能夠用 electron-builder 。