從0到1構(gòu)建跨平臺Electron應(yīng)用,這篇文章就夠了

簡介

Electron 基于 Chromium 和 Node.js, 讓你可以使用 HTML, CSS 和 JavaScript 構(gòu)建桌面端應(yīng)用。

這篇文章摘錄了我自己在真實(shí)項(xiàng)目中的要點(diǎn)問題。

可能有的小伙伴會問,為什么不說一下與Vue、React或者其他的庫如何結(jié)合開發(fā)?

這是因?yàn)閂ue或者React以及其他庫都是UI頁面,可以單獨(dú)獨(dú)立開發(fā),只需要在代碼打包壓縮后,使用BrowserWindow中的loadURL或者loadFile加載即可。

剛開始學(xué)習(xí)Electron的小伙伴可以先看 Electron快速入手,擁有自己的第一個桌面應(yīng)用

Electron核心

Electron實(shí)現(xiàn)跨平臺桌面應(yīng)用程序的原理

image.png

通過集成瀏覽器內(nèi)核,使用前端的技術(shù)來實(shí)現(xiàn)不同平臺下的渲染,并結(jié)合了 Chromium 、Node.js 和用于調(diào)用系統(tǒng)本地功能的 API 三大板塊。

  1. Chromium 提供強(qiáng)大的 UI 渲染能力,用于顯示網(wǎng)頁內(nèi)容。
  2. Node.js 用于本地文件系統(tǒng)和操作系統(tǒng),提供GUI 的操作能力(如path、fs、crypto 等模塊)。
  3. 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個方面。

進(jìn)程間通信:處理主進(jìn)程與渲染進(jìn)程的雙向通信。

app生命周期:貫穿桌面端應(yīng)用整個生命周期(用的不多,但是很重要)。

BrowserWindow:窗口(大家可以理解成每一個BWindow都是一個獨(dú)立的瀏覽器,用于加載渲染我們的前端代碼)。

Application:應(yīng)用程序功能點(diǎn)(為應(yīng)用提供更佳完善的功能點(diǎn))。

從這4個主要核心點(diǎn)入手,開發(fā)人員會更快進(jìn)入開發(fā)狀態(tài)。

進(jìn)程間通信

ipcMain與ipcRenderer

electron存在多個進(jìn)程,那么多進(jìn)程間如何實(shí)現(xiàn)通信,electron官方提供了方法。

image.png

注意:不建議使用同步通信,這會對應(yīng)用整體性能產(chǎn)生影響。

注意:進(jìn)程通信所傳參數(shù)只能為原始類型數(shù)據(jù)和可被 JSON.stringify 的引用類型數(shù)據(jù)。故不建議使用JSON.stringify。

異步

通過event.reply(...)將異步消息發(fā)送回發(fā)送者。

event.reply會自動處理從非主 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])移除多個指定 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 完成初始化,這個是重點(diǎn)。

Electron中很多都是需要Electron初始化完成后,才可以執(zhí)行(如menu、窗口......)

為避免出現(xiàn)錯誤,建議所有的操作都在ready之后

const { app } = require('electron')
app.on('ready', () => {
    // create menu
    // create browserWindow
})

可以通過app.isReady() 來檢查該事件是否已被觸發(fā)。

若希望通過Promise實(shí)現(xiàn),使用 app.whenReady() 。

2、第二個實(shí)例創(chuàng)建

當(dāng)運(yùn)行第二個實(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)用被激活時(macos)

app.on('activate', (event, webContents, details) => {
    // 聚焦到最近使用的窗口
});

觸發(fā)此事件的情況很多:
首次啟動應(yīng)用程序、嘗試在應(yīng)用程序已運(yùn)行時或單擊應(yīng)用程序的塢站任務(wù)欄圖標(biāo)時重新激活它。

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ā)送一個SIGTERM,否則是被外部殺死的。
    • crashed - 進(jìn)程崩潰
    • oom - 進(jìn)程內(nèi)存不足
    • launch-failed - 進(jìn)程從未成功啟動
    • integrity-failure - 窗口代碼完整性檢查失敗
  • exitCode Integer - 進(jìn)程的退出代碼,除非在 reasonlaunch-failed 的情況下, exitCode 將是一個平臺特定的啟動失敗錯誤代碼。

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í)例將會在當(dāng)前實(shí)例退出后啟動。

2、所有窗口關(guān)閉

app.exit(0)

所有窗口都將立即被關(guān)閉,而不詢問用戶

3、當(dāng)前應(yīng)用程序目錄

app.getAppPath()

可以理解成獲取應(yīng)用啟動(代碼)目錄

4、設(shè)置 "關(guān)于" 面板選項(xiàng)

image.png
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)

父子窗口

拖動父窗口,子窗口跟隨父窗口而動。

const top = new BrowserWindow()
const child = new BrowserWindow({ parent: top })

注意:child 窗口將總是顯示在 top 窗口的頂部。

win.setParentWindow(parent)給窗口設(shè)置父窗口,null取消父窗口
win.getParentWindow()獲取窗口的父窗口
win.getChildWindows()獲取所有子窗口

顯示與隱藏

窗口可以直接展示也可以延后展示。但每次執(zhí)行show,都會將當(dāng)前桌面的焦點(diǎn)聚焦到執(zhí)行show的窗口上。

const win = new BrowserWindow({
    show: true, 
})

在加載頁面時,渲染進(jìn)程第一次完成繪制時,如果窗口還沒有被顯示,渲染進(jìn)程會發(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)其他窗口需要顯示時,但是不想中斷當(dāng)前窗口的操作)

win.isVisible()判斷窗口是否顯示

Bounds

1、設(shè)置窗口的size、position

setBounds(bounds[, animate]):同時設(shè)置size、position,但是同時也會重置窗口。

setSize(width, height[, animate]): 調(diào)整窗口的寬度和高度。

setPosition(x, y[, animate]):將窗口移動到x 和 y。

注意:animate只在macOS上才會生效。

// 設(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()返回一個包含當(dāng)前窗口位置的數(shù)組

center()將窗口移動到屏幕中央(常用)。

3、常見問題

win.setSize如果 width 或 height 低于任何設(shè)定的最小尺寸約束,窗口將對齊到約束的最小尺寸。

win.setPosition有的電腦機(jī)型存在兼容問題,執(zhí)行一次win.setPosition(x,y)不會生效,需要執(zhí)行兩次。

Application

應(yīng)用包含了很多程序工具,如Menu、Tray...

Menu

創(chuàng)建原生應(yīng)用菜單和上下文菜單。

image.png

在mac中,菜單展示在應(yīng)用內(nèi);而windows與linux,菜單則會展示在各個窗口的頂部。

可以對某一窗口單獨(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ū)


image.png
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)上懸停時顯示的文本

setContextMenu設(shè)置圖標(biāo)的內(nèi)容菜單(支持動態(tài)添加多個內(nèi)容)

image.png

dialog

系統(tǒng)對話框

image.png

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ì)的,各個窗口都可以觸發(fā)
}) 

globalShortcut.register(accelerator, callback)注冊全局快捷鍵
globalShortcut.isRegistered(accelerator)判斷是否注冊
globalShortcut.unregister(accelerator)取消注冊

注意:應(yīng)用程序退出時,注銷鍵盤事件

app.on('will-quit', () => {
    globalShortcut.unregisterAll()
})

Notification

系統(tǒng)通知

這個受限于當(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

管理瀏覽器會話、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() // 返回數(shù)組,所有的屏幕

screen.getPrimaryDisplay()返回主窗口Display

screen.getAllDisplays()返回所有的窗口Display[]數(shù)組

screen.getDisplayNearestPoint離光標(biāo)最近的窗口

Node

項(xiàng)目中會用到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ù)據(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還是會存在部分坑~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內(nèi)容