簡介
Electron 基于 Chromium 和 Node.js, 讓你可以使用 HTML, CSS 和 JavaScript 構(gòu)建桌面端應(yīng)用。
這篇文章摘錄了我自己在真實(shí)項(xiàng)目中的要點(diǎn)
和問題
触趴。
可能有的小伙伴會(huì)問,為什么不說一下與Vue渴肉、React或者其他的庫如何結(jié)合開發(fā)冗懦?
這是因?yàn)閂ue或者React以及其他庫都是UI頁面,可以單獨(dú)獨(dú)立開發(fā)仇祭,只需要在代碼打包壓縮后披蕉,使用BrowserWindow中的loadURL或者loadFile加載即可。
剛開始學(xué)習(xí)Electron的小伙伴可以先看 Electron快速入手乌奇,擁有自己的第一個(gè)桌面應(yīng)用 没讲。
Electron核心
Electron實(shí)現(xiàn)跨平臺(tái)桌面應(yīng)用程序的原理
通過集成瀏覽器內(nèi)核,使用前端的技術(shù)來實(shí)現(xiàn)不同平臺(tái)下的渲染礁苗,并結(jié)合了 Chromium
爬凑、Node.js
和用于調(diào)用系統(tǒng)本地功能的 API
三大板塊。
- Chromium 提供強(qiáng)大的
UI
渲染能力试伙,用于顯示網(wǎng)頁內(nèi)容嘁信。 - Node.js 用于本地文件系統(tǒng)和操作系統(tǒng)于样,提供
GUI
的操作能力(如path、fs潘靖、crypto 等模塊)百宇。 - Native API為Electron提供原生系統(tǒng)的
GUI
支持,使用 Electron Api 可以調(diào)用原生應(yīng)用程序接口秘豹。
Electron主要核心點(diǎn)
Electron其實(shí)很簡單携御,基本都是api,我自己整理了主要核心點(diǎn)有進(jìn)程間通信
既绕、app生命周期
啄刹、BrowserWindow
、Application
4個(gè)方面凄贩。
進(jìn)程間通信
:處理主進(jìn)程與渲染進(jìn)程的雙向通信誓军。
app生命周期
:貫穿桌面端應(yīng)用整個(gè)生命周期(用的不多,但是很重要)疲扎。
BrowserWindow
:窗口(大家可以理解成每一個(gè)BWindow都是一個(gè)獨(dú)立的瀏覽器昵时,用于加載渲染我們的前端代碼)。
Application
:應(yīng)用程序功能點(diǎn)(為應(yīng)用提供更佳完善的功能點(diǎn))椒丧。
從這4個(gè)主要核心點(diǎn)入手壹甥,開發(fā)人員會(huì)更快進(jìn)入開發(fā)狀態(tài)。
進(jìn)程間通信
ipcMain與ipcRenderer
electron存在多個(gè)進(jìn)程壶熏,那么多進(jìn)程間如何實(shí)現(xiàn)通信句柠,electron官方提供了方法。
注意:不建議使用同步通信棒假,這會(huì)對應(yīng)用整體性能產(chǎn)生影響溯职。
注意:進(jìn)程通信所傳參數(shù)只能為原始類型數(shù)據(jù)和可被 JSON.stringify 的引用類型數(shù)據(jù)。故不建議使用JSON.stringify帽哑。
異步
通過event.reply(...)
將異步消息發(fā)送回發(fā)送者谜酒。
event.reply會(huì)自動(dòng)處理從非主 frame 發(fā)送的消息,建議使用event.sender.send
或者win.webContents.send
總是把消息發(fā)送到主 frame妻枕。
主進(jìn)程main
const { ipcMain } = require('electron')
// 接收renderer的數(shù)據(jù)
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // 傳遞的數(shù)據(jù) ping
// event.reply('asynchronous-reply', 'pong') // 發(fā)送數(shù)據(jù)到渲染進(jìn)程
event.sender.send('asynchronous-reply', 'pong')
})
渲染進(jìn)程renderer
const { ipcRenderer } = require('electron')
// 接收main的數(shù)據(jù)
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
// 發(fā)送數(shù)據(jù)到主進(jìn)程
ipcRenderer.send('asynchronous-message', 'ping')
同步
也可以設(shè)置同步接收(不建議使用),通過event.returnValue
設(shè)置
主進(jìn)程main
ipcMain.on('synchronous-message', (event, arg) => {
event.returnValue = 'sync'
})
渲染進(jìn)程renderer
const syncMsg = ipcRenderer.sendSync('synchronous-message', 'ping');
console.log(syncMsg)
ipcMain.once(channel, listener)
只監(jiān)聽實(shí)現(xiàn)一次僻族。
ipcMain.removeListener(channel, listener)
刪除某一指定的監(jiān)聽。
ipcMain.removeAllListeners([channel])
移除多個(gè)指定 channel 的監(jiān)聽器佳头,無參移除所有鹰贵。
Promise
進(jìn)程發(fā)送消息,并異步等待結(jié)果
ipcMain提供handle康嘉,ipcRenderer提供了invoke。
// 渲染進(jìn)程
ipcRenderer.invoke('some-name', someArgument).then((result) => {
// ...
})
// 主進(jìn)程
ipcMain.handle('some-name', async (event, someArgument) => {
const result = await doSomeWork(someArgument)
return result
})
app
app控制應(yīng)用程序的事件生命周期
const {app, BrowserWindow} = require('electron')
const path = require('path')
function createWindow () {
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的生命周期
1籽前、ready
Electron 完成初始化亭珍,這個(gè)是重點(diǎn)
敷钾。
Electron中很多都是需要Electron初始化完成后砾隅,才可以執(zhí)行(如menu铅鲤、窗口......)
為避免出現(xiàn)錯(cuò)誤,建議所有的操作都在ready之后
const { app } = require('electron')
app.on('ready', () => {
// create menu
// create browserWindow
})
可以通過app.isReady()
來檢查該事件是否已被觸發(fā)漩怎。
若希望通過Promise實(shí)現(xiàn)众羡,使用 app.whenReady()
侨赡。
2、第二個(gè)實(shí)例創(chuàng)建
當(dāng)運(yùn)行第二個(gè)實(shí)例時(shí)粱侣,聚焦到主窗口
gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
let win = mainWindow;
if (win) {
if (win.isMinimized()) {
win.restore();
}
if (!win.isFocused()) {
win.show();
}
win.focus();
}
});
}
3羊壹、當(dāng)應(yīng)用被激活時(shí)(macos)
app.on('activate', (event, webContents, details) => {
// 聚焦到最近使用的窗口
});
觸發(fā)此事件的情況很多:
首次啟動(dòng)應(yīng)用程序、嘗試在應(yīng)用程序已運(yùn)行時(shí)或單擊應(yīng)用程序的塢站
或任務(wù)欄圖標(biāo)
時(shí)重新激活它齐婴。
4油猫、當(dāng)所有窗口關(guān)閉,退出應(yīng)用
const { app } = require('electron')
app.on('window-all-closed', () => {
app.quit()
})
5柠偶、渲染進(jìn)程進(jìn)程奔潰
app.on('render-process-gone', (event, webContents, details) => {
// 定位原因情妖,可通過log記錄;也可以做重啟retry操作诱担,切記限制count
// webContents.reloadIgnoringCache(); // 忽略緩存強(qiáng)制刷新頁面
});
details
Object
-
reason
string - 渲染進(jìn)程消失的原因毡证。 可選值:-
clean-exit
- 以零為退出代碼退出的進(jìn)程 -
abnormal-exit
- 以非零退出代碼退出的進(jìn)程 -
killed
- 進(jìn)程發(fā)送一個(gè)SIGTERM,否則是被外部殺死的蔫仙。 -
crashed
- 進(jìn)程崩潰 -
oom
- 進(jìn)程內(nèi)存不足 -
launch-failed
- 進(jìn)程從未成功啟動(dòng) -
integrity-failure
- 窗口代碼完整性檢查失敗
-
exitCode
Integer - 進(jìn)程的退出代碼情竹,除非在reason
是launch-failed
的情況下,exitCode
將是一個(gè)平臺(tái)特定的啟動(dòng)失敗錯(cuò)誤代碼匀哄。
6秦效、應(yīng)用退出
app.on('will-quit', (event, webContents, details) => {
// timer 或者 關(guān)閉第三方的進(jìn)程
});
app常用方法
const { app } = require('electron')
app.relaunch({ args: process.argv.slice(1).concat(['--relaunch']) })
app.exit(0)
1、重啟應(yīng)用
app.relaunch([options])
relaunch被多次調(diào)用時(shí),多個(gè)實(shí)例將會(huì)在當(dāng)前實(shí)例退出后啟動(dòng)涎嚼。
2阱州、所有窗口關(guān)閉
app.exit(0)
所有窗口都將立即被關(guān)閉,而不詢問用戶
3法梯、當(dāng)前應(yīng)用程序目錄
app.getAppPath()
可以理解成獲取應(yīng)用啟動(dòng)(代碼)目錄
4苔货、設(shè)置 "關(guān)于" 面板選項(xiàng)
app.setAboutPanelOptions({
applicationName: 'demo',
applicationVersion: '0.0.1',
version: '0.0.1'
});
5、注意立哑,部分事件存在著系統(tǒng)的區(qū)別
// 只適用于macOS
app.hide() // 隱藏所有的應(yīng)用窗口夜惭,不是最小化.
BrowserWindow
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ width: 800, height: 600 })
// 加載地址
win.loadURL('https://github.com')
// 加載文件
win.loadFile('index.html')
自定義窗口
可以通過自定窗口options,實(shí)現(xiàn)自定義樣式铛绰,如桌面窗口頁面诈茧、桌面通知、模態(tài)框捂掰。敢会。曾沈。
const options = {
width: 800,
height: 600
}
new BrowserWindow(options)
父子窗口
拖動(dòng)父窗口,子窗口跟隨父窗口而動(dòng)鸥昏。
const top = new BrowserWindow()
const child = new BrowserWindow({ parent: top })
注意:child
窗口將總是顯示在 top
窗口的頂部塞俱。
win.setParentWindow(parent)
給窗口設(shè)置父窗口,null取消父窗口
win.getParentWindow()
獲取窗口的父窗口
win.getChildWindows()
獲取所有子窗口
顯示與隱藏
窗口可以直接展示也可以延后展示吏垮。但每次執(zhí)行show
障涯,都會(huì)將當(dāng)前桌面
的焦點(diǎn)聚焦到執(zhí)行show的窗口上。
const win = new BrowserWindow({
show: true,
})
在加載頁面時(shí)膳汪,渲染進(jìn)程第一次完成繪制時(shí)唯蝶,如果窗口還沒有被顯示,渲染進(jìn)程會(huì)發(fā)出 ready-to-show
事件旅敷。在此事件后顯示窗口將沒有視覺閃爍:
const win = new BrowserWindow({ show: false })
win.once('ready-to-show', () => {
win.show()
})
win.hide()
窗口隱藏生棍,焦點(diǎn)消失
win.show()
窗口顯示,獲取焦點(diǎn)
win.showInactive()
窗口顯示媳谁,但不聚焦于窗口(多用于當(dāng)其他窗口需要顯示時(shí)涂滴,但是不想中斷當(dāng)前窗口的操作)
win.isVisible()
判斷窗口是否顯示
Bounds
1、設(shè)置窗口的size晴音、position
setBounds(bounds[, animate])
:同時(shí)設(shè)置size柔纵、position,但是同時(shí)也會(huì)重置窗口锤躁。
setSize(width, height[, animate])
: 調(diào)整窗口的寬度和高度搁料。
setPosition(x, y[, animate])
:將窗口移動(dòng)到x 和 y。
注意:animate只在macOS上才會(huì)生效系羞。
// 設(shè)置 bounds 邊界屬性
win.setBounds({ x: 440, y: 225, width: 800, height: 600 })
// 設(shè)置單一 bounds 邊界屬性
win.setBounds({ width: 100 })
win.setSize(800, 600)
win.setPosition(440, 225)
2郭计、獲取窗口size、position
getBounds()
獲取窗口的邊界信息椒振。
getSize()
獲取窗口的寬度和高度昭伸。
getPosition()
返回一個(gè)包含當(dāng)前窗口位置的數(shù)組
center()
將窗口移動(dòng)到屏幕中央(常用)。
3澎迎、常見問題
win.setSize
如果 width 或 height 低于任何設(shè)定的最小尺寸約束庐杨,窗口將對齊到約束的最小尺寸。
win.setPosition
有的電腦機(jī)型存在兼容問題夹供,執(zhí)行一次win.setPosition(x,y)不會(huì)生效灵份,需要執(zhí)行兩次。
Application
應(yīng)用包含了很多程序工具哮洽,如Menu填渠、Tray...
Menu
創(chuàng)建原生應(yīng)用菜單和上下文菜單。
在mac中,菜單展示在應(yīng)用內(nèi)揭蜒;而windows與linux横浑,菜單則會(huì)展示在各個(gè)窗口的頂部剔桨。
可以對某一窗口單獨(dú)設(shè)置或者刪除Menu屉更,但是這只是針對windows、linux生效洒缀。
win.setMenu(menu)
設(shè)置為窗口的菜單欄menu(只對windows瑰谜、linux生效)。
win.removeMenu()
刪除窗口的菜單欄(只對windows树绩、linux生效)萨脑。
如何全局設(shè)置Menu
const { Menu } = require('electron')
const template = [
{
label: 'Electron',
submenu: [
{ role: 'about', label: '關(guān)于' },
{ type: 'separator' },
{ role: 'services', label: '偏好設(shè)置' },
{ type: 'separator' },
{ role: 'hide', label: '隱藏' },
{ role: 'hideOthers', label: '隱藏其他' },
{ type: 'separator' },
{ role: 'quit', label: '退出' }
]
},
{
label: '編輯',
submenu: [
{ role: 'undo', label: '撤銷' },
{ type: 'separator' },
{ role: 'menu_copy', label: '復(fù)制' },
{ role: 'menu_paste', label: '粘貼' }
]
},
{
label: '窗口',
submenu: [
{
role: 'minimize',
label: '最小化',
click: function (event, focusedWindow, focusedWebContents) {}
},
{ role: 'close', label: '關(guān)閉' },
{ role: 'togglefullscreen', label: '全屏', accelerator: 'Cmd+,OrCtrl+,'}
]
}];
let menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
role:可以理解為官方命名好的指令,詳見官方menuitemrole
label:我們對指令自定義的展示文字饺饭。
click:觸發(fā)該指令的接受的函數(shù)
Tray
添加圖標(biāo)和上下文菜單到系統(tǒng)通知區(qū)
const {ipcMain, app, Menu, Tray} = require('electron')
const iconPath = path.join(__dirname, './iconTemplate.png')
const tray = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([{
label: 'tray 1',
click: () => {}
}, {
label: 'tray 2',
click: () => {}
}])
tray.setToolTip('Electron Demo in the tray.')
tray.setContextMenu(contextMenu)
setToolTip
設(shè)置鼠標(biāo)指針在托盤圖標(biāo)上懸停時(shí)顯示的文本
setContextMenu
設(shè)置圖標(biāo)的內(nèi)容菜單(支持動(dòng)態(tài)添加多個(gè)內(nèi)容)
dialog
系統(tǒng)對話框
1.支持多選渤早、默認(rèn)路徑
const { dialog } = require('electron')
const options = {
title: '標(biāo)題',
defaultPath: '默認(rèn)地址',
properties: [
openFile, // 容許選擇文件
openDirectory, // 容許選擇目錄
multiSelections, // 容許多選
showHiddenFiles, // 顯示隱藏文件
createDirectory瘫俊, // 創(chuàng)建新的文件鹊杖,只在mac生效
promptToCreate,// 文件目錄不存在扛芽,生成新文件夾骂蓖,只在windows生效
]
}
dialog.showOpenDialog(win, options); // win是窗口
2.支持過濾文件
過濾文件后綴名gif的文件,顯示所有文件用 * 代替
options.filters = [
{ name: 'Images', extensions: ['gif'] }
]
globalShortcut
鍵盤事件
需要先注冊globalShortcut.register(accelerator, callback)
const { globalShortcut } = require('electron')
globalShortcut.register('CommandOrControl+F', () => {
// 注冊鍵盤事件是全局性質(zhì)的川尖,各個(gè)窗口都可以觸發(fā)
})
globalShortcut.register(accelerator, callback)
注冊全局快捷鍵
globalShortcut.isRegistered(accelerator)
判斷是否注冊
globalShortcut.unregister(accelerator)
取消注冊
注意:應(yīng)用程序退出時(shí)登下,注銷鍵盤事件
app.on('will-quit', () => {
globalShortcut.unregisterAll()
})
Notification
系統(tǒng)通知
這個(gè)受限于當(dāng)前系統(tǒng)是否支持
桌面通知,在mac或windows電腦的設(shè)置中叮喳,需特別注意是否容許通知被芳。
const { Notification } = require('electron');
const isAllowed = Notification.isSupported();
if (isAllowed) {
const options = {
title: '標(biāo)題',
body: '正文文本,顯示在標(biāo)題下方',
silent: true, // 系統(tǒng)默認(rèn)的通知聲音
icon: '', // 通知圖標(biāo)
}
const notification = new Notification(argConig);
notification.on('click', () => { });
notification.on('show', () => { });
notification.on('close', () => { });
notification.show();
}
notification.close()
關(guān)閉通知
session
管理瀏覽器會(huì)話馍悟、cookie畔濒、緩存、代理設(shè)置等赋朦。
1篓冲、全局
const { session, BrowserWindow } = require('electron')
// 攔截下載
session.defaultSession.on('will-download', (event, item, webContents) => {
event.preventDefault() // 阻止默認(rèn)行為下載或做指定目錄下載
})
2、單獨(dú)窗口
const win = new BrowserWindow({ width: 800, height: 600 })
win.loadURL('http://github.com')
const ses = win.webContents.session
ses.setProxy(config)
:設(shè)置代理
ses.cookies
:設(shè)置cookies或獲取cookies
3宠哄、瀏覽器UserAgent
設(shè)置UserAgent可以通過app.userAgentFallback全局設(shè)置壹将,也可以通過ses.setUserAgent
設(shè)置。
screen
檢索有關(guān)屏幕大小毛嫉、顯示器诽俯、光標(biāo)位置等的信息。
const primaryDisplay = screen.getPrimaryDisplay() // 獲取光標(biāo)所在屏幕的屏幕信息
const { width, height } = primaryDisplay.workAreaSize // 獲取光標(biāo)下的屏幕尺寸
const allDisplay = screen.getAllDisplays() // 返回?cái)?shù)組,所有的屏幕
screen.getPrimaryDisplay()
返回主窗口Display
screen.getAllDisplays()
返回所有的窗口Display[]數(shù)組
screen.getDisplayNearestPoint
離光標(biāo)最近的窗口
Node
項(xiàng)目中會(huì)用到Node.js暴区,下面是我整理的常用方法闯团。
fs
本地文件讀寫
讀取目錄的內(nèi)容
fs.readdirSync(path[, options])
fs.readdir(path[, options])
讀取文件的內(nèi)容
fs.readFileSync(path[, options])
fs.readFile(path[, options])
文件的信息
const stats = fs.statSync(path);
stats.isDirectory() // 是否為系統(tǒng)目錄
stats.isFile() // 是否為文件
stats.size // 文件大小
。仙粱。房交。
路徑是否存在
fs.existsSync(path)
寫入文件
當(dāng) file
是文件名時(shí),將數(shù)據(jù)寫入文件伐割,如果文件已存在則替換該文件候味。
fs.writeFile(file, data[, options], callback)
fs.writeFileSync(file, data[, options], callback)
移除文件
fs.rmSync(path[, options])
fs.rmdirSync(path[, options])
更改文件的權(quán)限
fs.chmod(path, mode, callback)
fs.chmodSync(path, mode, callback)
注意:mode是8進(jìn)制,可通過parseInt(mode, 8)轉(zhuǎn)化
修改文件夾名
fs.renameSync
拷貝文件到指定地址
fs.copySync
path
拼接絕對路徑
path.resolve([...paths])
拼接路徑
path.join([...paths])
路徑文件夾名字
path.dirname(path)
獲取文件名
path.basename(path)
都是屬于Nodejs隔心,整理到最后懶得整理了~~白群,小伙伴們想研究的話,具體看Node.js
存在的問題
Electron還是會(huì)存在部分坑~~