用JS開發桌面應用(一)原理篇

導讀

使用Electron開發客戶端程序已經有一段時間了,整體感覺還是非常不錯的,其中也遇到了一些坑點,本文旨在從【運行原理】到【實際應用】對Electron進行一次系統性的總結。【多圖,長文預警~】

另外electron-react還可作為使用Electron + React + Mobx + Webpack技術棧的腳手架工程。

一、桌面應用程序

桌面應用程序,又稱為 GUI 程序(Graphical User Interface),但是和 GUI 程序也有一些區別。桌面應用程序 將 GUI 程序從 GUI 具體為“桌面”,使冷冰冰的像塊木頭一樣的電腦概念更具有 人性化,更生動和富有活力。

我們電腦上使用的各種客戶端程序都屬于桌面應用程序,近年來WEB和移動端的興起讓桌面程序漸漸暗淡,但是在某些日常功能或者行業應用中桌面應用程序仍然是必不可少的。

傳統的桌面應用開發方式,一般是下面兩種:

1.1 原生開發

直接將語言編譯成可執行文件,直接調用系統API,完成 UI 繪制等。這類開發技術,有著較高的運行效率,但一般來說,開發速度較慢,技術要求較高,例如:

  • 使用C++ / MFC開發Windows應用
  • 使用Objective-C開發MAC應用

1.2 托管平臺

一開始就有本地開發和 UI 開發。一次編譯后,得到中間文件,通過平臺或虛機完成二次加載編譯或解釋運行。運行效率低于原生編譯,但平臺優化后,其效率也是比較可觀的。就開發速度方面,比原生編譯技術要快一些。例如:

  • 使用C# / .NET Framework(只能開發Windows應用)
  • Java / Swing

不過,上面兩種對前端開發人員太不友好了,基本是前端人員不會設計的領域,但是在這個【大前端 ??】的時代,前端開發者正在想方設法涉足各個領域,使用WEB技術開發客戶端的方式橫空出世。

1.3 WEB 開發

使用WEB技術進行開發,利用瀏覽器引擎完成UI渲染,利用Node.js實現服務器端JS編程并可以調用系統API,可以把它想像成一個套了一個客戶端外殼的WEB應用。

在界面上,WEB的強大生態為UI帶來了無限可能,并且開發、維護成本相對較低,有WEB開發經驗的前端開發者很容易上手進行開發。

本文就來著重介紹使用WEB技術開發客戶端程序的技術之一【electron

二、Electron

Electron是由Github開發,用HTML,CSSJavaScript來構建跨平臺桌面應用程序的一個開源庫。 Electron通過將ChromiumNode.js合并到同一個運行時環境中,并將其打包為Mac,WindowsLinux系統下的應用來實現這一目的。

https://electronjs.org/docs

https://juejin.im/post/5c67619351882562276c3162#heading-5

2.1 使用 Electron 開發的理由:

  • 1.使用具有強大生態的Web技術進行開發,開發成本低,可擴展性強,更炫酷的UI
  • 2.跨平臺,一套代碼可打包為Windows、Linux、Mac三套軟件,且編譯快速
  • 3.可直接在現有Web應用上進行擴展,提供瀏覽器不具備的能力
  • 4.你是一個前端 ???

當然,我們也要認清它的缺點:性能比原生桌面應用要低,最終打包后的安裝包和其他文件都比較大。

2.2 開發體驗

兼容性

雖然你還在用WEB技術進行開發,但是你不用再考慮兼容性問題了,你只需要關心你當前使用Electron的版本對應Chrome的版本,一般情況下它已經足夠新來讓你使用最新的API和語法了,你還可以手動升級Chrome版本。同樣的,你也不用考慮不同瀏覽器帶了的樣式和代碼兼容問題。

Node 環境

這可能是很多前端開發者曾經夢想過的功能,在WEB界面中使用Node.js提供的強大API,這意味著你在WEB頁面直接可以操作文件,調用系統API,甚至操作數據庫。當然,除了完整的Node API,你還可以使用額外的幾十萬個npm模塊。

跨域

你可以直接使用Node提供的request模塊進行網絡請求,這意味著你無需再被跨域所困擾。

強大的擴展性

借助node-ffi,為應用程序提供強大的擴展性(后面的章節會詳細介紹)。

2.3 誰在用 Electron

現在市面上已經有非常多的應用在使用electron進行開發了,包括我們熟悉的VS Code客戶端、GitHub客戶端、Atom客戶端等等。印象很深的,去年迅雷在發布迅雷 X10.1時的文案:

從迅雷 X 10.1 版本開始,我們采用 Electron 軟件框架完全重寫了迅雷主界面。使用新框架的迅雷 X 可以完美支持 2K、4K 等高清顯示屏,界面中的文字渲染也更加清晰銳利。從技術層面來說,新框架的界面繪制、事件處理等方面比老框架更加靈活高效,因此界面的流暢度也顯著優于老框架的迅雷。至于具體提升有多大?您一試便知。

你可以打開VS Code,點擊【幫助】【切換開發人員工具】來VS Code客戶端的界面。

三、Electron 運行原理

Electron 結合了 Chromium、Node.js 和用于調用操作系統本地功能的API。

3.1 Chromium

ChromiumGoogle為發展Chrome瀏覽器而啟動的開源項目,Chromium相當于Chrome的工程版或稱實驗版,新功能會率先在Chromium上實現,待驗證后才會應用在Chrome上,故Chrome的功能會相對落后但較穩定。

ChromiumElectron提供強大的UI能力,可以在不考慮兼容性的情況下開發界面。

3.2 Node.js

Node.js是一個讓JavaScript運行在服務端的開發平臺,Node使用事件驅動,非阻塞I/O模型而得以輕量和高效。

單單靠Chromium是不能具備直接操作原生GUI能力的,Electron內集成了Nodejs,這讓其在開發界面的同時也有了操作系統底層API的能力,Nodejs 中常用的 Path、fs、Crypto 等模塊在 Electron 可以直接使用。

3.3 系統 API

為了提供原生系統的GUI支持,Electron內置了原生應用程序接口,對調用一些系統功能,如調用系統通知、打開系統文件夾提供支持。

在開發模式上,Electron在調用系統API和繪制界面上是分離開發的,下面我們來看看Electron關于進程如何劃分。

3.4 主進程

Electron區分了兩種進程:主進程和渲染進程,兩者各自負責自己的職能。

Electron 運行package.jsonmain 腳本的進程被稱為主進程。一個 Electron 應用總是有且只有一個主進程。

職責:

  • 創建渲染進程(可多個)
  • 控制了應用生命周期(啟動、退出APP以及對APP做一些事件監聽)
  • 調用系統底層功能、調用原生資源

可調用的 API:

  • Node.js API
  • Electron提供的主進程API(包括一些系統功能和Electron附加功能)

3.5 渲染進程

由于 Electron 使用了 Chromium 來展示 web 頁面,所以 Chromium 的多進程架構也被使用到。 每個Electron 中的 web頁面運行在它自己的渲染進程中。

主進程使用 BrowserWindow 實例創建頁面。 每個 BrowserWindow 實例都在自己的渲染進程里運行頁面。 當一個 BrowserWindow 實例被銷毀后,相應的渲染進程也會被終止。

你可以把渲染進程想像成一個瀏覽器窗口,它能存在多個并且相互獨立,不過和瀏覽器不同的是,它能調用Node API。

職責:

  • HTMLCSS渲染界面
  • JavaScript做一些界面交互

可調用的 API:

  • DOM API
  • Node.js API
  • Electron提供的渲染進程API

四、Electron 基礎

4.1 Electron API

在上面的章節我們提到,渲染進和主進程分別可調用的Electron API。所有ElectronAPI都被指派給一種進程類型。 許多API只能被用于主進程中,有些API又只能被用于渲染進程,又有一些主進程和渲染進程中都可以使用。

你可以通過如下方式獲取Electron API

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

下面是一些常用的Electron API

在后面的章節我們會選擇其中常用的模塊進行詳細介紹。

4.2 使用 Node.js 的 API

你可以同時在Electron的主進程和渲染進程使用Node.js API,)所有在Node.js可以使用的API,在Electron中同樣可以使用。

import { shell } from "electron";
import os from "os";

document.getElementById("btn").addEventListener("click", () => {
  shell.showItemInFolder(os.homedir());
});

有一個非常重要的提示: 原生 Node.js 模塊 (即指,需要編譯源碼過后才能被使用的模塊) 需要在編譯后才能和 Electron 一起使用。

4.3 進程通信

主進程和渲染進程雖然擁有不同的職責,然是他們也需要相互協作,互相通訊。

例如:在web頁面管理原生GUI資源是很危險的,會很容易泄露資源。所以在web頁面,不允許直接調用原生GUI相關的API。渲染進程如果想要進行原生的GUI操作,就必須和主進程通訊,請求主進程來完成這些操作。

4.4 渲染進程向主進程通信

ipcRenderer 是一個 EventEmitter 的實例。 你可以使用它提供的一些方法從渲染進程發送同步或異步的消息到主進程。 也可以接收主進程回復的消息。

在渲染進程引入ipcRenderer

import { ipcRenderer } from "electron";

異步發送:

通過 channel 發送同步消息到主進程,可以攜帶任意參數。

在內部,參數會被序列化為 JSON,因此參數對象上的函數和原型鏈不會被發送。

ipcRenderer.send("sync-render", "我是來自渲染進程的異步消息");

同步發送:

const msg = ipcRenderer.sendSync("async-render", "我是來自渲染進程的同步消息");

注意: 發送同步消息將會阻塞整個渲染進程,直到收到主進程的響應。

主進程監聽消息:

ipcMain模塊是EventEmitter類的一個實例。 當在主進程中使用時,它處理從渲染器進程(網頁)發送出來的異步和同步信息。 從渲染器進程發送的消息將被發送到該模塊。

ipcMain.on:監聽 channel,當接收到新的消息時 listener 會以 listener(event, args...) 的形式被調用。

ipcMain.on("sync-render", (event, data) => {
  console.log(data);
});

4.5 主進程向渲染進程通信

https://imweb.io/topic/5b13a663d4c96b9b1b4c4e9c

在主進程中可以通過BrowserWindowwebContents向渲染進程發送消息,所以,在發送消息前你必須先找到對應渲染進程的BrowserWindow對象。:

const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.webContents.send("main-msg", `ConardLi]`);

根據消息來源發送:

ipcMain接受消息的回調函數中,通過第一個參數event的屬性sender可以拿到消息來源渲染進程的webContents對象,我們可以直接用此對象回應消息。

ipcMain.on("sync-render", (event, data) => {
  console.log(data);
  event.sender.send("main-msg", "主進程收到了渲染進程的【異步】消息!");
});

渲染進程監聽:

ipcRenderer.on:監聽 channel, 當新消息到達,將通過listener(event, args...)調用 listener

ipcRenderer.on("main-msg", (event, msg) => {
  console.log(msg);
});

4.6 通信原理

ipcMainipcRenderer 都是 EventEmitter 類的一個實例。EventEmitter 類是 NodeJS 事件的基礎,它由 NodeJS 中的 events 模塊導出。

EventEmitter 的核心就是事件觸發與事件監聽器功能的封裝。它實現了事件模型需要的接口, 包括 addListener,removeListener, emit 及其它工具方法. 同原生 JavaScript 事件類似, 采用了發布/訂閱(觀察者)的方式, 使用內部 _events 列表來記錄注冊的事件處理器。

我們通過 ipcMainipcRendereron、send 進行監聽和發送消息都是 EventEmitter 定義的相關接口。

4.7 remote

remote 模塊為渲染進程(web 頁面)和主進程通信(IPC)提供了一種簡單方法。 使用 remote 模塊, 你可以調用 main 進程對象的方法, 而不必顯式發送進程間消息, 類似于 JavaRMI

import { remote } from "electron";

remote.dialog.showErrorBox("主進程才有的dialog模塊", "我是使用remote調用的");

但實際上,我們在調用遠程對象的方法、函數或者通過遠程構造函數創建一個新的對象,實際上都是在發送一個同步的進程間消息。

在上面通過 remote 模塊調用 dialog 的例子里。我們在渲染進程中創建的 dialog 對象其實并不在我們的渲染進程中,它只是讓主進程創建了一個 dialog 對象,并返回了這個相對應的遠程對象給了渲染進程。

4.8 渲染進程間通信

Electron并沒有提供渲染進程之間相互通信的方式,我們可以在主進程中建立一個消息中轉站。

渲染進程之間通信首先發送消息到主進程,主進程的中轉站接受到消息后根據條件進行分發。

4.9 渲染進程數據共享

在兩個渲染進程間共享數據最簡單的方法是使用瀏覽器中已經實現的HTML5 API。 其中比較好的方案是用Storage APIlocalStorage,sessionStorage 或者 IndexedDB。

就像在瀏覽器中使用一樣,這種存儲相當于在應用程序中永久存儲了一部分數據。有時你并不需要這樣的存儲,只需要在當前應用程序的生命周期內進行一些數據的共享。這時你可以用 Electron 內的 IPC 機制實現。

將數據存在主進程的某個全局變量中,然后在多個渲染進程中使用 remote 模塊來訪問它。

在主進程中初始化全局變量:

global.mainId = ...;
global.device = {...};
global.__dirname = __dirname;
global.myField = { name: 'ConardLi' };

在渲染進程中讀?。?/p>

import { ipcRenderer, remote } from "electron";

const { getGlobal } = remote;

const mainId = getGlobal("mainId");
const dirname = getGlobal("__dirname");
const deviecMac = getGlobal("device").mac;

在渲染進程中改變:

getGlobal("myField").name = "code秘密花園";

多個渲染進程共享同一個主進程的全局變量,這樣即可達到渲染進程數據共享和傳遞的效果。

文中如有錯誤,歡迎在評論區指正,如果這篇文章幫助到了你,歡迎點贊和關注。


歡迎大家到公眾號: you的日常 閱讀,體驗更好哦。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 十一、擴展能力 在很多情況下,你的應用程序要和外部設備進行交互,一般情況下廠商會為你提供硬件設備的開發包,這些開發...
    you的日常閱讀 447評論 1 2
  • 想一個好的工具型應用的點子非常不容易,有了點子以后會發現,在各個平臺實現,滿足各個平臺的用戶更是意見困難的事情???..
    石頭老張閱讀 12,498評論 0 11
  • 文by羅小早 清晨,雨聲清脆,把沉睡的人喚醒,假期的尾聲,或近或遠,都將回歸。 他鄉的她,迎來了人生的新起點,故事...
    時光菲101閱讀 497評論 1 29
  • 10月18日,全球軍人的運動會——第七屆世界軍人運動會(就是武漢的軍運會)即將在武漢開幕,這是繼北京奧運會后...
    月兒小公舉閱讀 135評論 0 0
  • 大學寶典-青芒杯征文大賽 編號:1436 文/索路 工作收尾,一切都處置妥當后,我匆匆踏上回家的路。時間溜得很快,...
    索路閱讀 668評論 5 26