系列
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/ # 子进程服务
│ │ ├── file_server/ # 文件处理服务
│ │ │ └── index.mjs # 子进程服务入口
│ │ └── h5_server/ # H5后台服务
│ │ │ ├── api/ # Koa路由接口
│ │ │ ├── socket/ # Socket服务端
│ │ │ ├── server.mjs # Koa服务
│ │ │ └── index.mjs # 子进程服务入口
│ │ ├── ChildServer.mjs # 子进程服务基类
│ │ └── index.mjs # 子进程入口
子服务入口
通过ChildServer
来 fork 子进程服务,这里用到了electron-vite 中的 ?modulePath`
这里导出的是两个服务的创建函数,用于在主进程中创建
// main/child_server/index.mjs
import ChildServer from './ChildServer.mjs'
import fileServerPath from './file_server/index.mjs?modulePath'
import h5ServerPath from './h5_server/index.mjs?modulePath'
// 文件处理子进程服务
export const createFileServer = () => new ChildServer('fileServer', fileServerPath)
// h5服务子进程服务
export const createH5Server = () => new ChildServer('h5Server', h5ServerPath)
ChildServer
主要功能:
- 统一子进程服务管理、数据通信
// main/child_server/ChildServer.mjs
import { utilityProcess, MessageChannelMain } from 'electron'
export default class ChildServer {
#serverName
#serverPath
#child
#port2
constructor(serverName, serverPath) {
global.logger.info(
`ChildServer INIT:: serverName => ${serverName}, serverPath => ${serverPath}`
)
this.#serverName = serverName
this.#serverPath = serverPath
this.#child = null
this.#port2 = null
}
start({ options, onMessage = () => {} } = {}) {
const { port1, port2 } = new MessageChannelMain()
this.#child = utilityProcess.fork(this.#serverPath, options)
global.logger.info(`ChildServer START:: serverName => ${this.#serverName}`)
this.#port2 = port2
this.#child.on('exit', () => {
global.logger.info(`ChildServer EXIT:: serverName => ${this.#serverName}`)
this.#child = null
this.#port2 = null
})
this.#port2.on('message', onMessage)
this.#port2.start()
// 初始消息
this.#child.postMessage(
{
serverName: this.#serverName,
event: 'SERVER_FORKED'
},
[port1]
)
// 服务启动消息
this.postMessage({
serverName: this.#serverName,
event: 'SERVER_START'
})
}
stop(callback) {
global.logger.info(`ChildServer STOP:: serverName => ${this.#serverName}`)
if (!this.#child) {
global.logger.info(
`ChildServer STOP FAILED:: SERVER NOT START, serverName => ${this.#serverName}`
)
return
}
const isSuccess = this.#child?.kill()
typeof callback === 'function' && callback(isSuccess)
}
restart({ params, onMessage = () => {}, stopCallback = () => {} } = {}) {
this.stop(stopCallback)
this.start({ params, onMessage })
}
postMessage(data) {
this.#port2?.postMessage(data)
}
}
文件服务子进程
服务入口
file_server
主要功能:
- 监听主进程刷新目录
REFRESH_DIRECTORY
事件 - 监听主进程处理图片质量
HANDLE_IMAGE_QUALITY
事件
这里将比较耗时的刷新、计算任务放入子进程处理,主进程只进行任务管理、数据入库操作,防止主进程被耗时任务卡死
// main/child_server/file_server/index.mjs
/**
* 文件服务子进程
* */
import { readDirRecursive, calculateImageByPath } from '../../utils/utils.mjs'
process.parentPort.on('message', (e) => {
const [port] = e.ports
const handleLogger = (type = 'info') => {
return (data) => {
if (!data) {
return
}
const postData = {
event: 'SERVER_LOG',
level: type,
msg: ''
}
if (typeof data === 'string') {
postData.msg = data
} else if (typeof data === 'object') {
postData.msg = JSON.stringify(data)
}
port.postMessage(postData)
}
}
const logger = {
info: handleLogger('info'),
warn: handleLogger('warn'),
error: handleLogger('error')
}
// 监听消息
port.on('message', async (e) => {
const { data } = e
// 分批处理大量文件
const processBatch = async (files, batchSize = 1000) => {
const results = []
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize)
results.push(...batch)
// 允许事件循环处理其他任务
await new Promise((resolve) => setTimeout(resolve, 0))
}
return results
}
if (data.event === 'SERVER_START') {
port.postMessage({
event: 'SERVER_START::SUCCESS'
})
} else if (data.event === 'REFRESH_DIRECTORY') {
const readDirTime = {
start: Date.now(),
end: Date.now()
}
try {
// 获取现有文件列表(如果有)
const existingFiles = data.existingFiles || []
const fileMap = new Map()
// 并行处理多个目录
const dirPromises = data.folderPaths.map((folderPath) =>
readDirRecursive(data.resourceName, folderPath, data.allowedFileExt, existingFiles)
)
// 等待所有目录处理完成
const results = await Promise.all(dirPromises)
// 合并结果
for (const fileList of results) {
if (fileList && fileList.length) {
for (const item of fileList) {
fileMap.set(item.filePath, item)
}
}
}
readDirTime.end = Date.now()
// 添加统计信息
const stats = {
newFiles: fileMap.size,
modifiedFiles: 0,
totalProcessed: fileMap.size
}
// 对于大量文件,使用批量处理
if (fileMap.size > 5000) {
// 先发送一个处理中的消息
port.postMessage({
event: 'REFRESH_DIRECTORY::PROCESSING',
isManual: data.isManual,
resourceName: data.resourceName,
totalFiles: fileMap.size,
refreshDirStartTime: data.refreshDirStartTime,
readDirTime
})
// 分批处理文件
const batchedList = await processBatch([...fileMap.values()], 1000)
// 发送最终结果
port.postMessage({
event: 'REFRESH_DIRECTORY::SUCCESS',
isManual: data.isManual,
resourceName: data.resourceName,
list: batchedList,
stats,
refreshDirStartTime: data.refreshDirStartTime,
readDirTime
})
} else {
// 对于少量文件,直接发送
port.postMessage({
event: 'REFRESH_DIRECTORY::SUCCESS',
isManual: data.isManual,
resourceName: data.resourceName,
list: [...fileMap.values()],
stats,
refreshDirStartTime: data.refreshDirStartTime,
readDirTime
})
}
} catch (err) {
logger.error(`[FileServer] ERROR => 刷新资源目录失败: ${err}`)
readDirTime.end = Date.now()
port.postMessage({
event: 'REFRESH_DIRECTORY::FAIL',
isManual: data.isManual,
resourceName: data.resourceName,
list: [],
refreshDirStartTime: data.refreshDirStartTime,
readDirTime
})
}
} else if (data.event === 'HANDLE_IMAGE_QUALITY') {
try {
const { list } = data
const ret = []
for (let i = 0; i < list.length; i++) {
const imgData = await calculateImageByPath(list[i].filePath)
ret.push({
id: list[i].id,
...imgData
})
}
port.postMessage({
event: 'HANDLE_IMAGE_QUALITY::SUCCESS',
resourceName: data.resourceName,
list: ret
})
} catch (err) {
logger.error(`[FileServer] ERROR => 处理图片质量失败: ${err}`)
port.postMessage({
event: 'HANDLE_IMAGE_QUALITY::FAIL',
resourceName: data.resourceName,
list: []
})
}
}
})
port.start()
})
H5服务子进程
服务入口
服务入口主要功能:
- 启动H5服务
- 设置数据广播同步
// main/child_server/h5_server/index.mjs
/**
* h5服务子进程
* */
import DatabaseManager from '../../store/DatabaseManager.mjs'
import SettingManager from '../../store/SettingManager.mjs'
import ResourcesManager from '../../store/ResourcesManager.mjs'
import FileManager from '../../store/FileManager.mjs'
import server from './server.mjs'
process.parentPort.on('message', (e) => {
const [port] = e.ports
const handleLogger = (type = 'info') => {
return (data) => {
if (!data) {
return
}
const postData = {
event: 'SERVER_LOG',
level: type,
msg: ''
}
if (typeof data === 'string') {
postData.msg = data
} else if (typeof data === 'object') {
postData.msg = JSON.stringify(data)
}
port.postMessage(postData)
}
}
const logger = {
info: handleLogger('info'),
warn: handleLogger('warn'),
error: handleLogger('error')
}
const postMessage = (data) => {
if (!data) {
return
}
port.postMessage(data)
}
let dbManager
let settingManager
let resourcesManager
let fileManager
let ioServer
// 监听消息
port.on('message', async (e) => {
try {
const { data } = e
// 启动h5服务
if (data.event === 'SERVER_START') {
// 初始化数据库管理器
dbManager = DatabaseManager.getInstance(logger)
await dbManager.waitForInitialization()
// 初始化各种管理器并等待它们初始化完成
settingManager = SettingManager.getInstance(logger, dbManager)
await settingManager.waitForInitialization()
fileManager = FileManager.getInstance(logger, dbManager, settingManager)
resourcesManager = ResourcesManager.getInstance(
logger,
dbManager,
settingManager,
fileManager
)
const serverRes = await server({
dbManager,
settingManager,
resourcesManager,
fileManager,
logger,
postMessage,
onStartSuccess: (url) => {
port.postMessage({
event: 'SERVER_START::SUCCESS',
url
})
},
onStartFail: (data) => {
port.postMessage({
event: 'SERVER_START::FAIL',
...data
})
}
})
ioServer = serverRes.ioServer
} else if (data.event === 'APP_SETTING_UPDATED') {
await settingManager.getSettingData()
// 广播设置更新给所有客户端
ioServer?.emit('settingUpdated', {
success: true,
data: settingManager.settingData
})
}
} catch (err) {
logger.error(`[H5Server] ERROR => ${err}`)
}
})
port.start()
})
H5服务
server
主要功能:
- 使用
Koa
创建本机IP
的http\https
的服务,这里使用了自签名证书主要是为了H5前端可以控制屏幕常亮 - 使用
@koa/router
创建接口服务,用于处理图片加载请求 - 使用
@koa-static
创建静态资源服务,用于支撑H5页面渲染,注意在打包前后资源目录路径不同 - 使用
socket.io
创建Socket服务,用于数据查询、设置数据更新、多端数据同步
// main/child_server/h5_server/server.mjs
import Koa from 'koa'
import KoaRouter from '@koa/router'
import staticServe from 'koa-static'
import { bodyParser } from '@koa/bodyparser'
import { Server } from 'socket.io'
import http from 'http'
import https from 'https' // 添加https模块
import fs from 'fs' // 添加fs模块用于读取证书文件
import path from 'path'
import { fileURLToPath } from 'url'
import { getLocalIP, findAvailablePort, generateSelfSignedCert } from '../../utils/utils.mjs' // 添加证书生成函数
import useApi from './api/index.mjs'
import { t } from '../../../i18n/server.js'
import setupSocketIO from './socket/index.mjs'
// 创建 Koa 服务器
export default async ({
port = 8888,
host = '0.0.0.0',
useHttps = true, // 添加HTTPS选项
dbManager,
settingManager,
resourcesManager,
fileManager,
logger = () => {},
postMessage = () => {},
onStartSuccess = () => {},
onStartFail = () => {}
} = {}) => {
let httpServer
let ioServer
try {
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const isProduction = process.env.NODE_ENV === 'production'
port = await findAvailablePort(port)
host = getLocalIP()
// 创建 Koa 服务器
const app = new Koa()
// 创建服务器 (HTTP 或 HTTPS)
if (useHttps) {
// 证书路径
const certPath = process.env.FBW_CERTS_PATH
let sslOptions
// 检查证书文件是否存在
try {
sslOptions = {
key: fs.readFileSync(path.join(certPath, 'private.key')),
cert: fs.readFileSync(path.join(certPath, 'certificate.crt'))
}
} catch (err) {
// 证书不存在,生成自签名证书
logger.info('[H5Server] INFO => 未找到SSL证书,正在生成自签名证书...')
// 确保证书目录存在
if (!fs.existsSync(certPath)) {
fs.mkdirSync(certPath, { recursive: true })
}
// 生成自签名证书
const { key, cert } = generateSelfSignedCert(host)
// 保存证书
fs.writeFileSync(path.join(certPath, 'private.key'), key)
fs.writeFileSync(path.join(certPath, 'certificate.crt'), cert)
sslOptions = { key, cert }
}
// 创建HTTPS服务器
httpServer = https.createServer(sslOptions, app.callback())
logger.info('[H5Server] INFO => 已创建HTTPS服务器')
} else {
// 创建HTTP服务器
httpServer = http.createServer(app.callback())
}
// 创建 Socket.IO 服务器
ioServer = new Server(httpServer, {
cors: {
origin: '*',
methods: ['GET', 'POST']
},
// 添加性能优化配置
transports: ['websocket', 'polling'], // 优先使用websocket
pingTimeout: 30000,
pingInterval: 25000,
upgradeTimeout: 10000,
maxHttpBufferSize: 1e6 // 1MB
})
// 包装 postMessage 函数,确保它能正常工作
const safePostMessage = (data) => {
try {
return postMessage(data)
} catch (err) {
logger.error(`[H5Server] ERROR => postMessage 错误: ${err}`)
return false
}
}
// 使用中间件方式挂载方法
app.use(async (ctx, next) => {
// 绑定方法到 ctx 对象
ctx.t = t
ctx.logger = logger
ctx.postMessage = safePostMessage
ctx.ioServer = ioServer // 将 Socket.IO 实例添加到上下文
await next()
})
// 解析请求体
app.use(bodyParser())
const staticPath = isProduction
? path.resolve(process.env.FBW_RESOURCES_PATH, './h5')
: path.resolve(__dirname, '../h5')
logger.info(`[H5Server] INFO => H5静态资源路径: ${staticPath}`)
// 提供静态资源服务
app.use(
staticServe(staticPath, {
// 添加静态资源服务配置,提高性能
maxage: 86400000, // 缓存一天
gzip: true, // 启用gzip压缩
br: true, // 启用brotli压缩(如果可用)
setHeaders: (res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Cache-Control', 'public, max-age=86400')
}
})
)
// 创建路由 - 仅保留 getImage 接口
const router = new KoaRouter()
// 注册 http 接口
useApi(router)
// 注册路由中间件
app.use(router.routes()).use(router.allowedMethods())
try {
// 设置 Socket.IO - 等待初始化完成
await setupSocketIO(ioServer, {
t,
dbManager,
settingManager,
resourcesManager,
fileManager,
logger,
postMessage: safePostMessage
})
} catch (err) {
typeof onStartFail === 'function' &&
onStartFail({
msg: `Socket.IO 初始化失败: ${err}`
})
}
logger.info('[H5Server] INFO => Socket.IO 初始化完成,准备启动服务器')
// 添加性能优化中间件
app.use(async (ctx, next) => {
// 设置缓存控制头
ctx.set('Cache-Control', 'public, max-age=86400')
ctx.set('X-Content-Type-Options', 'nosniff')
await next()
})
// 启动服务器
httpServer.listen(port, host, () => {
const protocol = useHttps ? 'https' : 'http'
const serverUrl = `${protocol}://${host}:${port}`
typeof onStartSuccess === 'function' && onStartSuccess(serverUrl)
})
// 处理端口被占用的情况
httpServer.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
logger.error(`[H5Server] ERROR => 端口 ${port} 已被占用,尝试使用其他端口...`)
// 关闭当前服务器
httpServer.close()
// 尝试使用新端口重新启动
findAvailablePort(port + 1)
.then((newPort) => {
logger.info(`[H5Server] INFO => 尝试使用新端口: ${newPort}`)
httpServer.listen(newPort, host)
})
.catch((err) => {
typeof onStartFail === 'function' &&
onStartFail({
msg: `查找可用端口失败: ${err}`
})
})
} else {
typeof onStartFail === 'function' &&
onStartFail({
msg: `服务器错误: ${err}`
})
}
})
} catch (err) {
typeof onStartFail === 'function' &&
onStartFail({
msg: `服务器启动失败: ${err}`
})
}
return {
httpServer,
ioServer
}
}
API
使用@koa/router
创建接口服务,用于处理图片加载请求
// main/child_server/h5_server/api/index.mjs
import { getImage } from './images.mjs'
const useApi = (router) => {
// 图片相关接口
router.get('/api/images/get', getImage)
}
export default useApi
这里复用了与主进程中的handleFileRsponse
方法,实现统一的图片请求逻辑
// main/child_server/h5_server/api/images.mjs
import { handleFileRsponse } from '../../../utils/file.mjs'
export const getImage = async (ctx) => {
const { filePath, w, h } = ctx.request.query
const res = await handleFileRsponse({ filePath, w, h })
ctx.set(res.headers)
ctx.status = res.status
ctx.body = res.data
}
Socket
Socket
主功能:
- 监听H5前端请求消息,调用各个管理类进行数据操作
// main/child_server/h5_server/socket/index.mjs
export default async function setupSocketIO(
ioServer,
{ t, dbManager, settingManager, resourcesManager, fileManager, logger, postMessage }
) {
// 添加一个安全调用回调的辅助函数
const safeCallback = (callback, response) => {
if (typeof callback === 'function') {
try {
callback(response)
} catch (err) {
logger.error('[H5Server] ERROR => 回调函数执行错误:', err)
}
} else {
logger.error('[H5Server] ERROR => 回调函数不是一个函数')
}
}
ioServer.on('connection', (socket) => {
// 获取设置
socket.on('getSettingData', async (params, callback) => {
try {
const res = await settingManager.getSettingData()
safeCallback(callback, res)
} catch (err) {
logger.error(`[H5Server] ERROR => 获取设置错误: ${err}`)
safeCallback(callback, {
success: false,
data: null,
msg: t('messages.operationFail')
})
}
})
// 更新设置
socket.on('h5UpdateSettingData', async (data, callback) => {
try {
const res = await settingManager.updateSettingData(data)
if (res.success && res.data) {
// 向主进程发送设置更新消息
postMessage({
event: 'H5_SETTING_UPDATED',
data: res.data
})
// 广播设置更新给所有客户端
ioServer.emit('settingUpdated', res)
safeCallback(callback, {
success: true,
data: res.data,
msg: t('messages.operationSuccess')
})
} else {
safeCallback(callback, {
success: false,
data: null,
msg: t('messages.operationFail')
})
}
} catch (err) {
logger.error(`[H5Server] ERROR => 更新设置错误: ${err}`)
safeCallback(callback, {
success: false,
data: null,
msg: t('messages.operationFail')
})
}
})
// 获取资源数据
socket.on('getResourceMap', async (params, callback) => {
try {
const res = await dbManager.getResourceMap()
safeCallback(callback, res)
} catch (err) {
logger.error(`[H5Server] ERROR => 获取资源数据错误: ${err}`)
safeCallback(callback, {
success: false,
data: null,
msg: t('messages.operationFail')
})
}
})
// 搜索图片
socket.on('searchImages', async (params, callback) => {
try {
const res = await resourcesManager.searchImages(params)
safeCallback(callback, res)
} catch (err) {
logger.error(`[H5Server] ERROR => 搜索图片错误: ${err}`)
safeCallback(callback, {
success: false,
data: null,
msg: t('messages.operationFail')
})
}
})
// 切换收藏状态
socket.on('toggleFavorite', async (id, callback) => {
try {
if (!id) {
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
return
}
// 检查是否已经收藏
const isFavorite = await resourcesManager.checkFavorite(id)
if (isFavorite) {
// 如果已经收藏,则取消收藏
const res = await resourcesManager.removeFavorites(id)
safeCallback(callback, {
success: res.success,
msg: res.success ? t('messages.operationSuccess') : t('messages.operationFail')
})
} else {
// 如果没有收藏,则添加收藏
const res = await resourcesManager.addToFavorites(id)
safeCallback(callback, {
success: res.success,
msg: res.success ? t('messages.operationSuccess') : t('messages.operationFail')
})
}
} catch (err) {
logger.error(`[H5Server] ERROR => 切换收藏状态错误: ${err}`)
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
}
})
// 加入收藏
socket.on('addToFavorites', async (id, callback) => {
try {
if (!id) {
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
return
}
const res = await resourcesManager.addToFavorites(id)
safeCallback(callback, {
success: res.success,
msg: res.success ? t('messages.operationSuccess') : t('messages.operationFail')
})
} catch (err) {
logger.error(`[H5Server] ERROR => 加入收藏错误: ${err}`)
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
}
})
// 更新收藏数量
socket.on('updateFavoriteCount', async ({ id, count }, callback) => {
try {
if (!id) {
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
return
}
const res = await resourcesManager.updateFavoriteCount(id, count)
safeCallback(callback, {
success: res.success,
msg: res.success ? t('messages.operationSuccess') : t('messages.operationFail')
})
} catch (err) {
logger.error(`[H5Server] ERROR => 更新收藏数量错误: ${err}`)
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
}
})
// 取消收藏
socket.on('removeFavorites', async (id, callback) => {
try {
if (!id) {
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
return
}
const res = await resourcesManager.removeFavorites(id)
safeCallback(callback, {
success: res.success,
msg: res.success ? t('messages.operationSuccess') : t('messages.operationFail')
})
} catch (err) {
logger.error(`[H5Server] ERROR => 取消收藏错误: ${err}`)
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
}
})
// 删除图片
socket.on('deleteImage', async (item, callback) => {
try {
if (!item) {
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
return
}
const res = await fileManager.deleteFile(item)
safeCallback(callback, {
success: res.success,
msg: res.success ? t('messages.operationSuccess') : t('messages.operationFail')
})
} catch (err) {
logger.error(`[H5Server] ERROR => 删除图片错误: ${err}`)
safeCallback(callback, {
success: false,
msg: t('messages.operationFail')
})
}
})
// 断开连接
socket.on('disconnect', () => {
logger.info(`[H5Server] INFO => 客户端断开连接: ${socket.id}`)
})
})
logger.info('[H5Server] INFO => Socket.IO 管理器初始化完成')
}