如何使用Electron構(gòu)建桌面端應(yīng)用

本文介紹如何構(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 文件使碾。
webPreferencesBrowserWindow的屬性蜜徽,用來(lái)設(shè)置網(wǎng)頁(yè)功能。preloadwebPreferences屬性的參數(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)程
  1. 在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,
  });
};
  1. 在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)程
  1. 在文件生成成功后欧漱,使用當(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 })
})
  1. 在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


    80285313-F89E-4FE0-8459-EAA643012081.png
  • windows


    FC5639BA-90CA-48EC-B31A-42A2063E9283.png

打開(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打包失敗

9B96BB0C-E3B0-491B-AA8A-9DD08DE127DA.png

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/
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末此叠,一起剝皮案震驚了整個(gè)濱河市纯续,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌灭袁,老刑警劉巖猬错,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異茸歧,居然都是意外死亡倦炒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)软瞎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)逢唤,“玉大人,你說(shuō)我怎么就攤上這事涤浇≈遣#” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵芙代,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我盖彭,道長(zhǎng)纹烹,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任召边,我火速辦了婚禮铺呵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隧熙。我一直安慰自己片挂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布贞盯。 她就那樣靜靜地躺著音念,像睡著了一般。 火紅的嫁衣襯著肌膚如雪躏敢。 梳的紋絲不亂的頭發(fā)上闷愤,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音件余,去河邊找鬼讥脐。 笑死遭居,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旬渠。 我是一名探鬼主播俱萍,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼告丢!你這毒婦竟也來(lái)了枪蘑?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芋齿,失蹤者是張志新(化名)和其女友劉穎腥寇,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體觅捆,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赦役,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了栅炒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掂摔。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖赢赊,靈堂內(nèi)的尸體忽然破棺而出乙漓,到底是詐尸還是另有隱情,我是刑警寧澤释移,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布叭披,位于F島的核電站,受9級(jí)特大地震影響玩讳,放射性物質(zhì)發(fā)生泄漏涩蜘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一熏纯、第九天 我趴在偏房一處隱蔽的房頂上張望同诫。 院中可真熱鬧,春花似錦樟澜、人聲如沸误窖。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)霹俺。三九已至,卻和暖如春毒费,著一層夾襖步出監(jiān)牢的瞬間吭服,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工蝗罗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留艇棕,地道東北人蝌戒。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像沼琉,于是被迫代替她去往敵國(guó)和親北苟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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