Electron - 跨平台桌面应用开发工具的使用总结

一、使用electron-vite新建项目

  • 1、npm命令 npm create @quick-start/electron
  • 2、yarn命令 yarn create @quick-start/electron
  • 3、electron镜像地址:
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
  • 4、填写项目名称
? Project name: electron-vue-app 
  • 5、选择vue框架:
? Select a framework: 
    vanilla
>   vue
    react
    svelte
    solid
  • 6、其他配置项
? Add TypeScript? » No / Yes   
Yes
? Add Electron updater plugin? » No / Yes 
Yes
? Enable Electron download mirror proxy? » No / Yes
Yes

二、目录结构

├─ /.vscode
├─ /build
├─ /node_modules
├─ /out                         # 运行时的输出目录
├─ /resources                   # 主进程和预加载脚本资源文件目录
├─ /src
|  ├─ /main                     # Electron主进程
|  ├─ /preload                  # Electron预加载脚本
|  └─ /renderer                 # Electron渲染进程, vue常规目录结构
|  |  ├─ /assets
|  |  ├─ /components
|  |  ├─ /views
|  |  ├─ App.vue
|  |  ├─ main.js
|  |  └─ index.html
├─ .editorconfig
├─ .eslintignore
├─ .eslintrc.cjs
├─ .gitignore
├─ .npmrc
├─ .prettierignore
├─ .prettierrc.yaml
├─ dev-app-update.yml
├─ electron-builder.yml
├─ electron.vite.config.mjs
├─ package.json
├─ README.md

三、渲染进程调用主进程

1、方式一 —— 允许有返回值

· src/main/index.js
import { app, ipcMain, BrowserWindow } from 'electron'
app.whenReady().then(() => {
	// ...

	new BrowserWindow({
        width: 800,
        height: 600,
        // 其他窗口配置...
    })
	
    ipcMain.handle('renderderCallMain', async (event, value) => {
        console.log('renderder call main...', value)
    })

	// ...
})
· src/preload/index.js
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
if (process.contextIsolated) {
	try {
		contextBridge.exposeInMainWorld('electron', electronAPI)
	} catch (error) {
		console.error(error)
	}
} else {
	window.electron = electronAPI
}
· src/renderer/index.html
window.electron.ipcRenderer.invoke('renderderCallMain', 'hello world!')

2、方式二—— 允许有返回值 (推荐写法)

· src/main/index.js
import { app, ipcMain, BrowserWindow } from 'electron'
app.whenReady().then(() => {
	// ...

	new BrowserWindow({
        width: 800,
        height: 600,
        // 其他窗口配置...
    })
	
    ipcMain.handle('renderderCallMain', async (event, value) => {
        console.log('renderder call main...', value)
    })

	// ...
})
· src/preload/index.js
import { contextBridge, ipcRenderer } from 'electron'

const call = {
	renderderCallMain: (value) => ipcRenderer.invoke('renderderCallMain', value),
}

if (process.contextIsolated) {
	try {
		contextBridge.exposeInMainWorld('call', call)
	} catch (error) {
		console.error(error)
	}
} else {
	window.call = call
}
· src/renderer/index.html
window.call.renderderCallMain('hello world!')

3、方式三 —— 无返回值,不等待响应

· src/main/index.js
import { app, ipcMain, BrowserWindow } from 'electron'
app.whenReady().then(() => {
	// ...

	new BrowserWindow({
        width: 800,
        height: 600,
        // 其他窗口配置...
    })
	
    ipcMain.on('ping', () => console.log('pong'))

	// ...
}
· src/preload/index.js
import { contextBridge, ipcRenderer } from 'electron'

const call = {
	ping: () => ipcRenderer.send('ping'),
}

if (process.contextIsolated) {
	try {
		contextBridge.exposeInMainWorld('call', call)
	} catch (error) {
		console.error(error)
	}
} else {
	window.call = call
}
· src/renderer/index.html
window.call.ping()

四、主进程触发渲染进程

· src/main/index.js

import { app, globalShortcut, BrowserWindow } from 'electron'
app.whenReady().then(() => {
	// ...

	const mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        // 其他窗口配置...
    })

    globalShortcut.register('CommandOrControl+S', () => {
		mainWindow.webContents.send('commandOrControlS', 'command or control + s...')
	})

	// ...
})

· src/preload/index.js

import { contextBridge, ipcRenderer } from 'electron'
const listen = {
	commandOrControlS: (callback) => ipcRenderer.on('commandOrControlS', async (event, value) => callback(value)),
}

if (process.contextIsolated) {
	try {
		contextBridge.exposeInMainWorld('listen', listen)
	} catch (error) {
		console.error(error)
	}
} else {
	window.listen = listen
}

· src/renderer/index.html

window.listen.commandOrControlS((value) => {
    console.log(value)
})

五、功能介绍

1、透明窗口设置

const mainWindow = new BrowserWindow({
    width: 80,
    height: 162,
    frame: false,                   // 透明窗口
    transparent: true,              // 透明窗口
    backgroundColor: '#00000000',   // 透明窗口
    autoHideMenuBar: true,          // 透明窗口
})

2、系统截图功能

import { desktopCapturer } from 'electron'

async function desktopCapturerHandle() {
    const sources = await desktopCapturer.getSources({
        types: ['window'],
        thumbnailSize: {
            width: 1920,
            height: 1080,
        },
    })

    return sources[0]?.thumbnail.toDataURL('image/png'),
}

3、系统托盘图标及闪烁功能

import { app, BrowserWindow, Tray, Menu } from 'electron'

let flickerTimer = null

app.whenReady().then(() => {
	// ...

	const mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        // 其他窗口配置...
    })
	
    const tray = new Tray('./icon.ico')
	const contextMenu = Menu.buildFromTemplate([
		{ label: '显示       ', click: () => {
			mainWindow.show()
			stopTray(tray)
		}},
		{ type: 'separator' },
		{ label: '退出       ', click: () => {
			mainWindow.off('close', closeHandle)
			app.quit()
		}},
	])
	tray.setContextMenu(contextMenu)
	tray.on('double-click', function() {
		mainWindow.show() // 点击图标时恢复窗口
		stopTray(tray)
	})
	tray.on('click', function() {
		if (flickerTimer) {
			mainWindow.show() // 点击图标时恢复窗口
			stopTray(tray)
		}
	})

	// ...
})

function flickerTray(tray) {
	if (!flickerTimer) {
		let hasIco = false
		flickerTimer = setInterval(() => {
            // 图标与透明图标切换以实现闪烁功能
			tray.setImage(hasIco ? './icon.ico' : './empty.ico')
			hasIco = !hasIco
		}, 500)
	}
}

function stopTray(tray) {
	if (flickerTimer) {
		clearInterval(flickerTimer)
		flickerTimer = null
	}
	tray.setImage('./icon.ico')
}

六、问题处理

1、图标打包路径无法获取问题

· 修改图标路径获取
import { app } from 'electron'
const path = require('path')

function resolvePath() {
	const publicPath = 'build'
    const fileName = 'icon.ico'
	if (process.env.NODE_ENV === 'development') {
        return './' + publicPath + '/' + fileName
	}
	return path.join(path.dirname(app.getPath('exe')), '/resources/' + publicPath + '/' + 'fileName')
}
· package.json 配置图标打包
{
    ...

    "build": {
        "extraResources": [
            {
                "from": "./build",
                "to": "./build"
            }
        ]
    }

    ...
}

图标大小需 256x256,格式为ico

2、允许更改安装目录

· package.json 配置
{
    ...

    "build": {
        "nsis": {
            "allowToChangeInstallationDirectory": true, // 允许更改安装目录
            "installerIcon": "./build/icon.ico",        // 安装图标
            "uninstallerIcon": "./build/icon.ico",      // 卸载图标
            "installerHeaderIcon": "./build/icon.ico",  // 安装程序头部图标
            "createDesktopShortcut": true,              // 创建桌面快捷方式
            "createStartMenuShortcut": false,           // 加入开始菜单
        }
    }

    ...
}

3、loadFile添加参数

import { app, BrowserWindow } from 'electron'
import { join } from 'path'

app.whenReady().then(() => {
	// ...

	const mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        // 其他窗口配置...
    })
	
    mainWindow.loadFile(join(__dirname, '../renderer/index.html'), {
        search: 'id=test',
    })

	// ...
})

4、http数据请求与cookie设置

  • 1)、由于 fill:// 协议无法设置cookie,所以请求只能在主进程发起
  • 2)、使用Chromium的原生网络库发出HTTP / HTTPS请求 或 Node.js的HTTP 和 HTTPS 模块
const { app } = require('electron')

app.whenReady().then(() => {
    const { net } = require('electron')
    const request = net.request('https://github.com')
    request.on('response', (response) => {
        console.log(response)
        response.on('data', (chunk) => {
            console.log('BODY: ' + chunk)
        })
        response.on('end', () => {
            console.log('No more data in response.')
        })
    })
    request.end()
})
  • 3)、从请求结果中 set-cookie 获取cookie并返回给渲染端进行存储

5、iframe CSP

<meta 
    http-equiv="Content-Security-Policy"
    content="script-src 'self' https://example.com; img-src https://example.com"
/>

修改html中的meta属性,这将允许在iframe中加载来自 selfexample.com 域的脚本,并允许加载来自 example.com 域的图像

6、多页面打包

· electron.vite.config.mjs
export default defineConfig({
	renderer: {
		build: {
			rollupOptions: {
				input: {
					index: './src/renderer/index.html',
					other: './src/renderer/other.html',
				}
			}
		}
	}
})

7、多预渲染脚本打包

· electron.vite.config.mjs
export default defineConfig({
	preload: {
		build: {
			lib: {
				entry: ['src/preload/index', 'src/preload/iframe'],
			},
		},
	},
})

8、路由切换主进程代码多次执行

检查 ipcRenderer.on() 路由切换后是否多次在 onMounted 注册,需在 onBeforeUnmount 中及时调用主进程 ipcRenderer.removeListener() 注销事件的监听

七、npm库

  • 1、"@jitsi/robotjs": "^0.6.11" 适用于自动化工具,有控制I/O、系统截图等功能
  • 2、"tesseract.js": "^5.0.5" 基于js的图像识别
  • 3、"jimp": "^0.22.12" 图像处理工具,有图片裁剪、保存、图片大小更改等功能
  • 4、"nodemailer": "^6.9.13" 基于node的邮件发送工具

References

[1] Electron文档
[2] 基于electron-vite构建Vue桌面客户端

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值