系列
Electron + Vue3开源跨平台壁纸工具实战(二)本地运行
Electron + Vue3开源跨平台壁纸工具实战(三)主进程
Electron + Vue3开源跨平台壁纸工具实战(四)主进程-数据管理(1)
Electron + Vue3开源跨平台壁纸工具实战(五)主进程-数据管理(2)
Electron + Vue3开源跨平台壁纸工具实战(六)子进程服务
Electron + Vue3开源跨平台壁纸工具实战(七)进程通信
Electron + Vue3开源跨平台壁纸工具实战(八)主进程-核心功能
Electron + Vue3开源跨平台壁纸工具实战(九)子进程服务(2)
Electron + Vue3开源跨平台壁纸工具实战(十)渲染进程
源码
省流点我进Github获取源码,欢迎fork、star、PR
主进程
目录结构
├── main/ # Electron主进程
│ ├── child_server/ # 子进程服务
│ ├── jobs/ # 定时任务
│ ├── store/ # 数据存储
│ ├── utils/ # 工具函数
│ ├── ApiBase.js # API基类
│ ├── cache.mjs # 缓存管理
│ ├── logger.mjs # 日志系统
│ ├── updater.mjs # 自动更新
│ └── index.mjs # 主进程入口
环境变量
在主进程中初始化定义了一些应用中会使用到的目录变量,此处注意resources目录在打包后路径会变化。
// main/index.mjs
const userDataPath = app.getPath('userData')
// 目录
process.env.FBW_USER_DATA_PATH = userDataPath
process.env.FBW_LOGS_PATH = getDirPathByName(userDataPath, 'logs')
process.env.FBW_DATABASE_PATH = getDirPathByName(userDataPath, 'database')
process.env.FBW_DATABASE_FILE_PATH = path.join(process.env.FBW_DATABASE_PATH, 'fbw.db')
process.env.FBW_DOWNLOAD_PATH = getDirPathByName(userDataPath, 'download')
process.env.FBW_CERTS_PATH = getDirPathByName(userDataPath, 'certs')
process.env.FBW_PLUGINS_PATH = getDirPathByName(userDataPath, 'plugins')
process.env.FBW_TEMP_PATH = getDirPathByName(userDataPath, 'temp')
// 获取资源路径,开发环境下使用项目根目录下的资源,生产环境下使用 resources 目录下的资源
process.env.FBW_RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'resources')
: path.join(__dirname, '../../resources')
全局变量
挂载全局变量,主要用于自定义插件扩展时使用
// main/index.mjs
// 全局变量
global.FBW = global.FBW || {}
// 导出API开发所需的工具和基类
global.FBW.apiHelpers = {
axios,
ApiBase,
calculateImageOrientation,
calculateImageQuality
}
logger
日志系统使用pino,主要是看重了性能和JSON格式,便于后期处理。
- pino-pretty 用于在DEV环境输出日志到控制台
- pino-roll 用于在PROD环境输出日志到文件
logger初始化
// main/logger.mjs
import { join } from 'path'
import pino from 'pino'
import { isDev } from './utils/utils.mjs'
export default (logDir, fileName = 'app.log') => {
// 获取用户数据目录
const logFilePath = join(logDir || process.env.FBW_LOGS_PATH, fileName)
// 配置 pino-roll 传输
const transport = pino.transport(
isDev()
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
}
: {
target: 'pino-roll',
options: {
file: logFilePath, // 日志文件路径
frequency: 'daily', // 每天生成一个新的日志文件
size: '10M', // 每个日志文件的最大大小
mkdir: true, // 自动创建目录
dateFormat: 'yyyy-MM-dd' // 自定义日志文件名称,eg: app.log.2025-02-27.1
}
}
)
// 创建 Pino 实例
const logger = pino(transport)
// 初始化日志 挂载全局变量
global.logger = logger
}
在子进程服务中,通过port.postMessage、port.on('message', () => {...}进行消息传递,再在主进程中onMessage监听消息并进行日志打印
输出日志
{"level":30,"time":1747106322108,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"isDev: false process.env.NODE_ENV: undefined"}
{"level":30,"time":1747106322108,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"getIconPath FBW_RESOURCES_PATH: /Applications/Flying Bird Wallpaper.app/Contents/Resources/resources"}
{"level":30,"time":1747106322108,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"getIconPath resourcesPath: /Applications/Flying Bird Wallpaper.app/Contents/Resources"}
{"level":30,"time":1747106322657,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"数据库初始化完成"}
{"level":30,"time":1747106322658,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"开始加载API插件,目录: /Applications/Flying Bird Wallpaper.app/Contents/Resources/resources/api"}
{"level":30,"time":1747106322742,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"成功加载API插件: bing"}
{"level":30,"time":1747106322742,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"成功加载API插件: nasa"}
{"level":30,"time":1747106322743,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"成功加载API插件: pexels"}
{"level":30,"time":1747106322743,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"成功加载API插件: pixabay"}
{"level":30,"time":1747106322743,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"成功加载API插件: unsplash"}
{"level":30,"time":1747106322744,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"成功加载API插件: birdpaper"}
{"level":30,"time":1747106322744,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"开始加载API插件,目录: /Users/OXOYO/Library/Application Support/Flying Bird Wallpaper/plugins/api"}
{"level":30,"time":1747106322744,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"成功加载 6 个API插件"}
{"level":30,"time":1747106322746,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"写入resourceMap信息成功"}
{"level":30,"time":1747106322746,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"ChildServer INIT:: serverName => fileServer, serverPath => /Applications/Flying Bird Wallpaper.app/Contents/Resources/app.asar/out/main/index-BA8g1k-3.js"}
{"level":30,"time":1747106322746,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"ChildServer INIT:: serverName => h5Server, serverPath => /Applications/Flying Bird Wallpaper.app/Contents/Resources/app.asar/out/main/index-Bhlql4S6.js"}
{"level":30,"time":1747106322750,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"ChildServer START:: serverName => fileServer"}
{"level":30,"time":1747106322866,"pid":60161,"hostname":"OXOYOMacBook-Pro.local","msg":"Store 初始化完成"}
异常处理
在主进程中全局捕获异常
// main/index.mjs
// 捕获未处理的异常
process.on('uncaughtException', (err) => {
// 获取错误的名称、消息和堆栈信息
const errorMessage = err.message // 错误消息
const errorName = err.name // 错误名称
const errorStack = err.stack // 堆栈信息
console.log(
`Uncaught Exception: Name => ${errorName}, Message => ${errorMessage}, Stack => ${errorStack}`
)
// 打印详细错误信息
global.logger.error(
`Uncaught Exception: Name => ${errorName}, Message => ${errorMessage}, Stack => ${errorStack}`
)
})
// 捕获未处理的 Promise 拒绝
process.on('unhandledRejection', (reason) => {
if (reason instanceof Error) {
// reason 是 Error 对象时,打印详细错误信息
const errorName = reason.name
const errorMessage = reason.message
const errorStack = reason.stack
console.log(
`Uncaught Rejection: Name => ${errorName}, Message => ${errorMessage}, Stack => ${errorStack}`
)
global.logger.warn(
`Uncaught Rejection: Name => ${errorName}, Message => ${errorMessage}, Stack => ${errorStack}`
)
} else {
console.log(`Unhandled Rejection: ${reason}`)
// reason 不是 Error 对象时,直接打印 reason
global.logger.warn(`Unhandled Rejection: ${reason}`)
}
})
自定义协议
注册自定义协议,用于处理在渲染进程中加载图片
// main/index.mjs
// 在 app.whenReady() 之前
protocol.registerSchemesAsPrivileged([
{
scheme: 'fbwtp',
privileges: {
bypassCSP: true,
secure: true,
standard: true,
supportFetchAPI: true,
allowServiceWorkers: true,
stream: true
}
}
])
处理自定义协议,使用RESTFUL规范来统一请求,方便请求处理及后续扩展
// main/index.mjs
// 处理自定义协议
const handleProtocol = () => {
protocol.handle('fbwtp', async (request) => {
const url = new URL(request.url)
switch (url.pathname) {
// 处理图片请求
case '/api/images/get': {
const filePath = url.searchParams.get('filePath')
const w = url.searchParams.get('w')
const h = url.searchParams.get('h')
const res = await handleFileRsponse({ filePath, w, h })
return new Response(res.data, {
status: res.status,
headers: res.headers
})
}
case '/api/videos/get': {
const filePath = url.searchParams.get('filePath')
const res = await handleFileRsponse({ filePath })
return new Response(res.data, {
status: res.status,
headers: res.headers
})
}
}
})
}
这里提取统一的文件响应处理逻辑,便于H5\APP复用处理逻辑,共用数据缓存。
- 使用lru-cache提高响应速度避免每次都读取文件;通过sharp剪切图片,避免每次都加载原图。
- 使用
filePath=${filePath}&width=${width}作为缓存Key,便于通过width控制相同文件是否压缩以及压缩效果 - 响应头添加Server-Timing便于在devtools中查看请求处理耗时
// main/utils/file.mjs
import fs from 'fs'
import path from 'path'
import sharp from 'sharp'
import cache from '../cache.mjs'
import { mimeTypes } from '../../common/publicData'
export const handleFileRsponse = async (query) => {
const ret = {
status: 404,
headers: {},
data: null
}
try {
let T1, T2, T3, T4
T1 = Date.now()
// 获取图片 URL 和尺寸
let { filePath, w } = query
if (!filePath) {
return ret
}
// 处理 Windows 上的绝对路径(例如 'E:/xx/yy')
if (process.platform === 'win32') {
filePath = filePath.replace(/\//g, '\\') // 将所有斜杠替换为反斜杠
// 修复丢失的冒号(:),假设路径是 e\xx\yy 应该是 e:\xx\yy
filePath = filePath.replace(/^([a-zA-Z])\\/, '$1:\\') // 在盘符后面补上冒号
} else {
// macOS 和 Linux 确保是绝对路径
if (!filePath.startsWith('/')) {
filePath = '/' + filePath
}
}
filePath = decodeURIComponent(filePath)
// 定义默认图片尺寸
const width = w ? parseInt(w, 10) : null
// 生成缓存键
const cacheKey = `filePath=${filePath}&width=${width}`
// 检查缓存
if (cache.has(cacheKey)) {
const cacheData = cache.get(cacheKey)
// 返回文件内容和 MIME 类型
ret.status = 200
ret.headers = {
...cacheData.headers,
'Server-Timing': `cache-hit;dur=${Date.now() - T1}`
}
ret.data = cacheData.data
return ret
}
T2 = Date.now()
// 获取文件信息
const stats = await fs.promises.stat(filePath)
const originalFileSize = stats.size
const extension = path.extname(filePath).toLowerCase()
const mimeType = mimeTypes[extension] || 'application/octet-stream'
T3 = Date.now()
// 读取文件并处理
let fileBuffer
// 只对图片进行调整大小,视频文件直接读取
// 文件大小超过指定大小才进行调整,单位bytes
const limitSize = 5 * 1024 * 1024
if (
['.png', '.jpg', '.jpeg', '.avif', '.webp', '.gif'].includes(extension) &&
width &&
originalFileSize > limitSize
) {
fileBuffer = await sharp(filePath)
.resize({
width,
fit: 'inside', // 保持宽高比
withoutEnlargement: true, // 避免放大小图片
kernel: 'lanczos3', // 使用最好的缩放算法
fastShrinkOnLoad: true // 启用快速缩小
})
.toBuffer()
} else {
fileBuffer = await fs.promises.readFile(filePath)
}
const fileSize = fileBuffer.length.toString()
T4 = Date.now()
const headers = {
'Accept-Ranges': 'bytes',
'Content-Type': mimeType,
'Original-Size': originalFileSize,
'Content-Length': fileSize,
'Compressed-Size': fileSize,
'Cache-Control': 'max-age=3600',
ETag: `"${stats.mtimeMs}-${originalFileSize}"`,
'Last-Modified': stats.mtime.toUTCString(),
'Server-Timing': `file-check;dur=${T2 - T1}, file-stat;dur=${T3 - T2}, resize;dur=${T4 - T3}, total;dur=${T4 - T1}`,
'X-File-Check-Time': T2 - T1 + 'ms',
'X-File-Stat-Time': T3 - T2 + 'ms',
'X-Resize-Time': T4 - T3 + 'ms',
'X-Total-Time': T4 - T1 + 'ms'
}
// 缓存文件内容
cache.set(cacheKey, {
data: fileBuffer,
headers
})
// 返回文件内容
ret.status = 200
ret.headers = headers
ret.data = fileBuffer
return ret
} catch (err) {
return ret
}
}
使用时需在index.html中将fbwtp协议头加入,以通过内容安全策略(CSP)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title></title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: gap: content: https://xxx.com;media-src * blob: 'self' http://* 'unsafe-inline' 'unsafe-eval';style-src * 'self' 'unsafe-inline';img-src * 'self' data: content: fbwtp:;connect-src * blob:;"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="main.js"></script>
</body>
</html>
在渲染进程中使用时就和网络图片一样可以直接使用了
<img src="fbwtp://fbw/api/images/get?filePath=${item.filePath}&w=100"
应用单例
// main/index.mjs
// 确保单实例
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
// 如果没拿到锁,直接退出当前实例
app.quit()
} else {
app.on('second-instance', () => {
// 当运行第二个实例时,这里将会被调用
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
if (!mainWindow.isVisible()) {
mainWindow.show()
}
mainWindow.focus()
}
})
应用托盘
这里使用setTemplateImage可以在MAC适配适应明暗菜单栏
// main/index.mjs
// 创建托盘
const createTray = () => {
const trayIcon = nativeImage.createFromPath(iconTray).resize({ width: 20, height: 20 })
trayIcon.setTemplateImage(true) // 设置为模板图标
tray = new Tray(trayIcon)
tray.setToolTip(t('appInfo.name'))
// 监听托盘图标点击事件
tray.on('click', () => {
toggleMainWindow()
})
// 监听托盘图标点击事件右键点击事件
tray.on('right-click', () => {
// 托盘右键菜单
const contextMenuList = [
{
label: store?.settingData?.autoSwitchWallpaper
? t('actions.autoSwitchWallpaper.stop')
: t('actions.autoSwitchWallpaper.start'),
click: () => {
store?.toggleAutoSwitchWallpaper()
}
},
{
label: t('actions.nextWallpaper'),
click: () => {
store?.doManualSwitchWallpaper('next')
}
},
{
label: t('actions.prevWallpaper'),
click: () => {
store?.doManualSwitchWallpaper('prev')
}
},
{
label: store?.settingData?.suspensionBallVisible
? t('actions.closeSuspensionBall')
: t('actions.openSuspensionBall'),
click: () => {
toggleSuspensionBall()
}
}
]
contextMenuList.push({
type: 'separator'
})
// 菜单子项
const enabledMenuChildren = []
// 功能菜单
const trayFuncMenuList = []
menuList.forEach((item) => {
if (item.placement.includes('trayMenuChildren')) {
enabledMenuChildren.push({
// FIXME 拼接空格,防止菜单项宽度过短
label: t(item.locale).padEnd(20, ' '),
click: () => handleJumpToPage(item.name)
})
} else if (item.placement.includes('trayFuncMenu')) {
trayFuncMenuList.push({
label: t(item.locale),
click: () => handleJumpToPage(item.name)
})
}
})
if (enabledMenuChildren.length) {
contextMenuList.push({
label: t('actions.menu'),
submenu: enabledMenuChildren
})
contextMenuList.push({
type: 'separator'
})
}
if (trayFuncMenuList.length) {
contextMenuList.push(...trayFuncMenuList, {
type: 'separator'
})
}
// 其他菜单项
contextMenuList.push(
{
label: `Version: ${app.getVersion()}`,
// 禁用该项
enabled: false
},
{
label: t('actions.checkUpdate'),
click: () => {
// “检查更新”功能
updater.checkUpdate()
}
},
{
type: 'separator'
},
{
label: t('actions.quit'),
click: () => {
flags.isQuitting = true
app.quit()
}
}
)
const contextMenu = Menu.buildFromTemplate(contextMenuList)
tray.popUpContextMenu(contextMenu)
})
}
检查更新
这里仅实现了检查更新进行系统通知,没有考虑增量更新
// main/updater.mjs
import { app, ipcMain } from 'electron'
import electronUpdater from 'electron-updater'
const { autoUpdater } = electronUpdater
export default class Updater {
constructor() {
this.init()
}
init() {
if (!app.isPackaged) {
// 开启开发环境调试
autoUpdater.forceDevUpdateConfig = true
autoUpdater.logger = global.logger
console.log('更新配置路径:', autoUpdater.configOnDisk)
// 开发环境忽略代码签名检查
autoUpdater.disableWebInstaller = true
// 支持降级
autoUpdater.allowDowngrade = true
// 配置自定义更新服务器
autoUpdater.setFeedURL({
provider: 'generic',
url: 'http://localhost:8080'
})
}
// 配置自动更新
autoUpdater.autoDownload = false // 自动下载
autoUpdater.autoInstallOnAppQuit = true // 应用退出后自动安装
// 监听渲染进程的检查更新事件,触发检查更新
ipcMain.on('main:checkUpdate', () => {
autoUpdater.checkForUpdatesAndNotify()
})
}
on(event, callback = () => {}) {
if (typeof callback !== 'function') {
throw new Error('callback must be a function')
}
autoUpdater.on(event, callback)
}
checkUpdate() {
// 检测是否有更新包并通知
autoUpdater.checkForUpdatesAndNotify()
}
}
在主进程里实例化Updater,并绑定监听事件
// main/index.mjs
// 检查更新
updater = new Updater()
// 绑定事件
updater.on('update-available', (info) => {
global.logger.info('有可用更新', info)
// 显示系统通知
const notice = new Notification({
title: t('actions.checkUpdate'),
body: t('messages.updateAvailable', {
version: `v${info.version}`
})
})
notice.on('click', () => {
// 打开更新页面
shell.openExternal(appInfo.github + '/releases')
})
notice.show()
})
updater.on('update-not-available', (info) => {
global.logger.info('无需更新', info)
// 显示系统通知
new Notification({
title: t('actions.checkUpdate'),
body: t('messages.updateNotAvailable')
}).show()
})
updater.on('error', (err) => {
global.logger.error(`更新失败: error => ${err}`)
// 显示系统通知
new Notification({
title: t('actions.checkUpdate'),
body: t('messages.checkUpdateFail')
}).show()
})
Cache
使用lru-cache进行通用的缓存管理
// main/cache.mjs
import { LRUCache } from 'lru-cache'
const options = {
// 缓存的最大条目数。超过此数量时,最久未使用的条目会被淘汰
max: 1000,
// 缓存的最大大小(基于 sizeCalculation 计算的值)
maxSize: 1000 * 1024 * 1024, // 1GB
// 用于计算缓存项大小的函数
sizeCalculation: (value, key) => {
// 计算 data 的大小
const dataSize = value.data.length
// 计算 headers 的大小
const headersSize = Buffer.byteLength(JSON.stringify(value.headers))
// 返回总大小
return dataSize + headersSize
},
// 当缓存项被淘汰时调用的回调函数,用于清理资源
dispose: (value, key) => {
// console.log(`缓存项 ${key} 被淘汰,值为 ${value}`)
},
// 缓存项的默认存活时间(毫秒)。超过此时间后,缓存项会自动失效。
ttl: 1000 * 60 * 30,
// 是否自动清理过期的缓存项
ttlAutopurge: true,
// 是否允许返回过期的缓存项(即使过期,仍然可以获取到值)
allowStale: false,
// 当调用 get 方法时,是否更新缓存项的存活时间(TTL)
updateAgeOnGet: false,
// 当调用 has 方法时,是否更新缓存项的存活时间(TTL)
updateAgeOnHas: false
// 当缓存未命中时,用于异步获取数据的函数
// fetchMethod: async (key, staleValue, { options, signal, context }) => {}
}
const cache = new LRUCache(options)
export default cache
devtool
在窗口配置中开启devtool
// main/index.mjs
webPreferences: {
preload: path.join(__dirname, '../preload/index.mjs'),
sandbox: false,
webSecurity: false,
// 开启devTools
devTools: true
}
- 使用electron-devtools-installer安装
vue-devtools - 使用
webContents捕获自定义协议,用于在devtools中调试
// main/index.mjs
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer'
...
// 安装vue-devtools
installExtension(VUEJS3_DEVTOOLS)
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err))
// 使用 webContents 捕获自定义协议,用于在devtools中调试
webContents.getAllWebContents().forEach((wc) => {
// 启用调试器
try {
wc.debugger.attach('1.3') // 指定 DevTools 调试协议版本
} catch (err) {
console.error('Debugger attach failed:', err)
}
// 监听调试器消息
wc.debugger.on('message', (event, method, params) => {
if (method === 'Network.requestWillBeSent') {
console.log('Request URL:', params.request.url)
}
if (method === 'Network.responseReceived') {
console.log('Response:', params.response)
}
})
// 启用网络事件捕获
wc.debugger.sendCommand('Network.enable')
})
窗口
当前共有5个窗口
- loadingWindow: 显示窗口加载动画
- mainWindow: 主界面窗口
- viewImageWindow: 预览图片独立窗口
- suspensionBall: 悬浮球透明窗口
- dynamicWallpaperWindow: 动态壁纸窗口
// main/index.mjs
let loadingWindow
let mainWindow
let viewImageWindow
let suspensionBall
let dynamicWallpaperWindow = null
let store
let tray
let updater
// 加载动画页面地址
const loadingURL = `${path.join(process.env.FBW_RESOURCES_PATH, '/loading.html')}`
const getWindowURL = (name) => {
let url = isDev()
? `${process.env['ELECTRON_RENDERER_URL']}/windows/${name}/index.html`
: `${path.join(__dirname, `../renderer/windows/${name}/index.html`)}`
return url
}
// 窗口根地址
const MainWindowURL = getWindowURL('MainWindow')
const ViewImageWindowURL = getWindowURL('ViewImageWindow')
const SuspensionBallURL = getWindowURL('SuspensionBall')
const DynamicWallpaperURL = getWindowURL('DynamicWallpaper')
加载动画,这里其实就是先创建一个动画窗口,等待加载完成后再执行`callback`
const showLoading = (callback) => {
if (!isFunc(callback)) {
return
}
// 确保之前的 loadingWindow 被清理
if (loadingWindow) {
loadingWindow.destroy()
loadingWindow = null
}
loadingWindow = new BrowserWindow({
width: 200,
height: 200,
minWidth: 200,
minHeight: 200,
show: false,
frame: false,
resizable: false,
transparent: true
})
// 设置加载超时
const timeout = setTimeout(() => {
if (loadingWindow) {
loadingWindow.destroy()
loadingWindow = null
callback() // 超时后仍然执行回调
}
}, 10000) // 10秒超时
// 处理加载失败
loadingWindow.webContents.on('did-fail-load', () => {
clearTimeout(timeout)
if (loadingWindow) {
loadingWindow.destroy()
loadingWindow = null
callback() // 加载失败后仍然执行回调
}
})
loadingWindow.once('show', () => {
clearTimeout(timeout)
callback()
})
loadingWindow.once('ready-to-show', () => {
if (loadingWindow) {
loadingWindow.show()
}
})
// 加载动画页面
loadingWindow.loadFile(loadingURL).catch((err) => {
global.logger.error(`加载动画页面失败: ${err}`)
clearTimeout(timeout)
if (loadingWindow) {
loadingWindow.destroy()
loadingWindow = null
callback() // 加载失败后仍然执行回调
}
})
}
使用时
...
showLoading(createMainWindow)
...
阻止窗口右键菜单事件
// main/index.mjs
// 阻止窗口右键菜单事件
const preventContextMenu = (win) => {
if (!win) {
return
}
win.webContents.on('context-menu', (event) => {
event.preventDefault() // 阻止默认右键菜单行为
})
if (isWin()) {
// 监听 278 消息,关闭因为css: -webkit-app-region: drag 引起的默认右键菜单
win.hookWindowMessage(278, () => {
// 阻止默认的窗口关闭行为
win.setEnabled(false)
setTimeout(() => {
win.setEnabled(true)
}, 100)
return true
})
}
}
数据管理
主进程中的数据管理统一在store目录下
- index.mjs: Store类,数据管理入口,用于实例化各个管理类,启动定时任务、子服务等
- DatabaseManager.mjs: 数据库管理类,用于建表、初始化数据库
- FileManager.mjs: 文件管理类,用于启动文件子服务、处理文件
- ApiManager.mjs: 资源接口管理类,用于加载应用自带资源API、用户自定义资源API
- ResourcesManager.mjs: 资源查询管理类,用于查询、操作资源数据
- SettingManager.mjs: 设置数据管理类,用于获取、更新设置数据
- TaskScheduler.mjs: 定时任务管理类,用于管理应用内各种定时任务的启停清理
- WallpaperManager.mjs: 壁纸管理类,用于壁纸切换、下载、清理
- WordsManager.mjs: 词库管理类,用于查询、处理分词