使用Electron構建跨平臺的桌面應用

蘇寧影片雲發表於2022-12-05

作者:李曉健。蘇寧影片雲前端部門經理。擁有7年前端業務研發和架構經驗,目前負責蘇寧雲影片前端研發和架構工作。

Electron簡介

Electron 是一個使用 JavaScript,HTML 和 CSS 等 Web 技術建立原生桌面程式的框架。官網是

Electron就是使用Web技術來開發,然後打包成桌面客戶端的一個框架。它渲染Web內容使用的是Chromium,Chromium基本上和大家常用的chrome瀏覽器差不多,並且Electron在打包時是將Chromium嵌入到了應用裡,所以它並不依賴使用者機器上安裝的瀏覽器,大家都知道做Web開發,最大的一個問題就是相容性的問題,經常相同的程式碼在不同作業系統中不同的瀏覽器裡,甚至不同版本的瀏覽器裡都可能會出現不同的效果。Electron相當於是把一個固定版本的瀏覽器嵌入到應用中,所以他就不存在相容性問題,只要我們開發時寫的程式碼可以正常執行,就不存在打包後在使用者的機器上因為相容性問題無法使用的情況。

Electron支援Node.js的原生模組,也就是說可以在Electron應用中直接使用Node.js的模組。Web應用為了安全等一系列的問題考慮,限制了很多的功能,比如本地檔案的讀寫、作業系統中硬體裝置的呼叫等等,但是Node.js並沒有這些限制。也就是說在Electron應用中,我們不光可以使用Web技術的特點,還能夠突破Web技術的限制,使用Node.js的模組功能,使應用功能更加豐富。

Electron開發出來的應用是跨平臺的,當應用開發完成後,我們可以將同一套程式碼打包成Linux、macOS、Windows平臺上的可執行程式或安裝包。

Electron中比較重要的概念

學習任何一門框架,裡面或多或少都會有一些新的概念,只有把這些新的概念都弄懂了,才能更好的掌握和運用這門框架,Electron中新的概念並不是很多,接下來我就就來說說最重要的3個概念。

1. 主程式:Electron應用中執行package.json的main指令碼的程式被稱為主程式,在主程式中執行的指令碼透過建立web頁面來展示使用者介面。一個 Electron 應用總是有且只有一個主程式。

2. 渲染程式:Electron應用中是透過在主程式裡建立web頁面來展示使用者介面的,在主程式中可以透過BrowserWindow 例項建立頁面,這裡的一個web頁面就是執行在一個渲染程式裡的,一個Electron應用可以包含多個web頁面,所以一個Electron 應用中是可以有多個渲染程式的,當一個 BrowserWindow 例項被銷燬後,相應的渲染程式也會被終止。

3. IPC:既然有主程式和渲染程式,而主程式管控的是一個應用全域性的設定和方法,而渲染程式只在一個web頁面中存在,那他們肯定不會始終完全孤立的存在,有時也是需要有通訊的,比如我在頁面做了一個操作,整個應用需要去做一個響應,就是就需要用到主程式和渲染程式的通訊。就裡就有一個概念IPC(Inter-Proess Communication),程式中通訊。IPC就提供了相應的方法來供主程式和渲染程式間進行通訊。

所以他們大概的關係就是一個應用中有一個主程式,然後主程式可以建立多個渲染程式,主程式和渲染程式間透過IPC來通訊。如下圖:

使用Electron構建跨平臺的桌面應用

Electron的安裝

Electron是以Node.js中的模組的方式進行釋出的,所以他的安裝也非常簡單,透過npm來進行安裝就可以了。既然Electron是透過Node.js的模組起先發布的,那它的安裝就需要依賴Node.js的環境,也就是需要在我們的電腦上安裝Node.js,安裝Node.js直接去其官網( )下載相應的安裝包,然後按照預設的設定一路安裝就行了,在安裝Node.js的同時也會安裝上npm,安裝完成後檢測Node.js和npm是否安裝成功,只需要在命令列工具攔執行相應的版本檢視命令就行了,如果能看到有版本號輸出,則說明安裝成功。如下圖:

使用Electron構建跨平臺的桌面應用

接下來我們就可以直接使用npm來安裝我們所需要的模組了。首先進入到我們的專案開發目錄,然後初始化好專案(建立好package.json檔案),然後在該目錄執行

npm install electron --save

然後等待安裝完成就行了。

在這個安裝過程中,可能由於網路問題會安裝失敗,我們可以修改npm的映象源,將npm的映象源修改成國內的映象源:在命令列執行 npm config set registry 或者安裝cnpm,然後透過cnpm來安裝Node.js的模組。

安裝完成後,在命令列執行

electron -v

可以看到electron的版本號,就說明Electron安裝成功了。

Electron專案的搭建

Electron安裝好就,就可以搭建專案了。官網給我們提供了一個空的種子專案,這個專案裡面非常的乾淨,所以可以直接使用,首先我們透過命令列進入我們工作目錄,克隆下這個專案(前提是我們的電腦需要安裝git),執行

git clone

克隆完成後,我們的工作目錄中就多了一個 electron-quick-start 目錄,這個目錄就是

我們的專案目錄,這裡面有幾個檔案是我們專案需要用到的檔案

1. package.json:專案的一些依賴和配置檔案,裡面有一些我們專案的基本資訊,我們可以根據自己專案的情況來修改這裡的值 bash

{
  "name": "electron-quick-start", //專案的名稱
  "version": "1.0.0", //專案的版本號
  "description": "A minimal Electron application", //專案的描述
  "main": "main.js", //主入口檔案
  "scripts": { //快捷指令碼
    "start": "electron ."
  },
  "repository": "程式碼倉庫
  "keywords": [ //專案的關鍵詞
    "Electron",
    "quick",
    "start",
    "tutorial",
    "demo"
  ],
  "author": "GitHub", //專案的作者
  "license": "CC0-1.0", //專案的許可協議
  "devDependencies": { //專案的依賴
    "electron": "^2.0.0"
  }
}

2. main.js:這個檔案是專案的啟動檔案,也是專案的入口檔案,該檔案就是執行在主程式中的。

// 引入electron中的API

const {app, BrowserWindow} = require('electron')


//定義一個web視窗 這個視窗就執行在一個一個渲染程式中

let mainWindow


//建立web視窗的方法

function createWindow () {

  // 透過BrowserWindow來建立一個視窗

  mainWindow = new BrowserWindow({width: 800, height: 600})


  // 在web視窗中載入html頁面

  mainWindow.loadFile('index.html')


  // 監聽視窗關閉的事件

  mainWindow.on('closed', function () {

    mainWindow = null

  })

}


// 當應用準備好了  就呼叫建立web視窗的方法

app.on('ready', createWindow)


// 當所有的視窗都關閉後,就退出應用

app.on('window-all-closed', function () {

    //在非MAC平臺下退出應用

    //在MAC平臺下所有視窗關閉 應用並不需要退出,它還可以透過dock來重新啟用應用

  if (process.platform !== 'darwin') {

    app.quit()

  }

})


//當應用被啟用事件 

app.on('activate', function () {

  //如果視窗不存在 就建立一個視窗

  if (mainWindow === null) {

    createWindow()

  }

})


3. index.html:這是web視窗裡展示需要展示的內容,當然這裡也可以是一個遠端html的url


<!DOCTYPE html>

<html>

  <head>

    <meta charset="UTF-8">

    <title>Hello World!</title>

  </head>

  <body>

    <h1>Hello World!</h1>

    <!-- All of the Node.js APIs are available in this renderer process. -->

    We are using Node.js <script>document.write(process.versions.node)</script>,

    Chromium <script>document.write(process.versions.chrome)</script>,

    and Electron <script>document.write(process.versions.electron)</script>.


    <script>

      // You can also require other files to run in this process

      require('./renderer.js')

    </script>

  </body>

</html>


這裡就是一個普通的html,但是這裡面也有和普通html不同之處,我們可以看到html的script標籤裡有用到process.xxx.xxx;並且前面沒沒有定義process,這裡確可以直接訪問,html裡引入script用了require('./renderer.js')。這些寫法在我們平常的script裡都會報錯的,但是這裡確可以正常執行的,原因就是我們前面說過的Electron應用中可以直接使用Node.js的Api,這裡的process就是Node.js裡的變數,require就是Node.js中引入外部檔案的關鍵字。

renderer.js:這個檔案是被引入到index.html中的,所以它就是用來寫html中邏輯的檔案,所以這個檔案也是執行在渲染程式中的。

我們上面說到的4個檔案的檔名和路徑並不是固定的,也可以根據專案需要進行修改,他們的關係是main.js 就是 package.json中main屬性對應的值,如果需要修改main.js的名稱,這兩處需要同時修改。index.html 就是在main.js中 mainWindow.loadFile('index.html') 引入的檔案,如果需要修改這兩處也需要同時修改。renderer.js就是在index.html中require('./renderer.js') 引入的js檔案,當然這裡也可以引入多個js誰的,和我們普通的html一樣。

檔案作用和關係都理清楚了,我們就可以安裝依賴了。在命令列執行

npm install

等待安裝完成之後,就可以執行專案了,在命令列執行

npm run start

這裡的start 就是我們在package.json中scripts屬性中配置的值 "start": "electron .",我們在命令列執行 npmrun start 就相當於我們執行 npm run electron . ;因為有時有些命令比較長或帶了比較多的引數,所以我們在package.json中配置一些別名會大大減少我們在命令列的輸入。等命令執行完成我們就可以看到我們的桌面上開啟了一個客戶端,如下圖:

使用Electron構建跨平臺的桌面應用

這樣我們的一個初始專案就算搭建完成了。

Electron中常用的功能模組

electron不光是可以將我們的頁面打包成客戶端應用,它也為我們提供了大量的功能模組,方便我們的開發,下面我們就簡單介紹一下用的比較多的功能模組。

1. app: 控制應用程式的事件生命週期,用於監聽應用的的事件,該模組在主程式中。

2. BrowserWindow:建立和控制瀏覽器視窗,在建立視窗時可以傳入很多的引數,來控制視窗的樣式和行為該模組在主程式中。

3. ipcMain:從主程式到渲染程式的非同步通訊,該模組在主程式中。

4. ipcRenderer:從渲染程式到主程式的非同步通訊,該模組在渲染程式中。

5. remote:在渲染程式中引入主程式模組,該模組在渲染程式。

6. dialog:顯示用於開啟和儲存檔案、警報等的本機系統對話方塊,該模組在主程式中。

7. crashReporter:將崩潰日誌提交給遠端伺服器

8. globalShortcut:當應用程式沒有鍵盤焦點時監聽全域性鍵盤事件,該模組在主程式中。

9. Menu:建立原生應用選單和上下文選單,該模組在主程式中。

10. desktopCapturer:可以訪問那些用於從桌面上捕獲音訊和影片的媒體源資訊,該模組在渲染程式中。

11. net:使用Chromium的原生網路庫發出HTTP/ HTTPS請求,該模組在主程式中。

12. autoUpdater:使應用程式能夠自動更新,該模組在主程式中。

以上的這些模組都是比較常用的模組,所有的功能模組並不是在任何地方都可以呼叫,有些模組在主程式中可以直接引入,有些模組只能在渲染程式中引入,當然也有些模組可以直接主程式和渲染程式中引入使用。這些在主程式中的模組也是可以在渲染程式中引入的,不過不能直接引入,需要透過remote模組來間接引入,所以在主程式中的模組也是可以在渲染程式中使用,例如我們想在渲染程式中使用BrowserWindow:

//我們需要先引入remote模組

const {remote} = require('electron');

//再從remote模組中引入BrowserWindow

const {BrowserWindow} = remote

let win = new BrowserWindow({width: 800, height: 600})

win.loadURL(')


更多的功能模組和使用方法可以參考Electron的文件:docs

Electron的Demo

Electron的大概內容已經介紹的差不多了,接下來我們就使用Electron來做一個拍照儲存的小demo,能正常執行需要您的電腦上有攝像頭。我們的這個demo就基於前面搭建好的空專案來做。前面我們說過,專案的檔名和路徑並不是固定的,接下來我們就來調整一下前面那個專案的目錄結構,我們將我們需要寫程式碼都放到src目錄下,調整個目錄如下:

使用Electron構建跨平臺的桌面應用

我們將html程式碼放到view目錄下,將樣式檔案放到style目錄下,將圖片放入到images目錄下,將我們的js程式碼放到scripts目錄下,將main.js改名為index.js(這裡改檔案只是為了說明這個檔名是可以修改的,並不是固定的),然後直接放到src目錄下。

package.json 修改如下

{
  "name": "take-photo",
  "version": "0.0.1",
  "description": "A take photo Electron application",
  "main": "src/index.js",
  "scripts": {
    "start": "electron ."
  },
  "repository": "",
  "keywords": ["Electron","demo"],
  "author": "pptvyun",
  "license": "MIT",
  "devDependencies": {
    "electron": "^2.0.0"
  }
}

index.js 修改如下


// 引入electron中的API

const {app, BrowserWindow} = require('electron')


//定義一個web視窗 這個視窗就執行在一個一個渲染程式中

let mainWindow


//建立web視窗的方法

function createWindow () {

    // 透過BrowserWindow來建立一個視窗

    mainWindow = new BrowserWindow({width: 800, height: 600})


    // 在web視窗中載入html頁面

    mainWindow.loadFile(`${__dirname}/view/index.html`)


    // 監聽視窗關閉的事件

    mainWindow.on('closed', function () {

        mainWindow = null

    })

}


// 當應用準備好了  就呼叫建立web視窗的方法

app.on('ready', createWindow)


// 當所有的視窗都關閉後,就退出應用

app.on('window-all-closed', function () {

    //在非MAC平臺下退出應用

    //在MAC平臺下所有視窗關閉 應用並不需要退出,它還可以透過dock來重新啟用應用

    if (process.platform !== 'darwin') {

        app.quit()

    }

})


//當應用被啟用事件

app.on('activate', function () {

    //如果視窗不存在 就建立一個視窗

    if (mainWindow === null) {

        createWindow()

    }

})


index.html 修改如下


<!DOCTYPE html>

<html>

  <head>

    <meta charset="UTF-8">

    <title>Hello World!</title>

  </head>

  <body>

    <h1>Hello World!</h1>

    <!-- All of the Node.js APIs are available in this renderer process. -->

    We are using Node.js <script>document.write(process.versions.node)</script>,

    Chromium <script>document.write(process.versions.chrome)</script>,

    and Electron <script>document.write(process.versions.electron)</script>.


    <script>

       //這裡修改了renderer.js的引入路徑

      require('../scripts/renderer.js')

    </script>

  </body>

</html>


接下來我們就根據我們的業務來寫具體的程式碼,首先我們的程式碼肯定是需要除錯的,Electron使用的是Chromium來做渲染的,所以它也給我們提供了chrome的開發者工具供我們使用。開發完成後的程式碼如下


<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title>take photo</title>

    <link rel="stylesheet" href="../style/index.css">

</head>

<body>

<header>拍照儲存</header>

<div class="content">

    <video src="" id="video"></video>

    <canvas width="250" height="187" id="canvas"></canvas>

    <div class="button-box">

        <a href="javascript:;" id="openCamera" class="button">開啟攝像頭</a>

        <a href="javascript:;" id="takePhoto" class="button">拍攝照片</a>

        <a href="javascript:;" id="savePhoto" class="button">儲存照片</a>

    </div>

</div>


<script>

    require('../scripts/renderer.js')

</script>

</body>

</html>


src/style/index.css

使用Electron構建跨平臺的桌面應用

使用Electron構建跨平臺的桌面應用

src/index.js


// 引入electron中的API

const {app, BrowserWindow,globalShortcut} = require('electron')


//定義一個web視窗 這個視窗就執行在一個一個渲染程式中

let mainWindow


//建立web視窗的方法

function createWindow () {

    // 透過BrowserWindow來建立一個視窗

    mainWindow = new BrowserWindow({width: 800, height: 600})


    // 在web視窗中載入html頁面

    mainWindow.loadFile(`${__dirname}/view/index.html`)


    // 監聽視窗關閉的事件

    mainWindow.on('closed', function () {

        mainWindow = null

    })

    //這裡我們註冊一個開啟 開發者工具有快捷鍵 win下是Ctrl+F12 macOS下是command+f12

    globalShortcut.register('CommandOrControl+F12', () => {

        mainWindow.webContents.openDevTools()

    })

}


// 當應用準備好了  就呼叫建立web視窗的方法

app.on('ready', createWindow)


// 當所有的視窗都關閉後,就退出應用

app.on('window-all-closed', function () {

    //在非MAC平臺下退出應用

    //在MAC平臺下所有視窗關閉 應用並不需要退出,它還可以透過dock來重新啟用應用

    if (process.platform !== 'darwin') {

        app.quit()

    }

})


//當應用被啟用事件

app.on('activate', function () {

    //如果視窗不存在 就建立一個視窗

    if (mainWindow === null) {

        createWindow()

    }

})


src/sctipts/renderer.js



//引入Electron中的dialog 用來顯示訊息提示

const {dialog} = require('electron').remote

//引入Node.js的fs模組  用來寫檔案

const fs = require('fs');

//頁面開啟攝像頭的按鈕

const button = document.querySelector('#openCamera');

//頁面拍攝照片的按鈕

const takephoto = document.querySelector('#takePhoto');

//頁面儲存照片的按鈕

const saveImage = document.querySelector('#savePhoto');

//頁面的video標籤,用來展示攝像頭拍攝的畫面

const video = document.querySelector('#video');

//頁面的canvas標籤,用來展示拍攝的照片

const canvas = document.querySelector('#canvas');

const ctx = canvas.getContext('2d');


//獲取攝像頭的引數

const mediaConfig = {

    video: true

}

//點選開啟攝像頭的按鈕

button.addEventListener('click',e=>{

    //呼叫電腦的攝像頭

    navigator.mediaDevices.getUserMedia(mediaConfig)

        .then(function(stream) {

            //將攝像頭拍攝的內容給到video標籤,並從video標籤上播放

            video.srcObject = stream;

            video.onloadedmetadata = function(e) {

                video.play();

            };

        })

        .catch(function(err) {

            alert('開啟挺像頭失敗')

        });

})


//點選拍照按鈕

takephoto.addEventListener('click',e=>{

    //將video上的畫面畫到canvas上

    ctx.drawImage(video,0,0,250,187);

})

//點選儲存圖片按鈕

saveImage.addEventListener('click',e=>{

    //選擇圖片的儲存路徑

    dialog.showOpenDialog({properties:['openDirectory']},filePaths=>{

        if(filePaths && filePaths[0]){

            //儲存圖片的目錄

            const floder = filePaths[0]+'/';

            //獎canvas上的圖片轉成base64編碼

            let dataURL = canvas.toDataURL('image/png');

            var regex = /^data:.+\/(.+);base64,(.*)$/;

            if(dataURL){

                var matches = dataURL.match(regex);

                var buffer = new Buffer(matches[2], 'base64');

                //將檔案流寫入選擇的目錄裡

                const file = floder+Date.now()+'.' + matches[1];

                fs.writeFile(file, buffer,err=>{

                    if(err){

                        dialog.showErrorBox("錯誤提示", '圖片儲存失敗')

                    }else{

                        //圖片儲存成功的提示

                        dialog.showMessageBox({

                            type:'info',

                            title:'提示資訊',

                            message:'圖片儲存成功,檔案儲存在'+file,

                        })

                    }

                });

            }

        }

    })

})


程式碼開發完成後,執行專案,在命令列輸入

npm run start

就可以看到專案結果了,頁面會出現3個按鈕,點選開啟攝頭,客戶端上就會出現攝像頭拍攝的內容,然後點選儲存照片按鈕,客戶端就會出現一張靜態的圖片,然後點選儲存按鈕,這張圖片就會儲存在我們選擇的目錄下。效果如下圖:

使用Electron構建跨平臺的桌面應用


Electron專案的構建

這個demo開發到這裡就算完成了,但是我們的客戶端還是需要給別人來使用,總不能讓別人把我們的程式碼拷過去,然後安裝依賴,在命令列執行npm run start吧,我們常見的客戶端都是有一個安裝程式,安裝之後就可以直接雙擊執行的,或者是一個綠色版,有一個可執行程式,我們雙擊就可以執行的。接下來我們就也把我們的demo打包成一個安裝程式,可供使用者安裝使用。

將Electron打包成安裝程式有多種方式,今天我們說的是透過 electron-builder 模組來打包,electron-builder也是一個Node.js的模組,所以我們可以直接透過npm來安裝,在命令列執行

npm install electron-builder --save-dev

等待安裝完成就可以了,它的文件在 ,它可以打包出window、macOS、Linux上的可執行程式。然後我們參照文件修改配置檔案,這裡只需要修改package.json就好。修改後如下

{
  "name": "take-photo",
  "version": "0.0.1",
  "description": "A take photo Electron application",
  "main": "src/index.js",
  "scripts": {
    "start": "electron .",
    "build-all": "electron-builder -mwl",
    "build-win": "electron-builder --win --x64",
    "build-mac": "electron-builder --mac --x64",
    "build-linux": "electron-builder --linux --x64"
  },
  "build": {
    "appId": "com.pptvyun.test",
    "copyright": "pptvyun",
    "productName": "demo",
    "mac": {
      "target": "dmg"
    },
    "dmg": {
      "window": {
        "x": 200,
        "y": 200,
        "width": 800,
        "height": 600
      }
    },
    "win": {
      "icon": "logo.png",
      "target": "nsis"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "artifactName": "Setup-${productName}-${version}-${arch}.${ext}"
    },
    "snap": {}
  },
  "repository": "",
  "keywords": [
    "Electron",
    "demo"
  ],
  "author": "pptvyun",
  "license": "MIT",
  "devDependencies": {
    "electron": "^2.0.0",
    "electron-builder": "^20.19.2"
  }
}

在scripts中新增了在各個平臺上的打包指令碼,新增了build屬性,用來配置在各個平臺上的打包引數。我們用打包出window平臺的安裝程式為例,在命令列執行

npm run build-win

等待打包完成,第一次可能會有點慢,因為它需要下載一些依賴,打包完成後我們的專案裡就會多出來一個build的目錄,當然這個目錄是我們在package.json中配置的,這個目錄裡就是打包之後的檔案,裡面有一個win-unpacked的目錄,這個目錄裡有一個demo.exe的檔案,這個檔案就是可執行程式,這個win-unpacked目錄裡的檔案就相當於是我們平常見的綠色免安裝版,直接雙擊demo.exe就可以執行這個demo。還有一個Setup-demo-0.0.1.exe檔案,這個檔案就是我們需要的安裝程式,雙擊它就可以執行安裝,安裝完成後也會在我們的桌面建立快捷方式,不同平臺的打包命令可能需要到相應的平臺上去執行,比如在window上執行macOS的打包命令可能會失敗。

至此,我們的demo就算開發完成了,當然這裡還有一些細節上的東西需要注意,比如我們的應用需要在各大平臺上釋出,供其他使用者來下載安裝,就需要對程式碼進行簽名,程式碼簽名具體可參考文件docs/tutorial/code-signing ,這裡的客戶端介面我們用的是預設的,當然我們也可以做一個無邊框的應用,然後自定義客戶端的介面,還有自動升級等等功能,Electron都是可以支援的,可以檢視它的官方文件做出功能豐富的客戶端面應用。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559352/viewspace-2668147/,如需轉載,請註明出處,否則將追究法律責任。

相關文章