文章目錄
- 簡介
-
- 安裝electron依賴
- 本地資料庫選擇
-
- indexedDB
-
- 封裝的庫
- SQLite
- Lowdb
- electron-store
- electron-json-storage-alt、electron-json-storage
- localstorage/sessionstorage
- cookie
- 小結
- 自動升級
-
- electron-updater
- 自己寫的升級模組
-
- window 平台
- mac 平台
-
- 對於 asar檔案
- 對於 dmg 檔案
- 網路檢查(window)
- 主行程 http請求客戶端
-
- net模組封裝
- urllib封裝
- axios
- request.js
- 下載檔案
-
- 對於node http 模組封裝
- 對於urllib 的封裝
- 客戶端日誌
-
- winston
- electron-log
- IPC 通訊(渲染window 發送訊息給主行程)
-
- ipcMain
-
- method
- demo
- ipcRenderer
-
- method
- demo
- IPC 通訊(主行程發送訊息給渲染window)
- 設定開機啟動項
- 其他應用喚醒客戶端
- 全域快速鍵
- 托盤
- mac 應用選單
- 國際化
- TouchBar
- 硬體加速(mac)
- 模式(development、production)
- 崩潰日誌發送
- 獨體模式
- 白屏
- electron bridge
- BrowserWindow http請求攔截
- 整合 vue 、react
-
- vue
- react
- 無frame
-
- window
- mac
- 設定DOM拖拽聯通視窗拖拽
- 代理設定
- 打包釋出
-
- electron-builder
- electron-packager
-
- 優缺點
- 專案地址
簡介
在開發 electron 桌面端的時候,會遇到各種各樣的坑,稍微總結下。
- 安裝electron依賴
- 本地資料庫選擇
- 自動升級
- 網路檢查(window)
- 主行程http請求客戶端
- 下載檔案
- http請求客戶端
- IPC 通訊(渲染window 發送訊息給主行程)
- IPC 通訊(主行程發送訊息給渲染window)
- 設定開機啟動項
- 其他應用喚醒客戶端
- 全域快速鍵
- 托盤
- 應用選單(mac)
- 國際化
- TouchBar
- 硬體加速(mac)
- 模式(development、production)
- 崩潰日誌發送
- 獨體模式
- 白屏
- electron bridge
- 整合 vue 、react
- 代理設定
- 打包釋出
安裝electron依賴
由於網路問題,導致 yarn 或者 npm 安裝的時候,會報錯
方案
1 2 | sudo npm install -g cross-env cross-env npm_config_electron_mirror="https://npm.taobao.org/mirrors/electron/" npm_config_electron_custom_dir="9.4.0" npm install |
這裡的 9.4.0 就是你所想安裝的electron 版本。
本地資料庫選擇
本地資料庫有很多可選的,
indexedDB
可以同時支援web端和桌面端使用。
IndexedDB是Chromium內建的一個基於JavaScript的物件導向的資料庫,在Electron應用內它儲存的容量限制與使用者的磁碟容量有關,是使用者磁碟大小的1/3.
特點:
- NoSQL資料庫,瀏覽器自帶,可以儲存大量資料,容量為250MB以上
- 支援交易,有版本號的概念。
- 支援較多的欄位型別
封裝的庫
- localForage,支援類Storage API語法的客戶端資料儲存polyfill,支援回退到Storage和Web SQL
- dexie.js,提供更友好和簡單的語法便於快速的編碼開發,有Typescript支援。
- ZangoDB,提供類MongoDB的介面實現,提供了許多MangoDB的屬性實現
- JsStore,提供基於indexedDB的類SQL的語法實現。
SQLite
關係型資料庫,具有關係型資料庫的一切屬性,交易遵循ACID屬性。小巧輕便,有knex這樣的庫做ORM。
是node原生模組,需要重新編譯,而且有坑。
1 2 | npm install sqlite3 --save npm install electron-rebuild --save |
執行 electron-rebuild,重新編譯electron。
Lowdb
基於Loadsh的純JSON檔案資料庫,速度較慢.
不支援索引/交易/批量操作等資料庫功能
Small JSON database for Node, Electron and the browser. Powered by Lodash. ??
electron-store
適合簡單儲存
electron-json-storage-alt、electron-json-storage
Easily write and read user settings in Electron apps
localstorage/sessionstorage
自帶的儲存。
坑點:
LocalStorage儲存容量也很小,大概不會超過10M,它是以鍵值對形式儲存資料的,同樣也沒有關聯搜尋、條件搜尋的機制
SessionStorage最大的問題是,每次關閉應用程式,它裡面的內容會被清空,想永續化儲存資料,就不用考慮它了
cookie
Cookies儲存容量太小,只能存4kb的內容,而且每次與伺服端互動,同域下的Cookie還會被攜帶到伺服端,也沒有關聯搜尋、條件搜尋的機制
小結
一般大型專案 首選
自動升級
推薦使用
electron-updater
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | import { autoUpdater } from 'electron-updater'; let downloading = false; function checkForUpdates() { if (downloading) { dialog.showMessageBox({ type: 'info', buttons: ['OK'], title: pkg.name, message: `Downloading...`, detail: `Please leave the app open, the new version is downloading. You'll receive a new dialog when downloading is finished.` }); return; } autoUpdater.checkForUpdates(); } autoUpdater.on('update-not-available', e => { dialog.showMessageBox({ type: 'info', buttons: ['OK'], title: pkg.name, message: `${pkg.name} is up to date :)`, detail: `${pkg.name} ${pkg.version} is currently the newest version available, It looks like you're already rocking the latest version!` }); console.log('Update not available.'); }); autoUpdater.on('update-available', e => { downloading = true; checkForUpdates(); }); autoUpdater.on('error', err => { dialog.showMessageBox({ type: 'error', buttons: ['Cancel update'], title: pkg.name, message: `Failed to update ${pkg.name} :(`, detail: `An error occurred in retrieving update information, Please try again later.`, }); downloading = false; console.error(err); }); autoUpdater.on('update-downloaded', info => { var { releaseNotes, releaseName } = info; var index = dialog.showMessageBox({ type: 'info', buttons: ['Restart', 'Later'], title: pkg.name, message: `The new version has been downloaded. Please restart the application to apply the updates.`, detail: `${releaseName} ${releaseNotes}` }); downloading = false; if (index === 1) { return; } autoUpdater.quitAndInstall(); setTimeout(() => { mainWindow = null; app.quit(); }); }); |
自己寫的升級模組
可以在專案啟動或者頁面啟動狀態的時候,請求伺服端最新版本號,如果存在版本過低的情況可以通知使用者下載升級。
同樣的是利用
window 平台
執行執行升級exe
mac 平台
對於 asar檔案
執行
對於 dmg 檔案
可以透過 mac 自帶的
網路檢查(window)
一般的桌面端程式,都需要檢查網路連接情況,友好的提示給客戶。
在 window 裡面可以藉助
https://www.npmjs.com/package/network-interface
1 2 3 4 5 6 7 8 9 10 | const networkInterface = require('network-interface'); networkInterface.addEventListener('wlan-status-changed', (error, data) => { if (error) { throw error; return; } console.log('event fired: wlan-status-changed'); console.log(data); }); |
主行程 http請求客戶端
對於桌面端,主行程是會有http請求的。 例如升級啥的,或者下載檔案等,都是需要主行程去執行http請求。
- electron 封裝的net模組
- urllib
- axios
- request、request-promise
net模組封裝
net 模組是一個發送 HTTP(S) 請求的客戶端API。 它類似於Node.js的HTTP 和 HTTPS 模組 ,但它使用的是Chromium原生網路庫來替代Node.js的實現,提供更好的網路代理支援。 It also supports checking network status.
net 的優勢
- 系統代理配置的自動管理, 支援 wpad 協定和代理 pac 設定檔。
- HTTPS 請求的自動隧道。
- 支援使用basic、digest、NTLM、Kerberos 或協商身份驗證方案對代理進行身份驗證。
- 支援傳輸監控代理: 類似於Fiddler代理,用於訪問控制和監視。
1 | const { net } = require('electron') |
urllib封裝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | const os = require('os'); const urllib = require('urllib'); const Agent = require('agentkeepalive'); const {<!-- -->HttpsAgent} = require('agentkeepalive'); const {<!-- -->electron: electronVersion} = process.versions; const config = {<!-- --> defaultArgs: {<!-- --> timeout: 30000, dataType: 'json', followRedirect: true }, httpAgent: {<!-- --> keepAlive: true, freeSocketTimeout: 20000, maxSockets: Number.MAX_SAFE_INTEGER, maxFreeSockets: 256 }, httpsAgent: {<!-- --> keepAlive: true, freeSocketTimeout: 20000, maxSockets: Number.MAX_SAFE_INTEGER, maxFreeSockets: 256 } }; class HttpClient extends urllib.HttpClient2 {<!-- --> constructor(app) {<!-- --> const {<!-- -->pkg} = app.config; super({<!-- --> defaultArgs: config.defaultArgs, agent: new Agent(config.httpAgent), httpsAgent: new HttpsAgent(config.httpsAgent) }); this.app = app; this.logger = app.getLogger('httpClientLogger'); this.UA = `${<!-- -->pkg.name}/${<!-- -->pkg.version};electron/${<!-- -->electronVersion};${<!-- -->encodeURIComponent(os.hostname())};${<!-- -->urllib.USER_AGENT}`; } async request(url, options = {<!-- -->}) {<!-- --> const {<!-- -->app} = this; const {<!-- -->host} = app.config || ''; let request; options.headers = {<!-- --> "Content-Type": "application/json", referer: host, "user-agent": this.UA, ...options.headers }; const nowDate = Date.now(); let error; try {<!-- --> return request = await super.request(url, options); } catch (e) {<!-- --> error = e; error.name = 'httpError'; error.url = url; throw error; } finally {<!-- --> // 一次請求的時間差 const timestamp = Date.now() - nowDate; // logger 日誌紀錄 console.log(timestamp); if (!options.disableLogger) {<!-- --> this.logger.info([url, options.method, request && request.status, error && error.message].join("^")); } } } } module.exports = (app => {<!-- --> app.httpClient = new HttpClient(app); }) |
axios
axios 是支援在node.js的。可以在electron 主行程中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import axios from 'axios'; axios.defaults.baseURL = process.env.VUE_APP_BASE_URL; <!--強制使用node模組。--> axios.defaults.adapter = require('axios/lib/adapters/http'); // 請求攔截 設定統一header axios.interceptors.request.use( config => { return config; }, error => { console.log(error); return Promise.reject(error); } ); axios.interceptors.response.use( response => { return response; }, error => { // console.error(error); return Promise.reject(error); } ); export default axios; |
request.js
下載檔案
檔案下載,需要藉助
1 | const downloadPath = electronApp.getPath('downloads'); |
對於node http 模組封裝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | const http = require('http'); const fs = require('fs'); const downloadFileAsync = (uri, dest) => { return new Promise((resolve, reject) => { let file = ''; try { // 確保dest路徑存在,已經存在則刪除重新下載 if (fs.existsSync(dest)) { fs.unlinkSync(dest); } const path = process.platform === 'win32' ? dest.slice(0, dest.lastIndexOf('\')) : dest.slice(0, dest.lastIndexOf('/')); fs.mkdirSync(path, { recursive: true }); file = fs.createWriteStream(dest); } catch (e) { reject(e.message); } http.get(uri, (res) => { if (res.statusCode !== 200) { reject(response.statusCode); return; } res.on('end', () => { console.log('download end'); }); // 進度、超時等 file.on('finish', () => { console.log('finish write file') file.close(resolve); }).on('error', (err) => { fs.unlink(dest); reject(err.message); }) res.pipe(file); }); }); } |
對於urllib 的封裝
業務的封裝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | const {<!-- -->app: electronApp, dialog} = require('electron'); const {<!-- -->createWriteStream} = require('fs'); const {<!-- -->parse} = require('url'); const path = require('path'); const downloadFile = async (app, url) => {<!-- --> const downloadPath = electronApp.getPath('downloads'); const {<!-- -->pathname} = parse(url); const fileName = pathname.split('/').pop(); const localFilePath = path(downloadPath, fileName); const {<!-- -->canceled, filePath} = await dialog.showSaveDialog(app.mainWindow, {<!-- --> title: '儲存附件', default: localFilePath }) if (!canceled) {<!-- --> const savedFilePath = path.join(path.dirname(filePath), fileName); const writeSteam = createWriteStream(savedFilePath); const request = app.httpClient.request(url, {<!-- --> headers: {<!-- --> 'Content-Type': null }, streaming: true, followRedirect: true }) const needShowProgress = Number(request.headers['content-length']) > 1048576; const downloadResponse = (type) => {<!-- --> // progress app.mainWindow.webContents.send('download-progress', {<!-- -->type}); } request.res.on("data", data => {<!-- --> writeSteam.write(data); if (needShowProgress) {<!-- --> downloadResponse('data'); } }); request.res.on('end', () => {<!-- --> writeSteam.end(); downloadResponse('end'); }); request.res.on('error', () => {<!-- --> downloadResponse('error'); }) } }; |
客戶端日誌
這裡涉及到了,日誌的本地儲存,以及日誌的打包上傳到伺服器。
市面上現在的技術方案
- winston
- electron-log
winston
所使用到的外掛有
對於 winston 的封裝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | const path = require('path'); const winston = require('winston'); require('winston-daily-rotate-file'); const Transport = require('winston-transport'); const {<!-- -->app: electronApp} = require('electron'); const {<!-- -->format} = winston; const logReomteUrl = 'http://test.com/electron/log'; const logger = function (options = {<!-- -->}) {<!-- --> return () => {<!-- --> const logDir = options.logDir || path.join(options.debug ? process.cwd() : electronApp.getPath('userData'), 'logs'); const transportList = [ new winston.transports.DailyRotateFile({<!-- --> dirname: path.join(logDir, options.name), filename: `${<!-- -->options.filename || options.name}-TE%.log`, maxSize: '15m', maxFiles: 7, createSymlink: true, symlinkName: `${<!-- -->options.symlinkName || options.name}.log` }), new class extends Transport {<!-- --> constructor(props) {<!-- --> super(props); this.options = props; } log(options = {<!-- -->}, callback) {<!-- --> if (process.env.DISABLE_LOG_REMOTE) {<!-- --> return; } const data = {<!-- --> type: this.options.name, message: `${<!-- -->options.timestamp} ${<!-- -->options.message}` }; // 提交伺服端的日誌地址。 const url = logReomteUrl; // request app.httpClient.request(url, {<!-- --> method: 'POST', contentType: "json", data: data, disableLogger: true, }).catch(() => {<!-- --> }); callback(null, true); } }(options) ]; <!--是否同步主控臺輸出--> if (process.env.CONSOLE_LOGGER) {<!-- --> transportList.push(new winston.transports.Console); } return new winston.createLogger({<!-- --> format: format.combine( format.label({<!-- -->label: options.name}), format.timestamp({<!-- -->format: "YYYY-MM-DD HH:mm:ss"}), format.splat(), format.simple(), format.printf(({<!-- -->level, timestamp, message, label}) => {<!-- --> const {<!-- -->tracert = {<!-- -->}, currentUser = {<!-- -->}} = options.currentContext || {<!-- -->}; return [timestamp, level.toUpperCase(), `[${<!-- -->label}]`, tracert.traceId, currentUser.id, message].join("^") }) ), transports: transportList }) } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 使用 // electron 桌面端日誌 app.electronLog = logger({<!-- --> name: "electron", symlinkName: 'app', debug: app.isDev }) // 給web端呼叫的日誌 app.webLogger = logger({<!-- --> name: "web", debug: app.isDev }); |
electron-log
自行搜尋。
IPC 通訊(渲染window 發送訊息給主行程)
藉助electron 提供的
ipcMain
method
- on(channel,listener)
- once(channel,listener)
- removeListener(channel,listener)
- removeAllListeners([channel])
- handle(channel,listener)
- handleonce(channel,listener)
- removeHandler(channel)
demo
1 2 3 4 5 6 7 8 9 10 11 | // 在主行程中. const {<!-- --> ipcMain } = require('electron') ipcMain.on('asynchronous-message', (event, arg) => {<!-- --> console.log(arg) // prints "ping" event.reply('asynchronous-reply', 'pong') }) ipcMain.on('synchronous-message', (event, arg) => {<!-- --> console.log(arg) // prints "ping" event.returnValue = 'pong' }) |
ipcRenderer
method
- on(channel,listener)
- once(channel,listener)
- removeListener(channel,listener)
- removeAllListeners([channel])
- send(channel,…args)
- invoke(channel,…args)
- sendSync(channel,…args)
- postMessage(channel,message,[transfer])
- sendTo(webContentsId,channel,…args)
- sendToHost(channel,…args)
demo
1 2 3 4 5 6 7 8 | //在渲染器行程 (網頁) 中。 const {<!-- --> ipcRenderer } = require('electron') console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong" ipcRenderer.on('asynchronous-reply', (event, arg) => {<!-- --> console.log(arg) // prints "pong" }) ipcRenderer.send('asynchronous-message', 'ping') |
IPC 通訊(主行程發送訊息給渲染window)
主要藉助
1 2 | // 主行程 app.mainWindow.webContents.send('xxx','this is message'); |
1 2 3 4 5 6 | // 渲染window const {<!-- -->ipcRenderer} = require('electron'); ipcRenderer.on('xxx',(event, message)=>{<!-- --> console.log(message) // this is message }) |
設定開機啟動項
主要是透過
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | const {ipcMain, app: electronApp} = require('electron'); // 屬性回傳啟動 Node.js 行程的執行檔的絕對路徑名。 const exePath = process.execPath; module.exports = (() => { // 給渲染頁面取得當前的狀態 ipcMain.handle('get-auto-start-status', () => electronApp.getLoginItemSettings()) // 設定開啟自啟 ipcMain.on('auto-start-open', () => { electronApp.setLoginItemSettings({ openAtLogin: true, path: exePath, args: [] }) }); //設定開機不自啟 ipcMain.on('auto-start-closed', () => { electronApp.setLoginItemSettings({ openAtLogin: false, path: exePath, args: [] }) }) }); |
其他應用喚醒客戶端
主要是藉助
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const {app: electronApp, dialog} = require('electron'); const path = require('path'); const SCHEMA_NAME = 'xx'; // 協定前置詞 if (process.defaultApp) { if (process.argv.length >= 2) { electronApp.setAsDefaultProtocolClient(SCHEMA_NAME, process.execPath, [path.resolve(process.argv[1])]); } } else { electronApp.setAsDefaultProtocolClient(SCHEMA_NAME); } electronApp.on('open-url', ((event, url) => { dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); })); |
全域快速鍵
藉助
1 2 3 4 5 6 7 8 | const {<!-- -->globalShortcut} = require("electron"); module.exports = (app => {<!-- --> // 註冊全域快速鍵 globalShortcut.register("CommandOrControl+Option+Y", () => {<!-- --> app.mainWindow.show() }) }); |
托盤
需要考慮window 系統和macOS 系統,對於macOS 還需要考慮是不是暗黑模式。
對於mac 還有
1 2 3 | const macLightIcon = path.join(__dirname, "../../../dist-assets/tray/[email protected]"); const macDarkIcon = path.join(__dirname, "../../../dist-assets/tray/[email protected]"); const winLightIcon = path.join(__dirname, "../../../dist-assets/tray/tray-windows.png"); |
mac 應用選單
對於mac 端的左上角也有一個應用的選單,主要是透過
對於
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | const tabs = [ {<!-- --> label: 'Application', submenu:[ {<!-- --> label:'xxxx', accelerator: "CommandOrControl+,", click:()=>{<!-- --> // } }, {<!-- -->type: "separator"}, // 一根線 {<!-- --> label:'xxx2', accelerator: "CommandOrControl+Q", click:()=>{<!-- --> // } } ] } ] |
國際化
預設提供的是中文,然後透過設定檔,來取得到其他語言的翻譯
1 2 3 4 5 6 7 8 9 | // en-US module.exports = {<!-- --> "測試": "Test", "關於xx": "About xx", "退出": "Quit", "除錯": "Debug", "視窗": "Window", "托盤":'Tray' }; |
然後在app 方法裡面繫結一個方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const en = require('../locales/en-US'); // 匹配一個單字字元(字母、數值或者下劃線)。等效於 [A-Za-z0-9_]。 const interpolate_reg = /{(w*)}/g; // replace data const replaceData = (key, lang) => {<!-- --> return key.replace(interpolate_reg, value => {<!-- --> const tempKey = value.slice(1, value.length - 1); return lang[tempKey] ? lang[tempKey] : key; }) }; module.exports = (app => {<!-- --> // 繫結 t 方法。 app.t = ((title, lang = {<!-- -->}) => {<!-- --> // 繫結到app 上面的語言。 if (app.locale.startsWith('zh')) {<!-- --> return replaceData(title, lang); } const enLang = en[title]; return enLang ? replaceData(enLang, lang) : title; }) }); |
然後在呼叫的時候。
1 | app.t('關於xx') //這樣就會根據不同的語言情況。取得到不同翻譯了。 |
然後在啟動專案的時候,渲染執行緒,拉取桌面端的語言,然後根據語言更新window 端的語言。
// 在主執行緒裡面
1 2 3 4 5 6 7 8 9 | ipcMain.handle('get-app-config', async () => { const locale = await app.getLocale(); return { timestamp: (new Date).getTime(), locale: locale, ...app.config } }); |
1 2 3 4 5 6 7 8 9 10 11 | // 在渲染window 裡面 import {isElectron, getAppConfig, addEventListener, EVENTS} from "./utils/electron"; async created() { if (isElectron) { const appConfig = await getAppConfig(); console.log(appConfig); // this.$store.commit('UPDATE_ELECTRON_CONFIG', appConfig); } }, |
TouchBar
主要藉助
每個
最後透過
硬體加速(mac)
mac版本的的electron 可能會出現花屏現狀,方案就是關閉硬體加速。
透過
模式(development、production)
透過注入
在啟動electron的時候
1 2 3 4 5 | {<!-- --> "scripts":{<!-- --> "dev":"cross-env NODE_ENV=development electron ." } } |
這樣就可以在程式碼裡面透過
1 | this.isDev = "development" === process.env.NODE_ENV; |
崩潰日誌發送
1 2 3 4 5 6 7 8 9 10 | const {<!-- -->crashReporter} = require('electron'); crashReporter.start({<!-- --> productName: "test", companyName: "test", submitURL: "https://www.test.com", autoSubmit: true, uploadToServer: true, ignoreSystemCrashHandler: true }); |
獨體模式
透過
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | const singleInstanceLock = electronApp.requestSingleInstanceLock(); if (singleInstanceLock) {<!-- --> // electronApp.on('second-instance', () => {<!-- --> // 檢視開啟的是否是 login window 還是 main window app.loginWindow && !app.loginWindow.isDestroyed() ? (app.loginWindow.isVisible() || app.loginWindow.show(), app.loginWindow.isMinimized() && app.loginWindow.restore(), app.loginWindow.focus()) : app.mainWindow && (app.mainWindow.isVisible() || app.mainWindow.show(), app.mainWindow.isMinimized() && app.mainWindow.restore(), app.mainWindow.focus()) }); // 監聽 ready 事件 electronApp.on('ready', async () => {<!-- --> <!--app 的初始化操作--> await app.init(); <!--喚起登入,自動登入,選擇登入。啥的--> app.launchLogin(); }); } else {<!-- --> electronApp.quit(); } |
白屏
在專案啟動的時候,需要先用 loading.html 頁面載入,防止出現白屏現象。
依賴
新建完 BrowserWindow 之後,利用BrowserView 載入loading頁面,監聽BrowserWindow.webContens的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | const {<!-- -->BrowserView, BrowserWindow} = require('electron'); const browserWindow = new BrowserWindow({<!-- --> width: 500, height: 600, // 其他引數 }); const loadingBrowserView = new BrowserView(); browserWindow.setBrowserView(loadingBrowserView); loadingBrowserView.setBounds({<!-- --> x: 0, y: 0, width: 400, height: 600 }); loadingBrowserView.webContents.loadURL('loading.html'); browserWindow.webContents.on('will-navigate', () => {<!-- --> browserWindow.setBrowserView(loadingBrowserView); }); browserWindow.webContents.on('dom-ready', async (event) => {<!-- --> browserWindow.removeBrowserView(loadingBrowserView); }); |
electron bridge
透過在
在頁面執行其他腳本之前預先載入指定的腳本 無論頁面是否整合Node, 此腳本都可以訪問所有Node API 腳本路徑為檔案的絕對路徑。 當 node integration 關閉時, 預載入的腳本將從全域範圍重新引入node的全域引用標誌
透過這種方式,就可以實現相容
BrowserWindow http請求攔截
透過監聽
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 需要攔截的請求。 const webRequestFilter = { urls: ["*://test.aaa.com/*", "*://*.ccc.com/*"] }; browserWindow.webContents.session.webRequest.onBeforeRequest(webRequestFilter, (details, callback) => { // 監聽 before request // 是否存在下載 if (details.url.includes('desktop-download')) { downloadFile(app, details.url); } // 原始請求被阻止發送或完成,而不是重新導向到給定的URL callback({redirectURL: details.redirectURL}); }); browserWindow.webContents.session.webRequest.onBeforeSendHeaders(webRequestFilter, (details, callback) => { // 繫結header 頭部。 if (details.requestHeaders.Cookie) { const {ctoken = ''} = cookieParse(details.requestHeaders.Cookie); if (ctoken) { details.requestHeaders['x-csrf-token'] = ctoken; } } // When provided, request will be made with these headers. callback({requestHeaders: details.requestHeaders}); }); |
整合 vue 、react
vue
主要透過
react
主要是透過
無frame
只需要在建立
1 2 3 | const { BrowserWindow } = require('electron') const win = new BrowserWindow({ width: 800, height: 600, frame: false }) win.show() |
window
就需要自定義實現右上角最大、最小、關閉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <header class="drag-area"> <div class="header-actions"> <div @click="handleMinWindow"> <svg t="1586443316286" className="icon-min" viewBox="0 0 1024 1024" version="1.1"> <defs> <style type="text/css"></style> </defs> <path d="M65.23884 456.152041 958.760137 456.152041l0 111.695918L65.23884 567.847959 65.23884 456.152041z" p-id="1094"></path> </svg> </div> <div @click="handleUnMaxWindow" v-if="isWinMax"> <svg t="1586445181598" className="icon" viewBox="0 0 1157 1024" version="1.1"> <defs> <style type="text/css"></style> </defs> <path d="M1086.033752 753.710082 878.220684 753.710082 878.220684 951.774989 878.220684 1021.784509 878.220684 1023.113804 808.211164 1023.113804 808.211164 1021.784509 70.895716 1021.784509 70.895716 1023.113804 0.886196 1023.113804 0.886196 1021.784509 0.886196 951.774989 0.886196 339.413241 0.886196 269.403721 70.895716 269.403721 269.403721 269.403721 269.403721 0.886196 274.277802 0.886196 339.413241 0.886196 1086.033752 0.886196 1151.612289 0.886196 1156.043271 0.886196 1156.043271 683.700563 1156.043271 753.710082 1086.033752 753.710082ZM70.895716 951.774989 808.211164 951.774989 808.211164 753.710082 808.211164 683.700563 808.211164 339.413241 70.895716 339.413241 70.895716 951.774989ZM1086.033752 70.895716 339.413241 70.895716 339.413241 269.403721 808.211164 269.403721 878.220684 269.403721 878.220684 339.413241 878.220684 683.700563 1086.033752 683.700563 1086.033752 70.895716Z" p-id="2415"></path> </svg> </div> <div @click="handleMaxWindow" v-else> <svg t="1586443335243" className="icon-max" viewBox="0 0 1024 1024" version="1.1"> <defs> <style type="text/css"></style> </defs> <path d="M128.576377 895.420553 128.576377 128.578424l766.846222 0 0 766.842129L128.576377 895.420553zM799.567461 224.434585 224.432539 224.434585l0 575.134923 575.134923 0L799.567461 224.434585z" p-id="1340"></path> </svg> </div> <div @click="handleCloseWindow"> <svg t="1586443316286" className="icon-close" viewBox="0 0 1024 1024" version="1.1"> <path d="M895.423623 224.432539 607.855138 512l286.901289 297.699216 0.666172 85.723384-95.856162 0L512 607.856162 224.432539 895.423623l-95.856162 0 0-95.856162 287.567461-287.567461L128.576377 224.432539l0-95.856162 95.856162 0 287.567461 287.567461 287.567461-287.567461 95.856162 0L895.423623 224.432539z" p-id="1217"></path> </svg> </div> </div> </header> |
1 2 3 4 5 6 7 | .drag-area {<!-- --> -webkit-app-region: drag; -webkit-user-select: none; user-select: none; z-index: 500; width: 100vw; } |
mac
頂部需要設定一條可以拖拽的區域
1 2 3 4 5 6 7 8 9 10 11 | .drag-area {<!-- --> -webkit-app-region: drag; -webkit-user-select: none; user-select: none; z-index: 500; width: 100vw; background-color: transparent; height: 18px; position: fixed; } |
設定DOM拖拽聯通視窗拖拽
1 2 3 4 5 6 | .drag-area {<!-- --> -webkit-app-region: drag; // 支援視窗拖拽 -webkit-user-select: none; user-select: none; z-index: 500; } |
代理設定
待定。。。
打包釋出
主要的打包方案 如下:
- electron-builder
- electron-packager
electron-builder
透過在 package.json 裡面配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | {<!-- --> "scripts":{<!-- --> "build":"electron-builder" } "build":{<!-- --> "productName":"productName", "appId":"appId", "directories":{<!-- --> "output": "output" }, "files":[ ], "nsis":{<!-- --> }, "dmg":{<!-- --> }, "mac":{<!-- --> }, "win":{<!-- --> }, "linux":{<!-- --> } } } |
electron-packager
打包引數:
1 | electron-packager <sourcedir> <appname> <platform> <architecture> <electron version> <optional options> |
- sourcedir:專案所在路徑
- appname:應用名稱
- platform:確定了你要構建哪個平台的應用(Windows、Mac 還是 Linux)
- architecture:決定了使用 x86 還是 x64 還是兩個架構都用
- electron version:electron 的版本
- optional options:可選選項
優缺點
1、支援平台有:Windows (32/64 bit)、OS X (also known as macOS)、Linux (x86/x86_64);
2、進行應用更新時,使用electron內建的autoUpdate進行更新
3、支援CLI和JS API兩種使用方式;
專案地址
為此,我將業務系統裡面用到的各個客戶端需要的功能,剝離了出來,新建了一個template 方便新業務系統的開發。
編碼不易,歡迎star
https://github.com/bosscheng/electron-app-template
github