系列
Electron + Vue3开源跨平台壁纸工具实战(二)本地运行
Electron + Vue3开源跨平台壁纸工具实战(三)主进程
Electron + Vue3开源跨平台壁纸工具实战(四)主进程-数据管理(1)
Electron + Vue3开源跨平台壁纸工具实战(五)主进程-数据管理(2)
Electron + Vue3开源跨平台壁纸工具实战(六)子进程服务
Electron + Vue3开源跨平台壁纸工具实战(七)进程通信
Electron + Vue3开源跨平台壁纸工具实战(八)主进程-核心功能
Electron + Vue3开源跨平台壁纸工具实战(九)子进程服务(2)
Electron + Vue3开源跨平台壁纸工具实战(十)渲染进程
Electron + Vue3开源跨平台壁纸工具实战(十一)图片壁纸
源码
省流点我进Github获取源码,欢迎fork、star、PR
H5功能介绍
飞鸟壁纸的H5功能是一个完整的Web应用,包含后端服务和前端界面,通过Electron的子进程机制实现Web服务集成,为用户提供移动端友好的图片浏览体验。
架构概述
H5功能采用前后端分离架构:
- 后端服务:基于Node.js + Koa + Socket.IO的子进程服务
- 前端应用:基于Vue 3 + Vant UI的移动端Web应用
- 通信机制:通过Socket.IO实现实时双向通信
主进程中的H5后端服务
子进程管理
H5服务通过Electron的utilityProcess作为子进程运行,确保与主进程隔离:
// src/main/child_server/ChildServer.mjs
export default class ChildServer {
constructor(serverName, 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)
this.#port2 = port2
// 建立进程间通信
this.#child.postMessage({ serverName: this.#serverName, event: 'SERVER_FORKED' }, [port1])
this.postMessage({ serverName: this.#serverName, event: 'SERVER_START' })
}
}
服务器实现
H5服务器基于Koa框架构建,支持HTTP/HTTPS协议:
// src/main/child_server/h5_server/server.mjs
export default async ({
port = 8888,
host = '0.0.0.0',
useHttps = true,
dbManager,
settingManager,
resourcesManager,
fileManager,
logger = () => {},
postMessage = () => {},
onStartSuccess = () => {},
onStartFail = () => {}
} = {}) => {
// 创建 Koa 服务器
const app = new Koa()
// 支持HTTPS自签名证书
if (useHttps) {
const certPath = process.env.FBW_CERTS_PATH
const { key, cert } = generateSelfSignedCert(host)
const sslOptions = { key, cert }
httpServer = http2.createSecureServer(sslOptions, app.callback())
} else {
httpServer = http.createServer(app.callback())
}
// 创建 Socket.IO 服务器
ioServer = new Server(httpServer, {
cors: { origin: '*', methods: ['GET', 'POST'] },
transports: ['websocket', 'polling'],
pingTimeout: 30000,
pingInterval: 25000
})
}
静态资源服务
服务器提供H5前端静态资源服务:
// 处理H5静态资源地址
const staticPath = isDev()
? path.resolve(__dirname, '../h5')
: path.resolve(process.env.FBW_RESOURCES_PATH, './h5')
// 提供静态资源服务
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')
}
})
)
HTTP API接口
提供图片获取等HTTP接口:
// src/main/child_server/h5_server/api/index.mjs
const useApi = (router) => {
// 图片相关接口
router.get('/api/images/get', getImage)
}
// src/main/child_server/h5_server/api/images.mjs
export const getImage = async (ctx) => {
const { filePath, w, h, compressStartSize } = ctx.request.query
const res = await handleFileResponse({ filePath, w, h, compressStartSize })
ctx.set(res.headers)
ctx.status = res.status
ctx.body = res.data
}
Socket.IO实时通信
通过Socket.IO实现实时双向通信,支持多种操作:
// src/main/child_server/h5_server/socket/index.mjs
export default async function setupSocketIO(
ioServer,
{ t, dbManager, settingManager, resourcesManager, fileManager, logger, postMessage }
) {
ioServer.on('connection', (socket) => {
// 获取设置
socket.on('getSettingData', async (params, callback) => {
const res = await settingManager.getSettingData()
safeCallback(callback, res)
})
// 更新设置
socket.on('h5UpdateSettingData', async (data, callback) => {
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, res)
})
// 搜索图片
socket.on('searchImages', async (params, callback) => {
const res = await resourcesManager.search(params)
safeCallback(callback, res)
})
// 收藏相关操作
socket.on('toggleFavorite', async (id, callback) => {
const isFavorite = await resourcesManager.checkFavorite(id)
if (isFavorite) {
const res = await resourcesManager.removeFavorites(id)
} else {
const res = await resourcesManager.addToFavorites(id)
}
safeCallback(callback, res)
})
// 删除图片
socket.on('deleteImage', async (item, callback) => {
const res = await fileManager.deleteFile(item)
safeCallback(callback, res)
})
})
}
进程间通信
H5子进程与主进程通过MessageChannel进行通信:
// src/main/child_server/h5_server/index.mjs
process.parentPort.on('message', (e) => {
const [port] = e.ports
port.on('message', async (e) => {
const { data } = e
if (data.event === 'SERVER_START') {
// 初始化各种管理器
dbManager = DatabaseManager.getInstance(logger)
settingManager = SettingManager.getInstance(logger, dbManager)
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 })
}
})
} else if (data.event === 'APP_SETTING_UPDATED') {
await settingManager.getSettingData()
ioServer?.emit('settingUpdated', {
success: true,
data: settingManager.settingData
})
}
})
})
H5前端应用
技术栈
- 框架:Vue 3 + Composition API
- UI组件库:Vant 4(移动端UI)
- 状态管理:Pinia
- 构建工具:Vite
- 国际化:i18next
- 图标:Iconify
应用入口
// src/h5/main.js
import Vant, { Lazyload } from 'vant'
import 'vant/lib/index.css'
import './assets/main.css'
import App from './App.vue'
import useIconifyIcon from './utils/icons.js'
import useI18n from '@i18n/web.js'
const app = createApp(App)
const pinia = createPinia()
app.use(Vant)
app.use(Lazyload)
app.use(pinia)
useIconifyIcon(app)
useI18n(app).mount('#app')
主应用组件
<!-- src/h5/App.vue -->
<script setup>
import UseCommonStore from './stores/commonStore.js'
import UseSettingStore from './stores/settingStore.js'
const commonStore = UseCommonStore()
const settingStore = UseSettingStore()
const { activeTabbar, tabbarVisible } = storeToRefs(commonStore)
const { settingData } = storeToRefs(settingStore)
const tabbarList = [
{ name: 'home', title: '首页', locale: 'h5.tabbar.home', icon: 'ri:home-3-line' },
{ name: 'search', title: '搜索', locale: 'h5.tabbar.search', icon: 'ri:search-line' },
{ name: 'setting', title: '设置', locale: 'h5.tabbar.setting', icon: 'ri:settings-line' }
]
onBeforeMount(async () => {
// 初始化 Socket.IO 监听
settingStore.initSocketListeners()
// 获取设置数据
await settingStore.getSettingData()
// 获取资源数据
await commonStore.getResourceMap()
})
</script>
<template>
<van-config-provider
:theme="settingData.themes.dark ? 'dark' : 'light'"
:theme-vars="themeVars"
theme-vars-scope="global"
>
<div id="app">
<component :is="currentPage || pageEmpty" ref="pageRef"></component>
<van-tabbar v-if="tabbarVisible" v-model="activeTabbar" safe-area-inset-bottom>
<van-tabbar-item v-for="item in tabbarList" :key="item.name" :name="item.name">
{{ t(item.locale) }}
<template #icon>
<IconifyIcon :icon="item.icon" />
</template>
</van-tabbar-item>
</van-tabbar>
</div>
</van-config-provider>
</template>
状态管理
通用状态管理
// src/h5/stores/commonStore.js
const UseCommonStore = defineStore('common', {
state: () => ({
activeTabbar: 'home',
tabbarVisible: true,
resourceMap: JSON.parse(JSON.stringify(defaultResourceMap))
}),
actions: {
setActiveTabbar(name) {
this.activeTabbar = name
},
toggleTabbarVisible() {
this.tabbarVisible = !this.tabbarVisible
const tabbarHeight = this.tabbarVisible ? 'var(--van-tabbar-height)' : '0px'
document.documentElement.style.setProperty('--fbw-tabbar-height', tabbarHeight)
},
async getResourceMap() {
const res = await api.getResourceMap()
if (res.success) {
this.resourceMap = Object.assign({}, this.resourceMap, res.data)
}
return res
}
}
})
设置状态管理
// src/h5/stores/settingStore.js
const UseSettingStore = defineStore('setting', {
state: () => ({
settingData: { ...JSON.parse(JSON.stringify(defaultSettingData)) },
localSetting: { multiDeviceSync: true }
}),
actions: {
async getSettingData() {
const res = await api.getSettingData()
if (res.success) {
this.settingData = Object.assign({}, this.settingData, res.data)
}
return res
},
async h5UpdateSettingData(data) {
if (!this.localSetting.multiDeviceSync) {
this.settingData = Object.assign({}, this.settingData, data)
return { success: false, message: i18next.t('messages.multiDeviceSyncWarning') }
}
const res = await api.h5UpdateSettingData(data)
if (res.success) {
this.settingData = Object.assign({}, this.settingData, res.data)
}
return res
},
initSocketListeners() {
// 监听多设备设置更新事件
api.socketInstance.on('settingUpdated', (res) => {
if (res.success && this.localSetting.multiDeviceSync) {
this.settingData = Object.assign({}, this.settingData, res.data)
}
})
}
}
})
API通信层
import { io } from 'socket.io-client'
// 创建 Socket.IO 客户端实例
const socket = io(window.location.origin)
// 包装 Socket.IO 事件为 Promise
const emitAsync = (event, data) => {
return new Promise((resolve) => {
socket.emit(event, data, (response) => {
resolve(response)
})
})
}
// 图片相关接口
export const searchImages = async (data) => {
return await emitAsync('searchImages', data)
}
export const toggleFavorite = async (id) => {
return await emitAsync('toggleFavorite', id)
}
export const addToFavorites = async (id) => {
return await emitAsync('addToFavorites', id)
}
export const deleteImage = async (item) => {
return await emitAsync('deleteImage', item)
}
// 设置接口
export const getSettingData = async () => {
return await emitAsync('getSettingData')
}
export const h5UpdateSettingData = async (data) => {
return await emitAsync('h5UpdateSettingData', data)
}
export const getResourceMap = async () => {
return await emitAsync('getResourceMap')
}
// 导出 socket 实例
export const socketInstance = socket
页面组件
首页组件
首页提供图片浏览、搜索、收藏等功能:
<!-- src/h5/pages/home/index.vue -->
<script setup>
import UseCommonStore from '@h5/stores/commonStore.js'
import UseSettingStore from '@h5/stores/settingStore.js'
import * as api from '@h5/api/index.js'
const commonStore = UseCommonStore()
const settingStore = UseSettingStore()
const settingData = ref(settingStore.settingData)
// 自动切换相关状态
const autoSwitch = reactive({
switchTimer: null,
countdownTimer: null,
countdown: settingData.value.h5SwitchIntervalTime,
currentIndex: 0,
imageList: [],
total: 0
})
// 是否随机
const isRandom = computed(() => {
return settingData.value.h5SwitchType === 1
})
// 初始化方法
const init = async () => {
await loadImages()
if (settingData.value.h5AutoSwitch) {
startAutoSwitch()
}
}
// 加载图片
const loadImages = async () => {
const res = await api.searchImages({
page: pageInfo.startPage,
pageSize: pageInfo.pageSize,
random: isRandom.value
})
if (res.success) {
autoSwitch.imageList = res.data.list
autoSwitch.total = res.data.total
}
}
// 自动切换
const startAutoSwitch = () => {
if (autoSwitch.switchTimer) {
clearInterval(autoSwitch.switchTimer)
}
autoSwitch.switchTimer = setInterval(() => {
if (isRandom.value) {
autoSwitch.currentIndex = Math.floor(Math.random() * autoSwitch.imageList.length)
} else {
autoSwitch.currentIndex = (autoSwitch.currentIndex + 1) % autoSwitch.imageList.length
}
}, settingData.value.h5SwitchIntervalTime * 1000)
}
</script>
设置页面
提供H5相关设置选项:
<!-- src/h5/pages/setting/index.vue -->
<template>
<div class="setting-page">
<van-cell-group title="H5设置">
<van-cell title="自动切换" is-link @click="showAutoSwitchPopup = true">
<template #value>
<van-switch v-model="settingData.h5AutoSwitch" @change="onAutoSwitchChange" />
</template>
</van-cell>
<van-cell
title="切换间隔"
:value="`${settingData.h5SwitchIntervalTime}秒`"
is-link
@click="showIntervalPopup = true"
/>
<van-cell
title="切换方式"
:value="settingData.h5SwitchType === 1 ? '随机' : '顺序'"
is-link
@click="showTypePopup = true"
/>
<van-cell title="屏幕常亮" is-link @click="showWakeLockPopup = true">
<template #value>
<van-switch v-model="settingData.h5WeekScreen" @change="onWakeLockChange" />
</template>
</van-cell>
<van-cell title="震动反馈" is-link @click="showVibrationPopup = true">
<template #value>
<van-switch v-model="settingData.h5Vibration" @change="onVibrationChange" />
</template>
</van-cell>
</van-cell-group>
</div>
</template>
Electron中的Web服务集成
服务启动流程
- 主进程启动H5服务:
// src/main/store/index.mjs
handleH5ServerStart(maxRetries = 3, retryInterval = 2000) {
this.h5Server?.start({
options: { env: process.env },
onMessage: async ({ data }) => {
switch (data.event) {
case 'SERVER_START::SUCCESS': {
this.h5ServerUrl = data.url
global.logger.info(`H5服务器启动成功: ${this.h5ServerUrl}`)
// 发送消息到主窗口
if (global.FBW.mainWindow.win) {
global.FBW.sendCommonData(global.FBW.mainWindow.win)
global.FBW.sendMsg(global.FBW.mainWindow.win, {
type: 'success',
message: t('messages.h5ServerStartSuccess')
})
}
break
}
case 'H5_SETTING_UPDATED': {
await this.settingManager.getSettingData()
this.sendSettingDataUpdate()
break
}
}
}
})
}
-
子进程初始化:
- 创建Koa服务器
- 配置HTTPS/HTTP协议
- 设置Socket.IO服务
- 挂载静态资源
- 注册API路由
-
服务就绪:
- 返回服务URL给主进程
- 主进程通知渲染进程
- 用户可通过URL访问H5应用
进程间通信机制
- 主进程 ↔ 子进程:通过MessageChannel进行通信
- 子进程 ↔ H5前端:通过Socket.IO进行实时通信
- 主进程 ↔ 渲染进程:通过IPC进行通信
多设备同步
H5应用支持多设备设置同步:
// 设置更新时广播给所有客户端
socket.on('h5UpdateSettingData', async (data, callback) => {
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, res)
})
// 客户端监听设置更新
api.socketInstance.on('settingUpdated', (res) => {
if (res.success && this.localSetting.multiDeviceSync) {
this.settingData = Object.assign({}, this.settingData, res.data)
}
})
构建和部署
H5前端通过Vite构建:
// h5.vite.config.mjs
export default defineConfig({
root: resolve(__dirname, 'src/h5'),
base: './',
build: {
emptyOutDir: true,
outDir: '../../out/h5',
assetsDir: 'assets',
rollupOptions: {
input: resolve(__dirname, 'src/h5/index.html')
}
}
})
构建后的静态资源会被复制到Electron应用的resources目录,由H5服务器提供服务。
功能特性
移动端优化
- 响应式设计,适配各种移动设备
- 触摸手势支持(滑动、长按等)
- 屏幕常亮功能
- 震动反馈
实时通信
- Socket.IO实现实时双向通信
- 多设备设置同步
- 实时状态更新
性能优化
- 图片懒加载
- 静态资源缓存
- Gzip/Brotli压缩
- 连接池管理
安全性
- HTTPS自签名证书
- CORS跨域配置
- 输入验证和过滤
H5功能为飞鸟壁纸提供了完整的移动端Web体验,通过Electron的子进程机制实现了稳定的Web服务集成,支持多设备同步和实时通信,为用户提供了便捷的图片浏览和管理功能。