本文介紹如何構(gòu)建一個(gè)electron應(yīng)用,適用于有HTML/CSS/JavaScript基礎(chǔ)的人閱讀讲仰,在開(kāi)始前需要先在電腦上安裝Node.js骡澈。
Electron介紹
Electron 是由 Github開(kāi)發(fā)的開(kāi)源框架,它允許開(kāi)發(fā)者使用Web技術(shù)來(lái)開(kāi)發(fā)跨平臺(tái)的桌面應(yīng)用刹帕。著名項(xiàng)目包括GitHub的Atom和微軟的Visual Studio Code牡属。
Electron架構(gòu)的核心由三部分組成:
- Chromium: 為electron提供了強(qiáng)大的UI能力诺苹,可以不考慮兼容性的情況下以故,利用強(qiáng)大的Web生態(tài)來(lái)開(kāi)發(fā)界面
- Node.js:讓electron有了底層的操作能力扩氢,比如文件的讀寫(xiě)院喜,甚至是集成C++等等操作,并可以使用大量開(kāi)源的 npm 包來(lái)完成開(kāi)發(fā)需求
- Native API:Native API讓electron有了跨平臺(tái)和桌面端的原生能力证芭,比如說(shuō)它有統(tǒng)一的原生界面瞳浦,窗口、托盤(pán)這些
主進(jìn)程和渲染進(jìn)程
electron是多進(jìn)程架構(gòu)废士,在開(kāi)始項(xiàng)目搭建之前叫潦,先來(lái)了解下electron的兩個(gè)核心概念:主進(jìn)程和渲染進(jìn)程
主進(jìn)程
主進(jìn)程負(fù)責(zé)創(chuàng)建和管理BrowserWindow實(shí)例以及各種應(yīng)用程序事件。它還可以執(zhí)行諸如注冊(cè)全局快捷方式官硝,創(chuàng)建系統(tǒng)菜單和對(duì)話框矗蕊,響應(yīng)自動(dòng)更新事件等操作。應(yīng)用程序的入口點(diǎn)將指向?qū)⒃谥鬟M(jìn)程中執(zhí)行的JavaScript文件氢架。
一個(gè)項(xiàng)目有且只有一個(gè)主進(jìn)程
渲染進(jìn)程
渲染過(guò)程負(fù)責(zé)運(yùn)行應(yīng)用程序的用戶界面傻咖。
每創(chuàng)建一個(gè)窗口都會(huì)創(chuàng)建一個(gè)渲染進(jìn)程;并且每個(gè)渲染進(jìn)程都是獨(dú)立的岖研。
項(xiàng)目工程搭建
接下來(lái)開(kāi)始搭建electron項(xiàng)目工程
使用quick-start創(chuàng)建項(xiàng)目
為了簡(jiǎn)化步驟卿操,可以使用 quick-start 來(lái)搭建electron項(xiàng)目
// 第一步:clone electron-quick-start
git clone https://github.com/electron/electron-quick-start
// 第二步:安裝依賴
cd electron-project && npm install
// 運(yùn)行項(xiàng)目
npm run start
搭建過(guò)程中可能會(huì)遇到安裝依賴失敗的問(wèn)題警检,具體解決方法可以文末
項(xiàng)目結(jié)構(gòu)介紹
├── index.html
├── main.html
├── package.json
├── preload.js
上圖是剛剛創(chuàng)建的項(xiàng)目的目錄結(jié)構(gòu),接下來(lái)介紹下electron應(yīng)用中最主要的三種文件
package.json(元數(shù)據(jù))
配置文件害淤。配置應(yīng)用的相關(guān)信息及工程依賴扇雕。其中main字段定義了應(yīng)用的啟動(dòng)入口,在此項(xiàng)目中窥摄,入口文件為src/main.js
mian.js (啟動(dòng)文件)
運(yùn)行在項(xiàng)目主進(jìn)程中镶奉。在此文件中啟動(dòng)應(yīng)用,創(chuàng)建瀏覽器崭放,加載頁(yè)面哨苛。
const {app, BrowserWindow} = require('electron')
const path = require('path')
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
app
代表著整個(gè)應(yīng)用,用app.on
監(jiān)聽(tīng)?wèi)?yīng)用的狀態(tài)币砂,當(dāng)達(dá)到ready狀態(tài)后移国,使用BrowserWindow
(electron提供的模塊)創(chuàng)建了一個(gè)寬800,高600的窗口道伟,再使用loadFile
,在窗口中加載 index.html
文件使碾。
webPreferences
是BrowserWindow
的屬性蜜徽,用來(lái)設(shè)置網(wǎng)頁(yè)功能。preload
是webPreferences
屬性的參數(shù)票摇,在頁(yè)面運(yùn)行其他腳本之前預(yù)先加載指定的腳本拘鞋,無(wú)論頁(yè)面是否集成Node, 此腳本都可以訪問(wèn)所有Node API 腳本路徑為文件的絕對(duì)路徑。
可以在createWindow方法最后添加mainWindow.webContents.openDevTools()
代碼矢门,表示打開(kāi)控制臺(tái)
index.html
運(yùn)行在項(xiàng)目渲染進(jìn)程中盆色。該文件為項(xiàng)目展示的界面,類似于移動(dòng)端開(kāi)發(fā)的h5界面祟剔。
項(xiàng)目開(kāi)發(fā)
主進(jìn)程和渲染進(jìn)程的通信
electron 可以使用node.js的api和Native API隔躲,但是electron不建議直接在渲染進(jìn)程(即界面)中直接使用,需要通過(guò)兩個(gè)進(jìn)程的通信物延,在主進(jìn)程中完成操作宣旱。
考慮到在網(wǎng)頁(yè)中直接調(diào)用原生的 GUI 容易造成資源溢出,這很危險(xiǎn)叛薯,開(kāi)發(fā)者不能這么使用浑吟。如果開(kāi)發(fā)者想要在網(wǎng)頁(yè)上執(zhí)行 GUI 操作,必須要通過(guò)渲染器進(jìn)程和主進(jìn)程的通信實(shí)現(xiàn)耗溜。
主進(jìn)程和渲染進(jìn)程的通信可以使用ipc模塊來(lái)實(shí)現(xiàn)组力,以實(shí)現(xiàn)以下需求為例,簡(jiǎn)單介紹如何實(shí)現(xiàn)渲染器進(jìn)程和主進(jìn)程的通信實(shí)現(xiàn)抖拴。
頁(yè)面上有一個(gè)按鈕和一個(gè)輸入框燎字,當(dāng)點(diǎn)擊按鈕之后,向主進(jìn)程發(fā)送了一個(gè) write-file 的消息,當(dāng)主進(jìn)程接收到消息之后轩触,在安裝目錄下創(chuàng)建一個(gè)叫 hello.txt的文件寞酿,并寫(xiě)入輸入框內(nèi)的內(nèi)容。文件生成后發(fā)送成功的消息給渲染進(jìn)程脱柱,彈出提示告訴用戶已完成伐弹。
更改preload.js
在preload.js文件中,通過(guò)contextBridge模塊榨为,將ipcRenderer模塊的api暴露給渲染器
contextBridge Create a safe, bi-directional, synchronous bridge across isolated contexts
// preload.js
const ipcRenderer = require('electron').ipcRenderer
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('ipcRenderer', {
on: (eventName, callback) => {
ipcRenderer.on(eventName, callback)
},
once: (eventName, callback) => {
ipcRenderer.once(eventName, callback)
},
send: ipcRenderer.send,
})
ps:
在低版本的electron中惨好,直接賦值在window上
window.ipcRenderer = require('electron').ipcRenderer
渲染進(jìn)程 => 主進(jìn)程
- 在index.html渲染進(jìn)程中添加一個(gè)input輸入框和button按鈕,在點(diǎn)擊按鈕時(shí)獲取輸入框的內(nèi)容随闺,并使用ipcRenderer.send方法發(fā)送write-file事件
ipcRenderer 是一個(gè) EventEmitter 的實(shí)例日川。 可以使用它提供的一些方法從渲染進(jìn)程 (web 頁(yè)面) 發(fā)送同步或異步的消息到主進(jìn)程。 也可以接收主進(jìn)程回復(fù)的消息矩乐。
// index.html
// html部分
<input type="text" id="input">
<button id="button">say hi</button>
// js部分
document.getElementById('button').onclick = function () {
const content = document.getElementById('input').value
window.ipcRenderer.send('write-file', {
content: content,
});
};
- 在main.js主進(jìn)程中使用ipcMain.on監(jiān)聽(tīng)write-file事件龄句,接受到信息后使用node的fs模塊生成并寫(xiě)入hello.txt文件
ipcMain 是一個(gè) EventEmitter 的實(shí)例。 當(dāng)在主進(jìn)程中使用時(shí)散罕,它處理從渲染器進(jìn)程(網(wǎng)頁(yè))發(fā)送出來(lái)的異步和同步信息分歇。 從渲染器進(jìn)程發(fā)送的消息將被發(fā)送到該模塊。
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs')
ipcMain.on('write-file', (evt, data) => {
fs.writeFileSync('./hello.txt', data.content, 'utf-8')
})
主進(jìn)程 => 渲染進(jìn)程
- 在文件生成成功后欧漱,使用當(dāng)前窗口 BrowserWindow 實(shí)例的webContents屬性的send方法职抡,發(fā)送file-complete 事件
webContents是一個(gè)EventEmitter. 負(fù)責(zé)渲染和控制網(wǎng)頁(yè), 是 BrowserWindow 對(duì)象的一個(gè)屬性。
+ let mainWindow
const createWindow = () => {
+ mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
mainWindow.loadFile(path.join(__dirname, 'index.html'));
mainWindow.webContents.openDevTools();
};
ipcMain.on('write-file', (evt, data) => {
fs.writeFileSync('./hello.txt', data.content, 'utf-8')
+ mainWindow.webContents.send('write-complete', { status: true })
})
- 在index.html渲染進(jìn)程使用ipcRenderer.on方法監(jiān)聽(tīng)事件
document.getElementById('button').onclick = function () {
const content = document.getElementById('input').value
window.ipcRenderer.send('write-file', {
content: content,
});
+ window.ipcRenderer.once('write-complete', (event, data) => {
+ if (data.status) alert('文件生成成功')
+ });
};
打包及自動(dòng)更新
應(yīng)用打包
使用electron-builder來(lái)打包
安裝依賴
npm install electron-builder --save-dev
配置build屬性
在package.json文件中误甚,添加build屬性
"build": {
"appId": "com.test.electron", // 包名
"productName": "electron-project", // 項(xiàng)目名缚甩,也是生成包的前綴名
"mac": { // mac平臺(tái)相關(guān)配置
"icon": "public/icon.png", // mac應(yīng)用圖標(biāo),最小為512x512
"target": [ "dmg", "zip" ]
},
"dmg": {
"window": { // dmg安裝器窗口設(shè)置
"x": 200,
"y": 200,
"width": 400,
"height": 400
}
},
"win": { // windows平臺(tái)相關(guān)配置
"icon": "public/icon.png" // windows應(yīng)用圖標(biāo)窑邦,最小為256x256
},
"nsis": { // 安裝過(guò)程的配置
"oneClick": false,
"allowToChangeInstallationDirectory": true, //允許修改安裝目錄
"createDesktopShortcut": "always", //創(chuàng)建桌面圖標(biāo)
"createStartMenuShortcut": false, //創(chuàng)建開(kāi)始菜單圖標(biāo)
"installerIcon": "", // 安裝圖標(biāo)
"uninstallerIcon": "" // 卸載圖標(biāo)
}
},
配置打包命令
在package.json文件中添加script命令
"script": {
"start": "electron .",
+ "build": "electron-builder"
}
在不同平臺(tái)的環(huán)境下運(yùn)行npm run build
打包擅威,成功后安裝包在dist文件夾下
在打包過(guò)程中可能會(huì)遇到下載文件失敗的問(wèn)題,具體解決方法看文末
自動(dòng)更新
可以使用electron-builder來(lái)實(shí)現(xiàn)自動(dòng)更新
安裝electron-builder
npm install electron-builder --save-dev
配置publish
配置publish
字段冈钦,在打包后生成latest.yml
文件裕寨,程序更新依賴這個(gè)文件做版本判斷
latest.yml文件是打包過(guò)程生成的文件,為避免自動(dòng)更新出錯(cuò)派继,打包后禁止對(duì)latest.yml文件做任何修改宾袜。如果文件有誤,必須重新打包獲取新的latest.yml文件
// package.json
"build": {
+ "publish": [
+ {
+ "provider": "generic",
+ "url": ""
+ }
+ ],
}
主進(jìn)程代碼
在main.js中添加以下autoUpdater代碼
// main.js
const { autoUpdater } = require('electron-updater')
function createWindow () {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile(path.join(__dirname, 'index.html'))
mainWindow.webContents.openDevTools()
mainWindow.webContents.on('did-finish-load', function () {
setTimeout(() => {
updateApp()
}, 5000)
})
}
function sendUpdateMessage(message, data) {
mainWindow.webContents.send("message", { message, data });
}
function updateApp () {
let url = 'http://127.0.0.1:8080/' + process.platform // 安裝包所在服務(wù)器地址驾窟,本地測(cè)試可以使用http-server搭建靜態(tài)服務(wù)器
autoUpdater.setFeedURL(url); // 設(shè)置更新服務(wù)器的地址
autoUpdater.on("error", function(message) { // 報(bào)錯(cuò)
sendUpdateMessage("error", message);
})
autoUpdater.on("checking-for-update", function(message) { // 檢查更新事件
sendUpdateMessage("checking-for-update", message);
})
autoUpdater.on("update-available", function(message) { // 有需要更新的版本
sendUpdateMessage("update-available", message);
})
autoUpdater.on("update-not-available", function(message) { // 沒(méi)有需要更新的版本
sendUpdateMessage("update-not-available", message);
})
autoUpdater.on("download-progress", function(progressObj) { // 更新下載進(jìn)度事件
sendUpdateMessage("downloadProgress", progressObj);
})
autoUpdater.on("update-downloaded", function( // 下載成功事件
event,
releaseNotes,
releaseName,
releaseDate,
updateUrl,
quitAndUpdate
) {
ipcMain.on("updateNow", (e, arg) => {
// 停止當(dāng)前程序并安裝
autoUpdater.quitAndInstall();
});
sendUpdateMessage("isUpdateNow", null);
})
autoUpdater.checkForUpdates(); // 執(zhí)行檢查更新
}
渲染進(jìn)程代碼
在渲染進(jìn)程的js文件中添加以下代碼
window.ipcRenderer.on("message", (event, { message, data }) => {
switch (message) {
case "isUpdateNow":
if (confirm("現(xiàn)在更新庆猫?")) {
ipcRenderer.send("updateNow");
}
break;
default:
break;
}
});
上傳應(yīng)用
更新packge.json文件中的版本號(hào),打包后將安裝包和yml文件(MAC下是latest-mac.yml,zip和dmg文件绅络;Windows下是latest.yml和exe文件)放在服務(wù)器對(duì)應(yīng)平臺(tái)的目錄下
-
mac
-
windows
打開(kāi)低版本的應(yīng)用月培,electron-updater會(huì)通過(guò)對(duì)應(yīng)url下的yml文件檢查更新
問(wèn)題
依賴安裝失敗問(wèn)題解決
在創(chuàng)建項(xiàng)目的過(guò)程中嘁字,安裝依賴可能會(huì)報(bào)錯(cuò),可以嘗試使用以下方式解決
1. 設(shè)置國(guó)內(nèi)electron鏡像地址
mac直接運(yùn)行以下命令
export ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"
window需要添加環(huán)境變量 ELECTRON_MIRROR
值為 https://npm.taobao.org/mirrors/electron/
2. 重新npm install
electron-builder打包失敗
electron-builder 在打包時(shí)會(huì)檢測(cè)cache中是否有electron 包杉畜,如果沒(méi)有的話會(huì)從github上拉去纪蜒,在國(guó)內(nèi)網(wǎng)絡(luò)環(huán)境中拉取的過(guò)程大概率會(huì)失敗,所以你可以自己去下載一個(gè)包放到cache目錄里
各個(gè)平臺(tái)的目錄地址
MacOs: ~/Library/Caches/electron
Window: %LOCALAPPDATA%/electron/Cache or ~/AppData/Local/electron/Cache/
Linux: $XDG_CACHE_HOME or ~/.cache/electron/