Electron可以讓程序員重用Web的代碼,使用HTML、CSS、JavaScript來構建桌面應用,并在不同平臺上使用。
Electron官網上說:“比你想象的更簡單”————“如果你可以建一個網站,你就可以建一個桌面應用程序。 Electron 是一個使用 JavaScript, HTML 和 CSS 等 Web 技術創建原生程序的框架,它負責比較難搞的部分,你只需把精力放在你的應用的核心上即可。”
Vue 是一套用于構建用戶界面的漸進式框架。與其它大型框架不同的是,Vue 被設計為可以自底向上逐層應用。目前 Vue 已經成為繼 Rect 之后最流行的前端開發框架。
我找到了一個插件:vite-plugin-electron,可以很方便的幫我們把 electron 和 vue 結合起來,開發起來非常方便。
因為我們需要使用 Electron 和 vue 進行開發,因此我們把它們分開目錄存儲,基礎目錄結構如下:
目錄結構這么設計的原因是因為我們使用的 vite-plugin-electron 插件需要使用這樣的目錄結構,目前還沒有提供設置修改。
下面就開始初始項目
yarn create vite electron-desktop --template vue-ts
先使用 vite 創建 vue 項目,然后我們再將 electron 嵌入到里面
初始化完成后,我們先做一個本地 yarn 源的配置,防止下載庫的時候出現異常。
registry "https://registry.npm.taobao.org/"
electron_mirror "https://npm.taobao.org/mirrors/electron/"
electron_builder_binaries_mirror "http://npm.taobao.org/mirrors/electron-builder-binaries/"
配置完下載源后,就可以安裝 electron 了
yarn add -D electron electron-builder rimraf vite-plugin-electron electron-devtools-installer
vite-plugin-electron 插件是將 vite 和 electron 結合在一起的,可以讓我們非常方便的結合 electron 和 vue,需要做一些指定的配置。
可以參考 electron 官網的快速開始項目
// electron-main/index.ts
import { app, BrowserWindow } from 'electron';
import path from 'path';
const createWindow=()=> {
const win=new BrowserWindow({
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, '../electron-preload/index.js'),
},
});
if (app.isPackaged) {
win.loadFile(path.join(__dirname, '../index.html'));
} else {
// Use ['ENV_NAME'] avoid vite:define plugin
const url=`http://${process.env['VITE_DEV_SERVER_HOST']}:${process.env['VITE_DEV_SERVER_PORT']}`;
win.loadURL(url);
}
};
app.whenReady().then(()=> {
createWindow();
app.on('activate', ()=> {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length===0) createWindow();
});
});
app.on('window-all-closed', ()=> {
if (process.platform !=='darwin') {
app.quit();
}
});
需要注意引入的預加載文件應該是打包后的 js 文件,路徑和 ts 文件路徑相同,只要類型改為 js 即可。
在預加載文件中我們打印一下系統平臺
// electron-preload/index.ts
import os from 'os';
console.log(os.platform());
在 tsconfig.json 中監聽 electron 相關文件和提示
"include": [..., "electron-main/**/*.ts", "electron-preload/**/*.ts"],
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import * as path from 'path';
import electron from 'vite-plugin-electron';
import electronRenderer from 'vite-plugin-electron/renderer';
import polyfillExports from 'vite-plugin-electron/polyfill-exports';
export default defineConfig({
plugins: [
vue(),
electron({
main: {
entry: 'electron-main/index.ts',
},
preload: {
// Must be use absolute path, this is the limit of rollup
input: path.join(__dirname, './electron-preload/index.ts'),
},
}),
electronRenderer(),
polyfillExports(),
],
build: {
emptyOutDir: false, // 必須配置,否則electron相關文件將不會生成build后的文件
},
});
配置主進程和預加載腳本地址
{
"name": "electron-desktop",
"private": true,
"version": "1.0.0",
"main": "dist/electron-main/index.js",
"scripts": {
"dev": "vite",
"build": "rimraf dist && vite build && electron-builder"
},
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"electron": "^18.2.0",
"electron-builder": "^23.0.3",
"electron-devtools-installer": "^3.2.0",
"rimraf": "^3.0.2",
"typescript": "^4.5.4",
"vite": "^2.9.5",
"vite-plugin-electron": "^0.4.3",
"vue-tsc": "^0.34.7"
}
}
主要是增加入口文件,因為 electron 還沒有原生支持 ts,因此目前還是必須加載 js 文件,所以入口文件我們配置為解析后的 js 文件路徑:dist/electron-main/index.js,然后修改執行腳本,在 build 命令中增加 electron-builder 打包命令。
# package.json
{
......,
"build": {
"appId": "com.electron.desktop",
"productName": "ElectronVueVite",
"asar": true,
"copyright": "Copyright ? 2022 XingXingZaiXian",
"directories": {
"output": "release/${version}"
},
"files": [
"dist"
],
"mac": {
"artifactName": "${productName}_${version}.${ext}",
"target": [
"dmg"
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}_${version}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": false
}
}
}
electron-builder 的配置我們有一篇專門的文章介紹,這里并沒有什么特殊的配置,按照需求配置即可。
到這里就配置好了所有的文件,接下來我們執行開發命令看一看效果。
yarn run dev
然后我們執行打包命令看一看效果
yarn run build
執行完后會生成兩個目錄:dist 和 release
dist 目錄中生成的是前端打包文件,release 中生成的是 electron 打包文件,內容如下:
其中 win-uppacked 中生成的是無需安裝的執行文件,將此目錄直接壓縮后就可以發送給別人,解壓即可使用。ExectronVueVite_1.0.0.exe 文件是安裝包,打開會顯示安裝過程,執行完安裝過程后在系統的控制面板中的軟件列表中可以看到該軟件,也可以執行卸載。
打開后就是正常的軟件界面
我們創建好了項目結構,那么在使用 Vue 開發 Electron 桌面應用的時候還有一個比較重要的知識點要了解,就是消息通信。在新版本的 electron 中,推薦使用上下文隔離方式進行內部進程通信,electron 官網有很詳細的介紹和示例,這里我們只介紹一種方式,其他的方式大家可以通過查看官網示例來了解。
我們的示例是在 Vue 界面中顯示當前系統平臺。
在 electron-preload/index.ts 中添加如下代碼:
import os from 'os';
import { contextBridge } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
platform: os.platform(),
});
通過 electron 注冊的上下文隔離接口會添加給 window 對象,但是原始的 window 對象并不存在這些接口和屬性,ts 就會報錯,這時就需要我們為其編寫ts類型聲明文件.d.ts。
// src/types/global.d.ts
export interface IElectronAPI {
platform: string;
}
declare global {
interface Window {
electronAPI: IElectronAPI;
}
}
我們在 App.vue 中調用 window.electronAPI.platform 接口,把系統平臺信息顯示在界面上
// src/App.vue
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import HelloWorld from './components/HelloWorld.vue';
const platform=window.electronAPI.platform;
</script>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld :msg="`Hello Vue 3 + TypeScript + Vite in ${platform}`" />
</template>
執行完以上步驟后,運行項目看一下效果。
是否曾受限于瀏覽器的沙盒限制,或者再操作網頁應用時無法滿足下列場景下的使用需求,譬如讀寫本地文件、調用更多系統資源等。
因此,你想自行開發一個可以跨平臺的調動資源的程序,但是又擔心要學習新的語言,時間來不及,開發預算不足。
面對上述困擾,相信許多前端小伙伴腦中就會反應出一個應用程序——Electron。Electron的官網上說:
Electron是一個使用JavaScript、HTML 和 CSS構建,支持 Web 技術的跨平臺開發桌面應用程序( GUI ),其本身是基于 C++ 開發的,GUI 核心來自于 Chrome,而 JavaScript 引擎使用 V8。
01
Electron誕生
回顧這項技術的誕生歷程,堪稱有趣。
時間回到2011年,一個叫王文睿的程序員與同事在討論如何為Tizen WebRuntime增加擴展API的時候,創建了Node-Webkit項目,并打算用Node.js來操作WebKit。幸運的是,這樣的嘗試得到了單位的大力支持,當時王文睿就職的公司內部有個鼓勵創新計劃,這不僅給王文睿足夠的時間,同時還分配了招聘名額,讓他找到協助完善這個項目的同行人。
彼時,另外一位年輕人趙成進入了這個團隊,并在實習的半年多時間里開發了跨平臺的UI庫功能,以及對Node-Webkit進行了一系列的修改與完善。
后來,趙成離開了,但是探索的腳步并未停止。之后,趙成幫助github團隊嘗試把Node-Webki應用到Atom編輯器上。不過因項目穩定性等原因,最終這個嘗試以失敗告終。直至2013年,GitHub與趙成啟動了一個類似Node-Webkit的項目:Atom Shell。不久后,基于Atom 的框架,Electron被開發出來。2014年,這兩個項目正式開源。(此處背景源自網絡公開資料)
時至今日,Electron被Atom編輯器和許多其他應用程序使用,并且兼容Mac、Windows 和 Linux,可以構建出三個平臺的應用程序。其中,這兩年爆火的飛書就是基于這個技術開發而來,另外,VS Code、Slack等應用也是基于這個技術開發。
02
推薦一個GVP項目
初步了解這項技術的魅力及應用歷程之后,今天,勤奮好學的馬建倉就給大家推薦一款以Electron為核心的入門簡單、功能豐富的 js 跨平臺桌面軟件開發框架——electron-egg。
項目概述
1. 這是一套代碼,可以打包成Windows版、Mac版、Linux版或者以web網站運行,你只需要學習js語言即可,不過也同時支持Vue、React、EJS等前端技術。
2. 從理論上來說,electron-egg支持任何前端技術,能夠編寫出精美的UI效果。
3. 同時,該項目也可以用服務端的開發思維,來編寫桌面軟件,更重要的是還能同時啟動多個工作進程。
操作準備
正式操作之前,你需要了解以下三個知識
使用流程
向上滑動閱覽
1.選擇開發模式:打開配置文件:electron-egg/electron/config.js。
2. 啟動:
(1) vue模式【默認】;
(2) react模式,同vue;
(3) ejs模式,模板渲染
3. 編寫一個api,供前端使用。
4. 如果是ejs模板渲染方式,編寫一個hello頁面。
5. 打包成exe、dmg、deb可執行文件。
場景演示
使用Electron開發客戶端程序已經有一段時間了,整體感覺還是非常不錯的,其中也遇到了一些坑點,本文是從【運行原理】到【實際應用】對Electron進行一次系統性的總結?!径鄨D,長文預警~】
本文所有實例代碼均在我的github electron-react上,結合代碼閱讀文章效果更佳。另外electron-react還可作為使用Electron + React + Mobx + Webpack技術棧的腳手架工程。
github:https://github.com/ConardLi/electron-react
桌面應用程序,又稱為 GUI 程序(Graphical User Interface),但是和 GUI 程序也有一些區別。桌面應用程序 將 GUI 程序從GUI 具體為“桌面”,使冷冰冰的像塊木頭一樣的電腦概念更具有 人性化,更生動和富有活力。
我們電腦上使用的各種客戶端程序都屬于桌面應用程序,近年來WEB和移動端的興起讓桌面程序漸漸暗淡,但是在某些日常功能或者行業應用中桌面應用程序仍然是必不可少的。
傳統的桌面應用開發方式,一般是下面兩種:
直接將語言編譯成可執行文件,直接調用系統API,完成UI繪制等。這類開發技術,有著較高的運行效率,但一般來說,開發速度較慢,技術要求較高,例如:
一開始就有本地開發和UI開發。一次編譯后,得到中間文件,通過平臺或虛機完成二次加載編譯或解釋運行。運行效率低于原生編譯,但平臺優化后,其效率也是比較可觀的。就開發速度方面,比原生編譯技術要快一些。例如:
不過,上面兩種對前端開發人員太不友好了,基本是前端人員不會涉及的領域,但是在這個【大前端】的時代,前端開發者正在想方設法涉足各個領域,使用WEB技術開發客戶端的方式橫空出世。
使用WEB技術進行開發,利用瀏覽器引擎完成UI渲染,利用Node.js實現服務器端JS編程并可以調用系統API,可以把它想像成一個套了一個客戶端外殼的WEB應用。
在界面上,WEB的強大生態為UI帶來了無限可能,并且開發、維護成本相對較低,有WEB開發經驗的前端開發者很容易上手進行開發。
本文就來著重介紹使用WEB技術開發客戶端程序的技術之一【electron】
Electron是由Github開發,用HTML,CSS和JavaScript來構建跨平臺桌面應用程序的一個開源庫。 Electron通過將Chromium和Node.js合并到同一個運行時環境中,并將其打包為Mac,Windows和Linux系統下的應用來實現這一目的。
當然,我們也要認清它的缺點:性能比原生桌面應用要低,最終打包后的應用比原生應用大很多。
兼容性
雖然你還在用WEB技術進行開發,但是你不用再考慮兼容性問題了,你只需要關心你當前使用Electron的版本對應Chrome的版本,一般情況下它已經足夠新來讓你使用最新的API和語法了,你還可以手動升級Chrome版本。同樣的,你也不用考慮不同瀏覽器帶的樣式和代碼兼容問題。
Node環境
這可能是很多前端開發者曾經夢想過的功能,在WEB界面中使用Node.js提供的強大API,這意味著你在WEB頁面直接可以操作文件,調用系統API,甚至操作數據庫。當然,除了完整的Node API,你還可以使用額外的幾十萬個npm模塊。
跨域
你可以直接使用Node提供的request模塊進行網絡請求,這意味著你無需再被跨域所困擾。
強大的擴展性
借助node-ffi,為應用程序提供強大的擴展性(后面的章節會詳細介紹)。
現在市面上已經有非常多的應用在使用Electron進行開發了,包括我們熟悉的VS Code客戶端、GitHub客戶端、Atom客戶端等等。印象很深的,去年迅雷在發布迅雷X10.1時的文案:
從迅雷X 10.1版本開始,我們采用Electron軟件框架完全重寫了迅雷主界面。使用新框架的迅雷X可以完美支持2K、4K等高清顯示屏,界面中的文字渲染也更加清晰銳利。從技術層面來說,新框架的界面繪制、事件處理等方面比老框架更加靈活高效,因此界面的流暢度也顯著優于老框架的迅雷。至于具體提升有多大?您一試便知。
你可以打開VS Code,點擊【幫助】【切換開發人員工具】來調試VS Code客戶端的界面。
Electron 結合了 Chromium、Node.js 和用于調用操作系統本地功能的API。
Chromium是Google為發展Chrome瀏覽器而啟動的開源項目,Chromium相當于Chrome的工程版或稱實驗版,新功能會率先在Chromium上實現,待驗證后才會應用在Chrome上,故Chrome的功能會相對落后但較穩定。
Chromium為Electron提供強大的UI能力,可以在不考慮兼容性的情況下開發界面。
Node.js是一個讓JavaScript運行在服務端的開發平臺,Node使用事件驅動,非阻塞I/O模型而得以輕量和高效。
單單靠Chromium是不能具備直接操作原生GUI能力的,Electron內集成了Nodejs,這讓其在開發界面的同時也有了操作系統底層API的能力,Nodejs 中常用的 Path、fs、Crypto 等模塊在 Electron 可以直接使用。
為了提供原生系統的GUI支持,Electron內置了原生應用程序接口,對調用一些系統功能,如調用系統通知、打開系統文件夾提供支持。
在開發模式上,Electron在調用系統API和繪制界面上是分離開發的,下面我們來看看Electron關于進程如何劃分。
Electron區分了兩種進程:主進程和渲染進程,兩者各自負責自己的職能。
Electron 運行package.json的 main 腳本的進程被稱為主進程。一個 Electron 應用總是有且只有一個主進程。
職責:
可調用的API:
由于 Electron 使用了 Chromium 來展示 web 頁面,所以 Chromium 的多進程架構也被使用到。 每個Electron 中的 web頁面運行在它自己的渲染進程中。
主進程使用 BrowserWindow 實例創建頁面。 每個 BrowserWindow 實例都在自己的渲染進程里運行頁面。 當一個 BrowserWindow 實例被銷毀后,相應的渲染進程也會被終止。
你可以把渲染進程想像成一個瀏覽器窗口,它能存在多個并且相互獨立,不過和瀏覽器不同的是,它能調用Node API。
職責:
可調用的API:
在上面的章節我們提到,渲染進和主進程分別可調用的Electron API。所有Electron的API都被指派給一種進程類型。 許多API只能被用于主進程中,有些API又只能被用于渲染進程,又有一些主進程和渲染進程中都可以使用。
你可以通過如下方式獲取Electron API
const { BrowserWindow, ... }=require('electron')
復制代碼
下面是一些常用的Electron 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一起使用。
主進程和渲染進程雖然擁有不同的職責,然是他們也需要相互協作,互相通訊。
例如:在web頁面管理原生GUI資源是很危險的,會很容易泄露資源。所以在web頁面,不允許直接調用原生GUI相關的API。渲染進程如果想要進行原生的GUI操作,就必須和主進程通訊,請求主進程來完成這些操作。
ipcRenderer 是一個 EventEmitter 的實例。 你可以使用它提供的一些方法,從渲染進程發送同步或異步的消息到主進程。 也可以接收主進程回復的消息。
在渲染進程引入ipcRenderer:
import { ipcRenderer } from 'electron';
復制代碼
異步發送:
通過 channel 發送同步消息到主進程,可以攜帶任意參數。
在內部,參數會被序列化為 JSON,因此參數對象上的函數和原型鏈不會被發送。
ipcRenderer.send('async-render', '我是來自渲染進程的異步消息');
復制代碼
同步發送:
const msg=ipcRenderer.sendSync('sync-render', '我是來自渲染進程的同步消息');
復制代碼
注意: 發送同步消息將會阻塞整個渲染進程,直到收到主進程的響應。
主進程監聽消息:
ipcMain模塊是EventEmitter類的一個實例。 當在主進程中使用時,它處理從渲染器進程(網頁)發送出來的異步和同步信息。 從渲染器進程發送的消息將被發送到該模塊。
ipcMain.on:監聽 channel,當接收到新的消息時 listener 會以 listener(event, args...) 的形式被調用。
ipcMain.on('sync-render', (event, data)=> {
console.log(data);
});
復制代碼
在主進程中可以通過BrowserWindow的webContents向渲染進程發送消息,所以,在發送消息前你必須先找到對應渲染進程的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);
})
復制代碼
ipcMain 和 ipcRenderer 都是 EventEmitter 類的一個實例。EventEmitter 類是 NodeJS 事件的基礎,它由 NodeJS 中的 events 模塊導出。
EventEmitter 的核心就是事件觸發與事件監聽器功能的封裝。它實現了事件模型需要的接口, 包括 addListener,removeListener, emit 及其它工具方法. 同原生 JavaScript 事件類似, 采用了發布/訂閱(觀察者)的方式, 使用內部 _events 列表來記錄注冊的事件處理器。
我們通過 ipcMain和ipcRenderer 的 on、send 進行監聽和發送消息都是 EventEmitter 定義的相關接口。
remote 模塊為渲染進程(web頁面)和主進程通信(IPC)提供了一種簡單方法。 使用 remote 模塊, 你可以調用 main 進程對象的方法, 而不必顯式發送進程間消息, 類似于 Java 的 RMI 。
import { remote } from 'electron';
remote.dialog.showErrorBox('主進程才有的dialog模塊', '我是使用remote調用的')
復制代碼
但實際上,我們在調用遠程對象的方法、函數或者通過遠程構造函數創建一個新的對象,實際上都是在發送一個同步的進程間消息。
在上面通過 remote 模塊調用 dialog 的例子里。我們在渲染進程中創建的 dialog 對象其實并不在我們的渲染進程中,它只是讓主進程創建了一個 dialog 對象,并返回了這個相對應的遠程對象給了渲染進程。
Electron并沒有提供渲染進程之間相互通信的方式,我們可以在主進程中建立一個消息中轉站。
渲染進程之間通信首先發送消息到主進程,主進程的中轉站接收到消息后根據條件進行分發。
在兩個渲染進程間共享數據最簡單的方法是使用瀏覽器中已經實現的HTML5 API。 其中比較好的方案是用Storage API, localStorage,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秘密花園';
復制代碼
多個渲染進程共享同一個主進程的全局變量,這樣即可達到渲染進程數據共享和傳遞的效果。
主進程模塊BrowserWindow用于創建和控制瀏覽器窗口。
mainWindow=new BrowserWindow({
width: 1000,
height: 800,
// ...
});
mainWindow.loadURL('http://www.conardli.top/');
復制代碼
你可以在這里查看它所有的構造參數。
無框窗口是沒有鑲邊的窗口,窗口的部分(如工具欄)不屬于網頁的一部分。
在BrowserWindow的構造參數中,將frame設置為false可以指定窗口為無邊框窗口,將工具欄隱藏后,就會產生兩個問題:
可以通過指定titleBarStyle選項來再將工具欄按鈕顯示出來,將其設置為hidden表示返回一個隱藏標題欄的全尺寸內容窗口,在左上角仍然有標準的窗口控制按鈕。
new BrowserWindow({
width: 200,
height: 200,
titleBarStyle: 'hidden',
frame: false
});
復制代碼
默認情況下, 無邊框窗口是不可拖拽的。我們可以在界面中通過CSS屬性-webkit-app-region: drag手動制定拖拽區域。
在無框窗口中, 拖動行為可能與選擇文本沖突,可以通過設定-webkit-user-select: none;禁用文本選擇:
.header {
-webkit-user-select: none;
-webkit-app-region: drag;
}
復制代碼
相反的,在可拖拽區域內部設置 -webkit-app-region: no-drag則可以指定特定不可拖拽區域。
通過將transparent選項設置為true, 還可以使無框窗口透明:
new BrowserWindow({
transparent: true,
frame: false
});
復制代碼
使用 webview 標簽在Electron 應用中嵌入 "外來" 內容。外來內容包含在 webview 容器中。 應用中的嵌入頁面可以控制外來內容的布局和重繪。
與 iframe 不同, webview 在與應用程序不同的進程中運行。它與您的網頁沒有相同的權限, 應用程序和嵌入內容之間的所有交互都將是異步的。
dialog 模塊提供了api來展示原生的系統對話框,例如打開文件框,alert框,所以web應用可以給用戶帶來跟系統應用相同的體驗。
注意:dialog是主進程模塊,想要在渲染進程調用可以使用remote
dialog.showErrorBox用于顯示一個顯示錯誤消息的模態對話框。
remote.dialog.showErrorBox('錯誤', '這是一個錯誤彈框!')
復制代碼
dialog.showErrorBox用于調用系統對話框,可以為指定幾種不同的類型: "none", "info", "error", "question" 或者 "warning"。
在 Windows 上, "question" 與"info"顯示相同的圖標, 除非你使用了 "icon" 選項設置圖標。 在 macOS 上, "warning" 和 "error" 顯示相同的警告圖標
remote.dialog.showMessageBox({
type: 'info',
title: '提示信息',
message: '這是一個對話彈框!',
buttons: ['確定', '取消']
}, (index)=> {
this.setState({ dialogMessage: `【你點擊了${index ? '取消' : '確定'}??!】` })
})
復制代碼
dialog.showOpenDialog用于打開或選擇系統目錄。
remote.dialog.showOpenDialog({
properties: ['openDirectory', 'openFile']
}, (data)=> {
this.setState({ filePath: `【選擇路徑:${data[0]}】 ` })
})
復制代碼
這里推薦直接使用HTML5 API,它只能在渲染器進程中使用。
let options={
title: '信息框標題',
body: '我是一條信息~~~',
}
let myNotification=new window.Notification(options.title, options)
myNotification.onclick=()=> {
this.setState({ message: '【你點擊了信息框??!】' })
}
復制代碼
通過remote獲取到主進程的process對象,可以獲取到當前應用的各個版本信息:
獲取當前應用根目錄:
remote.app.getAppPath()
復制代碼
使用node的os模塊獲取當前系統根目錄:
os.homedir();
復制代碼
Electron提供的clipboard在渲染進程和主進程都可使用,用于在系統剪貼板上執行復制和粘貼操作。
以純文本的形式寫入剪貼板:
clipboard.writeText(text[, type])
復制代碼
以純文本的形式獲取剪貼板的內容:
clipboard.readText([type])
復制代碼
desktopCapturer用于從桌面捕獲音頻和視頻的媒體源的信息。它只能在渲染進程中被調用。
下面的代碼是一個獲取屏幕截圖并保存的實例:
getImg=()=> {
this.setState({ imgMsg: '正在截取屏幕...' })
const thumbSize=this.determineScreenShotSize()
let options={ types: ['screen'], thumbnailSize: thumbSize }
desktopCapturer.getSources(options, (error, sources)=> {
if (error) return console.log(error)
sources.forEach((source)=> {
if (source.name==='Entire screen' || source.name==='Screen 1') {
const screenshotPath=path.join(os.tmpdir(), 'screenshot.png')
fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error)=> {
if (error) return console.log(error)
shell.openExternal(`file://${screenshotPath}`)
this.setState({ imgMsg: `截圖保存到: ${screenshotPath}` })
})
}
})
})
}
determineScreenShotSize=()=> {
const screenSize=screen.getPrimaryDisplay().workAreaSize
const maxDimension=Math.max(screenSize.width, screenSize.height)
return {
width: maxDimension * window.devicePixelRatio,
height: maxDimension * window.devicePixelRatio
}
}
復制代碼
應用程序的菜單可以幫助我們快捷的到達某一功能,而不借助客戶端的界面資源,一般菜單分為兩種:
Electron為我們提供了Menu模塊用于創建本機應用程序菜單和上下文菜單,它是一個主進程模塊。
你可以通過Menu的靜態方法buildFromTemplate(template),使用自定義菜單模版來構造一個菜單對象。
template是一個MenuItem的數組,我們來看看MenuItem的幾個重要參數:
推薦:最好指定role與標準角色相匹配的任何菜單項,而不是嘗試手動實現click函數中的行為。內置role行為將提供最佳的本地體驗。
下面的實例是一個簡單的額菜單template。
const template=[
{
label: '文件',
submenu: [
{
label: '新建文件',
click: function () {
dialog.showMessageBox({
type: 'info',
message: '嘿!',
detail: '你點擊了新建文件!',
})
}
}
]
},
{
label: '編輯',
submenu: [{
label: '剪切',
role: 'cut'
}, {
label: '復制',
role: 'copy'
}, {
label: '粘貼',
role: 'paste'
}]
},
{
label: '最小化',
role: 'minimize'
}
]
復制代碼
使用Menu的靜態方法setApplicationMenu,可創建一個應用程序菜單,在 Windows 和 Linux 上,menu將被設置為每個窗口的頂層菜單。
注意:必須在模塊ready事件后調用此 API app。
我們可以根據應用程序不同的的生命周期,不同的系統對菜單做不同的處理。
app.on('ready', function () {
const menu=Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
})
app.on('browser-window-created', function () {
let reopenMenuItem=findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled=false
})
app.on('window-all-closed', function () {
let reopenMenuItem=findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled=true
})
if (process.platform==='win32') {
const helpMenu=template[template.length - 1].submenu
addUpdateMenuItems(helpMenu, 0)
}
復制代碼
使用Menu的實例方法menu.popup可自定義彈出上下文菜單。
let m=Menu.buildFromTemplate(template)
document.getElementById('menuDemoContainer').addEventListener('contextmenu', (e)=> {
e.preventDefault()
m.popup({ window: remote.getCurrentWindow() })
})
復制代碼
在菜單選項中,我們可以指定一個accelerator屬性來指定操作的快捷鍵:
{
label: '最小化',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}
復制代碼
另外,我們還可以使用globalShortcut來注冊全局快捷鍵。
globalShortcut.register('CommandOrControl+N', ()=> {
dialog.showMessageBox({
type: 'info',
message: '嘿!',
detail: '你觸發了手動注冊的快捷鍵.',
})
})
復制代碼
CommandOrControl代表在macOS上為Command鍵,以及在Linux和Windows上為Control鍵。
很多情況下程序中使用的打印都是用戶無感知的。并且想要靈活的控制打印內容,往往需要借助打印機給我們提供的api再進行開發,這種開發方式非常繁瑣,并且開發難度較大。第一次在業務中用到Electron其實就是用到它的打印功能,這里就多介紹一些。
Electron提供的打印api可以非常靈活的控制打印設置的顯示,并且可以通過html來書寫打印內容。Electron提供了兩種方式進行打印,一種是直接調用打印機打印,一種是打印到pdf。
并且有兩種對象可以調用打?。?/p>
上面兩種方式同時擁有print和printToPdf方法。
contents.print([options], [callback]);
復制代碼
打印配置(options)中只有簡單的三個配置:
首先要將我們使用的打印機名稱配置好,并且要在調用打印前首先要判斷打印機是否可用。
使用webContents的getPrinters方法可獲取當前設備已經配置的打印機列表,注意配置過不是可用,只是在此設備上安裝過驅動。
通過getPrinters獲取到的打印機對象:electronjs.org/docs/api/st…
我們這里只管關心兩個,name和status,status為0時表示打印機可用。
print的第二個參數callback是用于判斷打印任務是否發出的回調,而不是打印任務完成后的回調。所以一般打印任務發出,回調函數即會調用并返回參數true。這個回調并不能判斷打印是否真的成功了。
if (this.state.curretnPrinter) {
mainWindow.webContents.print({
silent: silent, printBackground: true, deviceName: this.state.curretnPrinter
}, ()=> { })
} else {
remote.dialog.showErrorBox('錯誤', '請先選擇一個打印機!')
}
復制代碼
printToPdf的用法基本和print相同,但是print的配置項非常少,而printToPdf則擴展了很多屬性。這里翻了一下源碼發現還有很多沒有被貼進文檔的,大概有三十幾個,包括可以對打印的margin,打印頁眉頁腳等進行配置。
contents.printToPDF(options, callback)
復制代碼
callback函數在打印失敗或打印成功后調用,可獲取打印失敗信息或包含PDF數據的緩沖區。
const pdfPath=path.join(os.tmpdir(), 'webviewPrint.pdf');
const webview=document.getElementById('printWebview');
const renderHtml='我是被臨時插入webview的內容...';
webview.executeJavaScript('document.documentElement.innerHTML=`' + renderHtml + '`;');
webview.printToPDF({}, (err, data)=> {
console.log(err, data);
fs.writeFile(pdfPath, data, (error)=> {
if (error) throw error
shell.openExternal(`file://${pdfPath}`)
this.setState({ webviewPdfPath: pdfPath })
});
});
復制代碼
這個例子中的打印是使用webview完成的,通過調用executeJavaScript方法可動態向webview插入打印內容。
上面提到,使用webview和webcontent都可以調用打印功能,使用webcontent打印,首先要有一個打印窗口,這個窗口不能隨時打印隨時創建,比較耗費性能??梢詫⑺诔绦蜻\行時啟動好,并做好事件監聽。
此過程需和調用打印的進行做好通信,大致過程如下:
可見通信非常繁瑣,使用webview進行打印可實現同樣的效果但是通信方式會變得簡單,因為渲染進程和webview通信不需要經過主進程,通過如下方式即可:
const webview=document.querySelector('webview')
webview.addEventListener('ipc-message', (event)=> {
console.log(event.channel)
})
webview.send('ping');
const {ipcRenderer}=require('electron')
ipcRenderer.on('ping', ()=> {
ipcRenderer.sendToHost('pong')
})
復制代碼
之前專門為ELectron打印寫過一個DEMO:electron-print-demo有興趣可以clone下來看一下。
下面是幾個針對常用打印功能的工具函數封裝。
/**
* 獲取系統打印機列表
*/
export function getPrinters() {
let printers=[];
try {
const contents=remote.getCurrentWindow().webContents;
printers=contents.getPrinters();
} catch (e) {
console.error('getPrintersError', e);
}
return printers;
}
/**
* 獲取系統默認打印機
*/
export function getDefaultPrinter() {
return getPrinters().find(element=> element.isDefault);
}
/**
* 檢測是否安裝了某個打印驅動
*/
export function checkDriver(driverMame) {
return getPrinters().find(element=> (element.options["printer-make-and-model"] || '').includes(driverMame));
}
/**
* 根據打印機名稱獲取打印機對象
*/
export function getPrinterByName(name) {
return getPrinters().find(element=> element.name===name);
}
復制代碼
崩潰監控是每個客戶端程序必備的保護功能,當程序崩潰時我們一般期望做到兩件事:
electron為我們提供給了crashReporter來幫助我們記錄崩潰日志,我們可以通過crashReporter.start來創建一個崩潰報告器:
const { crashReporter }=require('electron')
crashReporter.start({
productName: 'YourName',
companyName: 'YourCompany',
submitURL: 'https://your-domain.com/url-to-submit',
uploadToServer: true
})
復制代碼
當程序發生崩潰時,崩潰報日志將被儲存在臨時文件夾中名為YourName Crashes的文件文件夾中。submitURL用于指定你的崩潰日志上傳服務器。 在啟動崩潰報告器之前,您可以通過調用app.setPath('temp', 'my/custom/temp')API來自定義這些臨時文件的保存路徑。你還可以通過crashReporter.getLastCrashReport()來獲取上次崩潰報告的日期和ID。
我們可以通過webContents的crashed來監聽渲染進程的崩潰,另外經測試有些主進程的崩潰也會觸發該事件。所以我們可以根據主window是否被銷毀來判斷進行不同的重啟邏輯,下面是整個崩潰監控的邏輯:
import { BrowserWindow, crashReporter, dialog } from 'electron';
// 開啟進程崩潰記錄
crashReporter.start({
productName: 'electron-react',
companyName: 'ConardLi',
submitURL: 'http://xxx.com', // 上傳崩潰日志的接口
uploadToServer: false
});
function reloadWindow(mainWin) {
if (mainWin.isDestroyed()) {
app.relaunch();
app.exit(0);
} else {
// 銷毀其他窗口
BrowserWindow.getAllWindows().forEach((w)=> {
if (w.id !==mainWin.id) w.destroy();
});
const options={
type: 'info',
title: '渲染器進程崩潰',
message: '這個進程已經崩潰.',
buttons: ['重載', '關閉']
}
dialog.showMessageBox(options, (index)=> {
if (index===0) mainWin.reload();
else mainWin.close();
})
}
}
export default function () {
const mainWindow=BrowserWindow.fromId(global.mainId);
mainWindow.webContents.on('crashed', ()=> {
const errorMessage=crashReporter.getLastCrashReport();
console.error('程序崩潰了!', errorMessage); // 可單獨上傳日志
reloadWindow(mainWindow);
});
}
復制代碼
有的時候我們并不想讓用戶通過點關閉按鈕的時候就關閉程序,而是把程序最小化到托盤,在托盤上做真正的退出操作。
首先要監聽窗口的關閉事件,阻止用戶關閉操作的默認行為,將窗口隱藏。
function checkQuit(mainWindow, event) {
const options={
type: 'info',
title: '關閉確認',
message: '確認要最小化程序到托盤嗎?',
buttons: ['確認', '關閉程序']
};
dialog.showMessageBox(options, index=> {
if (index===0) {
event.preventDefault();
mainWindow.hide();
} else {
mainWindow=null;
app.exit(0);
}
});
}
function handleQuit() {
const mainWindow=BrowserWindow.fromId(global.mainId);
mainWindow.on('close', event=> {
event.preventDefault();
checkQuit(mainWindow, event);
});
}
復制代碼
這時程序就再也找不到了,任務托盤中也沒有我們的程序,所以我們要先創建好任務托盤,并做好事件監聽。
windows平臺使用ico文件可以達到更好的效果
export default function createTray() {
const mainWindow=BrowserWindow.fromId(global.mainId);
const iconName=process.platform==='win32' ? 'icon.ico' : 'icon.png'
tray=new Tray(path.join(global.__dirname, iconName));
const contextMenu=Menu.buildFromTemplate([
{
label: '顯示主界面', click: ()=> {
mainWindow.show();
mainWindow.setSkipTaskbar(false);
}
},
{
label: '退出', click: ()=> {
mainWindow.destroy();
app.quit();
}
},
])
tray.setToolTip('electron-react');
tray.setContextMenu(contextMenu);
}
復制代碼
在很多情況下,你的應用程序要和外部設備進行交互,一般情況下廠商會為你提供硬件設備的開發包,這些開發包基本上都是通過C++ 編寫,在使用electron開發的情況下,我們并不具備直接調用C++代碼的能力,我們可以利用node-ffi來實現這一功能。
node-ffi提供了一組強大的工具,用于在Node.js環境中使用純JavaScript調用動態鏈接庫接口。它可以用來為庫構建接口綁定,而不需要使用任何C++代碼。
注意node-ffi并不能直接調用C++代碼,你需要將C++代碼編譯為動態鏈接庫:在 Windows下是 Dll ,在 Mac OS下是 dylib ,Linux 是 so 。
node-ffi 加載 Library是有限制的,只能處理 C風格的 Library。
下面是一個簡單的實例:
const ffi=require('ffi');
const ref=require('ref');
const SHORT_CODE=ref.refType('short');
const DLL=new ffi.Library('test.dll', {
Test_CPP_Method: ['int', ['string',SHORT_CODE]],
})
testCppMethod(str: String, num: number): void {
try {
const result: any=DLL.Test_CPP_Method(str, num);
return result;
} catch (error) {
console.log('調用失敗~',error);
}
}
this.testCppMethod('ConardLi',123);
復制代碼
上面的代碼中,我們用ffi包裝C++接口生成的動態鏈接庫test.dll,并使用ref進行一些類型映射。
使用JavaScript調用這些映射方法時,推薦使用TypeScript來約定參數類型,因為弱類型的JavaScript在調用強類型語言的接口時可能會帶來意想不到的風險。
借助這一能力,前端開發工程師也可以在IOT領域一展身手了~
一般情況下,我們的應用程序可能運行在多套環境下(production、beta、uat、moke、development...),不同的開發環境可能對應不同的后端接口或者其他配置,我們可以在客戶端程序中內置一個簡單的環境選擇功能來幫助我們更高效的開發。
具體策略如下:
const envList=["moke", "beta", "development", "production"];
exports.envList=envList;
const urlBeta='https://wwww.xxx-beta.com';
const urlDev='https://wwww.xxx-dev.com';
const urlProp='https://wwww.xxx-prop.com';
const urlMoke='https://wwww.xxx-moke.com';
const path=require('path');
const pkg=require(path.resolve(global.__dirname, 'package.json'));
const build=pkg['build-config'];
exports.handleEnv={
build,
currentEnv: 'moke',
setEnv: function (env) {
this.currentEnv=env
},
getUrl: function () {
console.log('env:', build.env);
if (build.env==='production' || this.currentEnv==='production') {
return urlProp;
} else if (this.currentEnv==='moke') {
return urlMoke;
} else if (this.currentEnv==='development') {
return urlDev;
} else if (this.currentEnv==="beta") {
return urlBeta;
}
},
isDebugger: function () {
return build.env==='development'
}
}
復制代碼
最后也是最重要的一步,將寫好的代碼打包成可運行的.app或.exe可執行文件。
這里我把打包氛圍兩部分來做,渲染進程打包和主進程打包。
一般情況下,我們的大部分業務邏輯代碼是在渲染進程完成的,在大部分情況下我們僅僅需要對渲染進程進行更新和升級而不需要改動主進程代碼,我們渲染進程的打包實際上和一般的web項目打包沒有太大差別,使用webpack打包即可。
這里我說說渲染進程單獨打包的好處:
打包完成的html和js文件,我們一般要上傳到我們的前端靜態資源服務器下,然后告知服務端我們的渲染進程有代碼更新,這里可以說成渲染進程單獨的升級。
注意,和殼的升級不同,渲染進程的升級僅僅是靜態資源服務器上html和js文件的更新,而不需要重新下載更新客戶端,這樣我們每次啟動程序的時候檢測到離線包有更新,即可直接刷新讀取最新版本的靜態資源文件,即使在程序運行過程中要強制更新,我們的程序只需要強制刷新頁面讀取最新的靜態資源即可,這樣的升級對用戶是非常友好的。
這里注意,一旦我們這樣配置,就意味著渲染進程和主進程打包升級的完全分離,我們在啟動主窗口時讀取的文件就不應該再是本地文件,而是打包完成后放在靜態資源服務器的文件。
為了方便開發,這里我們可以區分本地和線上加載不同的文件:
function getVersion (mac,current){
// 根據設備mac和當前版本獲取最新版本
}
export default function () {
if (build.env==='production') {
const version=getVersion (mac,current);
return 'https://www.xxxserver.html/electron-react/index_'+version+'.html';
}
return url.format({
protocol: 'file:',
pathname: path.join(__dirname, 'env/environment.html'),
slashes: true,
query: { debugger: build.env==="development" }
});
}
復制代碼
具體的webpack配置這里就不再貼出,可以到我的github electron-react的/scripts目錄下查看。
這里需要注意,在開發環境下我們可以結合webpack的devServer和electron命令來啟動app:
devServer: {
contentBase: './assets/',
historyApiFallback: true,
hot: true,
port: PORT,
noInfo: false,
stats: {
colors: true,
},
setup() {
spawn(
'electron',
['.'],
{
shell: true,
stdio: 'inherit',
}
)
.on('close', ()=> process.exit(0))
.on('error', e=> console.error(e));
},
},//...
復制代碼
主進程,即將整個程序打包成可運行的客戶端程序,常用的打包方案一般有兩種,electron-packager和electron-builder。
electron-packager在打包配置上我覺得有些繁瑣,而且它只能將應用直接打包為可執行程序。
這里我推薦使用electron-builder,它不僅擁有方便的配置 protocol 的功能、內置的 Auto Update、簡單的配置 package.json 便能完成整個打包工作,用戶體驗非常不錯。而且electron-builder不僅能直接將應用打包成exe app等可執行程序,還能打包成msi dmg等安裝包格式。
你可以在package.json方便的進行各種配置:
"build": {
"productName": "electron-react", // app中文名稱
"appId": "electron-react",// app標識
"directories": { // 打包后輸出的文件夾
"buildResources": "resources",
"output": "dist/"
}
"files": [ // 打包后依然保留的源文件
"main_process/",
"render_process/",
],
"mac": { // mac打包配置
"target": "dmg",
"icon": "icon.ico"
},
"win": { // windows打包配置
"target": "nsis",
"icon": "icon.ico"
},
"dmg": { // dmg文件打包配置
"artifactName": "electron_react.dmg",
"contents": [
{
"type": "link",
"path": "/Applications",
"x": 410,
"y": 150
},
{
"type": "file",
"x": 130,
"y": 150
}
]
},
"nsis": { // nsis文件打包配置
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"shortcutName": "electron-react"
},
}
復制代碼
執行electron-builder打包命令時,可指定參數進行打包。
--mac, -m, -o, --macos macOS打包
--linux, -l Linux打包
--win, -w, --windows Windows打包
--mwl 同時為macOS,Windows和Linux打包
--x64 x64 (64位安裝包)
--ia32 ia32(32位安裝包)
復制代碼
關于主進程的更新你可以使用electron-builder自帶的Auto Update模塊,在electron-react也實現了手動更新的模塊,由于篇幅原因這里就不再贅述,如果有興趣可以到我的github查看main下的update模塊。
electron-builder打包出來的App要比相同功能的原生客戶端應用體積大很多,即使是空的應用,體積也要在100mb以上。原因有很多:
第一點;為了達到跨平臺的效果,每個Electron應用都包含了整個V8引擎和Chromium內核。
第二點:打包時會將整個node_modules打包進去,大家都知道一個應用的node_module體積是非常龐大的,這也是使得Electron應用打包后的體積較大的原因。
第一點我們無法改變,我們可以從第二點對應用體積進行優化:Electron在打包時只會將denpendencies的依賴打包進去,而不會將 devDependencies 中的依賴進行打包。所以我們應盡可能的減少denpendencies中的依賴。在上面的進程中,我們使用webpack對渲染進程進行打包,所以渲染進程的依賴全部都可以移入devDependencies。
另外,我們還可以使用雙packajson.json的方式來進行優化,把只在開發環境中使用到的依賴放在整個項目的根目錄的package.json下,將與平臺相關的或者運行時需要的依賴裝在app目錄下。具體詳見two-package-structure。
本項目源碼地址:https://github.com/ConardLi/electron-react
希望你閱讀本篇文章后可以達到以下幾點:
文中如有錯誤,歡迎在評論區指正,如果這篇文章幫助到了你,歡迎點贊和關注。
想閱讀更多優質文章、可關注我的github博客,你的star?、點贊和關注是我持續創作的動力!
github:https://github.com/ConardLi/ConardLi.github.io
原鏈接:https://juejin.im/post/5cfd2ec7e51d45554877a59f
*請認真填寫需求信息,我們會在24小時內與您取得聯系。