bg

@[TOC]

一、使用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桌面客户端