Browse Source

重构更新模块并支持事件作用域机制

- 修改 `config/app_config.json` 中的热更新下载地址,替换为新的服务器地址。
- 重写 `packages/base/event/main/index.ts` 实现支持作用域的事件系统,新增类型推导和前缀处理逻辑。
- 删除 `packages/helper/updater/common.ts` 文件,其功能迁移到新的事件定义文件中。
- 更新 `packages/helper/updater/main/handler.ts` 适配新的事件作用域机制。
- 重构 `packages/helper/updater/main/index.ts` 为新的 `main.ts`,整合事件发射器并支持进度回调。
- 删除废弃的 `packages/helper/updater/renderer.ts` 文件。
- 修改 `packages/utils/main/index.ts` 的广播函数以支持作用域事件。
- 更新 `src/common/event/Updater` 相关模块,使用新的事件作用域机制处理更新进度。
- 调整 `src/preload/index.ts` 的 IPC 事件监听机制以支持作用域前缀。
- 修改 `src/types/global.d.ts` 的类型定义,增强事件系统的类型安全性。
- 新增 `events.ts` 定义更新模块专用的事件类型和作用域常量。
- 在首页添加触发热更新的点击事件用于测试。
dev/alpha
谢亚昕 3 weeks ago
parent
commit
427ed9399f
  1. 2
      config/app_config.json
  2. 78
      packages/base/event/main/index.ts
  3. 3
      packages/helper/updater/common.ts
  4. 16
      packages/helper/updater/events.ts
  5. 23
      packages/helper/updater/main.ts
  6. 6
      packages/helper/updater/main/handler.ts
  7. 0
      packages/helper/updater/renderer.ts
  8. 20
      packages/utils/main/index.ts
  9. 12
      src/common/event/Updater/hook.ts
  10. 4
      src/common/event/Updater/index.ts
  11. 10
      src/common/event/Updater/main/command.ts
  12. 2
      src/main/modules/commands/index.ts
  13. 16
      src/preload/index.ts
  14. 6
      src/renderer/src/pages/index/index.vue
  15. 30
      src/types/global.d.ts

2
config/app_config.json

@ -3,7 +3,7 @@
"language": "zh", "language": "zh",
"dev:debug": 2, "dev:debug": 2,
"common.theme": "auto", "common.theme": "auto",
"update.hoturl": "https://alist.xieyaxin.top/d/%E8%B5%84%E6%BA%90/%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.zip?sign=eqy35CR-J1SOQZz0iUN2P3B0BiyZPdYH0362nLXbUhE=:1749085071", "update.hoturl": "http://117.21.250.136:9812/ZxqyGateway/biz/file/downApk/%E6%98%93%E4%BC%81%E6%95%B0%E8%BD%AC%E5%B9%B3%E5%8F%B0app-1.2.7.apk",
"update.repo": "wood-desktop", "update.repo": "wood-desktop",
"update.owner": "npmrun", "update.owner": "npmrun",
"update.allowDowngrade": false, "update.allowDowngrade": false,

78
packages/base/event/main/index.ts

@ -1,51 +1,87 @@
// type FireKey = string // 从完整事件名称中提取原始事件名称(移除作用域前缀)
type ExtractOriginalEventName<T extends string, S extends string> = T extends `${S}:${infer Original}` ? Original : never
// 从完整事件映射中提取原始事件映射
type ExtractBaseEventMaps<T extends Record<string, any>, S extends string> = {
[K in keyof T as ExtractOriginalEventName<K & string, S>]: T[K]
}
type FireFN = (...argu: any[]) => void type FireFN = (...argu: any[]) => void
class FireEvent<T extends Record<string | symbol, FireFN>> { // 支持作用域的事件类
#events: Record<keyof T, FireFN[]> = {} as any class ScopedFireEvent<
T extends Record<string, FireFN>, // 完整的事件映射类型
S extends string, // 作用域类型
B extends Record<string, FireFN> = ExtractBaseEventMaps<T, S>, // 从完整事件映射中提取的基础事件映射
> {
#scope: S
#events: Record<string, FireFN[]> = {}
constructor(scope: S) {
this.#scope = scope
}
// 获取完整的事件名称(添加作用域前缀)
public getFullEventName(eventName: string): string {
if (this.#scope === "") return eventName
return `${this.#scope}:${eventName}`
}
print() { print() {
Object.keys(this.#events).forEach(key => { Object.keys(this.#events).forEach(key => {
console.log(`${key}: ${this.#events[key]}\n`) console.log(`${key}: ${this.#events[key]}\n`)
}) })
} }
on<S extends keyof T>(name: S, fn: T[S]) {
if (!this.#events[name]) { // 订阅事件,支持自动添加作用域前缀
this.#events[name] = [] on<K extends keyof B>(name: K, fn: B[K]) {
const fullName = this.getFullEventName(name as string)
if (!this.#events[fullName]) {
this.#events[fullName] = []
} }
this.#events[name].push(fn) this.#events[fullName].push(fn as FireFN)
} }
emit<S extends keyof T>(name: S, ...argu: Parameters<T[S]>) {
if (this.#events[name]) { // 触发事件,支持自动添加作用域前缀
this.#events[name].forEach(fn => { emit<K extends keyof B>(name: K, ...argu: Parameters<B[K]>) {
const fullName = this.getFullEventName(name as string)
if (this.#events[fullName]) {
this.#events[fullName].forEach(fn => {
fn(...argu) fn(...argu)
}) })
} }
} }
off<S extends keyof T>(name: S, fn?: T[S]) {
const len = this.#events[name].length // 取消订阅事件,支持自动添加作用域前缀
off<K extends keyof B>(name: K, fn?: B[K]) {
const fullName = this.getFullEventName(name as string)
const len = this.#events[fullName]?.length || 0
if (!len) { if (!len) {
return return
} }
if (!fn) { if (!fn) {
this.#events[name] = [] this.#events[fullName] = []
} else { } else {
for (let i = len - 1; i >= 0; i--) { for (let i = len - 1; i >= 0; i--) {
const _fn = this.#events[name][i] const _fn = this.#events[fullName][i]
if (_fn === fn) { if (_fn === fn) {
this.#events[name].splice(i, 1) this.#events[fullName].splice(i, 1)
} }
} }
} }
} }
once<S extends keyof T>(name: S, fn: T[S]) {
const _fn: any = (...argu: any[]) => { // 一次性订阅事件,支持自动添加作用域前缀
once<K extends keyof B>(name: K, fn: B[K]) {
const _fn: FireFN = (...argu: any[]) => {
fn(...argu) fn(...argu)
this.off<S>(name, _fn) this.off(name, _fn as B[K])
} }
this.on(name, _fn) this.on(name, _fn as B[K])
} }
} }
export function buildEmitter<T extends Record<string | symbol, FireFN>>() { // 创建作用域事件发射器的工厂函数 - 仅需一个泛型参数
return new FireEvent<T>() export function buildEmitter<T extends Record<string | symbol, FireFN>>(scope?: string): ScopedFireEvent<T, typeof scope> {
return new ScopedFireEvent<T, typeof scope>(scope)
} }

3
packages/helper/updater/common.ts

@ -1,3 +0,0 @@
export type EventMaps = {
"update-progress": (data: { percent: number; all: number; now: number }) => void
}

16
packages/helper/updater/events.ts

@ -0,0 +1,16 @@
export const EventScope = "updater" as const
// 原始事件映射类型
export type BaseEventMaps = {
"download-progress": (data: { percent: number; all: number; now: number }) => void
error: () => void
"checking-for-update": () => void
"update-available": () => void
"update-not-available": () => void
"update-downloaded": () => void
}
// 将 EventScope 添加到所有事件名称前缀
export type EventMaps = {
[K in keyof BaseEventMaps as `${typeof EventScope}:${K}`]: BaseEventMaps[K]
}

23
packages/helper/updater/main/index.ts → packages/helper/updater/main.ts

@ -2,21 +2,28 @@ import pkg from "electron-updater"
import { app, dialog } from "electron" import { app, dialog } from "electron"
import Setting from "setting/main" import Setting from "setting/main"
import { BaseSingleton } from "base" import { BaseSingleton } from "base"
import { fetchHotUpdatePackage, flagNeedUpdate } from "./hot" import { fetchHotUpdatePackage, flagNeedUpdate } from "./main/hot"
import Locales from "locales/main" import Locales from "locales/main"
import _logger from "logger/main" import _logger from "logger/main"
import { buildEmitter } from "base/event/main" import { buildEmitter } from "base/event/main"
import { EventMaps } from "./events"
const logger = _logger.createNamespace("updater") const logger = _logger.createNamespace("updater")
const { autoUpdater } = pkg const { autoUpdater } = pkg
class _Updater extends BaseSingleton { class _Updater extends BaseSingleton {
public events = buildEmitter() public events = buildEmitter<EventMaps>()
private timer: ReturnType<typeof setInterval> | null = null private timer: ReturnType<typeof setInterval> | null = null
// autoReplace = false // autoReplace = false
async triggerHotUpdate(autoReplace = false) { async triggerHotUpdate(autoReplace = false) {
const url = Setting.values("update.hoturl") const url = Setting.values("update.hoturl")
await fetchHotUpdatePackage(url) await fetchHotUpdatePackage(url, (p, now, all) => {
this.events.emit("download-progress", {
percent: p,
all,
now,
})
})
flagNeedUpdate() flagNeedUpdate()
if (!autoReplace) { if (!autoReplace) {
dialog.showMessageBox({ dialog.showMessageBox({
@ -36,27 +43,36 @@ class _Updater extends BaseSingleton {
// 检查更新错误 // 检查更新错误
autoUpdater.on("error", error => { autoUpdater.on("error", error => {
this.events.emit("error")
logger.debug("Update error:", error) logger.debug("Update error:", error)
}) })
// 检查更新 // 检查更新
autoUpdater.on("checking-for-update", () => { autoUpdater.on("checking-for-update", () => {
this.events.emit("checking-for-update")
logger.debug("Checking for updates...") logger.debug("Checking for updates...")
}) })
// 有可用更新 // 有可用更新
autoUpdater.on("update-available", info => { autoUpdater.on("update-available", info => {
logger.debug("Update available:", info) logger.debug("Update available:", info)
this.events.emit("update-available")
this.promptUserToUpdate() this.promptUserToUpdate()
}) })
// 没有可用更新 // 没有可用更新
autoUpdater.on("update-not-available", info => { autoUpdater.on("update-not-available", info => {
this.events.emit("update-not-available")
logger.debug("Update not available:", info) logger.debug("Update not available:", info)
}) })
// 更新下载进度 // 更新下载进度
autoUpdater.on("download-progress", progressObj => { autoUpdater.on("download-progress", progressObj => {
this.events.emit("download-progress", {
percent: progressObj.percent,
all: progressObj.total,
now: progressObj.transferred,
})
logger.debug( logger.debug(
`Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`, `Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`,
) )
@ -64,6 +80,7 @@ class _Updater extends BaseSingleton {
// 更新下载完成 // 更新下载完成
autoUpdater.on("update-downloaded", info => { autoUpdater.on("update-downloaded", info => {
this.events.emit("update-downloaded")
logger.debug("Update downloaded:", info) logger.debug("Update downloaded:", info)
this.promptUserToInstall() this.promptUserToInstall()
}) })

6
packages/helper/updater/main/handler.ts

@ -1,6 +1,6 @@
import { broadcast } from "utils/main" import { broadcast } from "utils/main"
import { EventMaps } from "../common" import { EventMaps, BaseEventMaps, EventScope } from "../events"
export function emit(key: keyof EventMaps, ...args: Parameters<EventMaps[keyof EventMaps]>) { export function emit(key: keyof BaseEventMaps, ...args: Parameters<EventMaps[keyof EventMaps]>) {
broadcast(key, ...args) broadcast<EventMaps>(EventScope, key, ...args)
} }

0
packages/helper/updater/renderer.ts

20
packages/utils/main/index.ts

@ -3,8 +3,24 @@ import { webContents } from "electron"
import { join } from "node:path" import { join } from "node:path"
import { slash } from "utils" import { slash } from "utils"
export const broadcast = <T extends Record<string, (...argu: any[]) => void>>(event: keyof T, ...args: Parameters<T[keyof T]>) => { type FireFN = (...argu: any[]) => void
webContents.getAllWebContents().forEach(browser => browser.send(event as any, ...args)) // 从完整事件名称中提取原始事件名称(移除作用域前缀)
type ExtractOriginalEventName<T extends string, S extends string> = T extends `${S}:${infer Original}` ? Original : never
type ExtractOriginalName<T extends string> = T extends `${infer Original}:${string}` ? Original : never
// 从完整事件映射中提取原始事件映射
type ExtractBaseEventMaps<T extends Record<string, any>, S extends string> = {
[K in keyof T as ExtractOriginalEventName<K & string, S>]: T[K]
}
export const broadcast = <
T extends Record<string, (...argu: any[]) => void>,
B extends Record<string, FireFN> = ExtractBaseEventMaps<T, string>,
>(
scope: ExtractOriginalName<keyof T & string>,
eventName: keyof B,
...args: Parameters<B[keyof B]>
) => {
webContents.getAllWebContents().forEach(browser => browser.send((scope + ":" + (eventName as string)) as any, ...args))
} }
export function getFileUrl(app: string) { export function getFileUrl(app: string) {

12
src/common/event/Updater/hook.ts

@ -1,16 +1,20 @@
import { EventMaps } from "helper/updater/common" import { EventMaps, EventScope } from "helper/updater/events"
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { Updater } from "."
export const useSettingStore = defineStore( export const useUpdateStore = defineStore(
"Updater", "Updater",
() => { () => {
getApi<EventMaps>().on("update-progress", (_, data) => { getApi<EventMaps>().on(EventScope, "download-progress", (_, data) => {
console.log(data) console.log(data)
}) })
return {} return {}
}, },
{ {
persist: false, persist: false,
}, },
) )
export const triggerHotUpdate = () => {
Updater.getInstance().triggerHotUpdate()
}

4
src/common/event/Updater/index.ts

@ -5,8 +5,8 @@ class Updater extends BaseEvent {
super() super()
} }
test() { triggerHotUpdate() {
this.api this.api.call("UpdaterCommand.triggerHotUpdate")
} }
} }

10
src/common/event/Updater/main/command.ts

@ -1,5 +1,7 @@
import Updater from "helper/updater/main" import Updater from "helper/updater/main"
import { EventMaps, EventScope } from "helper/updater/events"
import _logger from "logger/main" import _logger from "logger/main"
import { broadcast } from "utils/main"
const logger = _logger.createNamespace("UpdaterCommand") const logger = _logger.createNamespace("UpdaterCommand")
@ -9,7 +11,13 @@ export default class UpdaterCommand {
logger.debug("UpdaterCommand init") logger.debug("UpdaterCommand init")
} }
constructor() {
Updater.events.on("download-progress", ({ percent, all, now }) => {
broadcast<EventMaps>(EventScope, "download-progress", { percent, all, now })
})
}
async triggerHotUpdate() { async triggerHotUpdate() {
Updater.triggerHotUpdate() await Updater.triggerHotUpdate(false)
} }
} }

2
src/main/modules/commands/index.ts

@ -89,7 +89,7 @@ export default class Commands extends BaseClass {
}) })
menu.on("menu-will-close", () => { menu.on("menu-will-close", () => {
this.sendMessage(name, `popup_menu_close:${options.menu_id}`) this.sendMessage(name, `popupmenu:popup_menu_close:${options.menu_id}`)
// broadcast(`popup_menu_close:${options.menu_id}`) // broadcast(`popup_menu_close:${options.menu_id}`)
}) })
menu.popup(options.popupOptions) menu.popup(options.popupOptions)

16
src/preload/index.ts

@ -37,19 +37,19 @@ const api = {
if (!command) return if (!command) return
return ipcRenderer.sendSync(command, ...argu) return ipcRenderer.sendSync(command, ...argu)
}, },
on(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) { on(scope: string, command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.on(command, cb) ipcRenderer.on(scope + ":" + command, cb)
return () => ipcRenderer.removeListener(command, cb) return () => ipcRenderer.removeListener(command, cb)
}, },
once(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) { once(scope: string, command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.once(command, cb) ipcRenderer.once(scope + ":" + command, cb)
return () => ipcRenderer.removeListener(command, cb) return () => ipcRenderer.removeListener(command, cb)
}, },
off(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) { off(scope: string, command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
return ipcRenderer.removeListener(command, cb) return ipcRenderer.removeListener(scope + ":" + command, cb)
}, },
offAll(command: string) { offAll(scope: string, command: string) {
return ipcRenderer.removeAllListeners(command) return ipcRenderer.removeAllListeners(scope + ":" + command)
}, },
popupMenu(options: IPopupMenuOption) { popupMenu(options: IPopupMenuOption) {
ipcRenderer.send("x_popup_menu", curWebContentName, options) ipcRenderer.send("x_popup_menu", curWebContentName, options)

6
src/renderer/src/pages/index/index.vue

@ -1,7 +1,9 @@
<script setup lang="ts"></script> <script setup lang="ts">
import { triggerHotUpdate } from "common/event/Updater/hook"
</script>
<template> <template>
<div h-full flex flex-col>sad</div> <div h-full flex flex-col @click="triggerHotUpdate">sad</div>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

30
src/types/global.d.ts

@ -1,15 +1,35 @@
type FireFN = (...argu: any[]) => void type FireFN = (...argu: any[]) => void
type Api<T extends Record<string | symbol, FireFN>> = { // 从完整事件名称中提取原始事件名称(移除作用域前缀)
type ExtractOriginalEventName<T extends string, S extends string> = T extends `${S}:${infer Original}` ? Original : never
type ExtractOriginalName<T extends string> = T extends `${infer Original}:${string}` ? Original : never
// 从完整事件映射中提取原始事件映射
type ExtractBaseEventMaps<T extends Record<string, any>, S extends string> = {
[K in keyof T as ExtractOriginalEventName<K & string, S>]: T[K]
}
type Api<T extends Record<string, (...argu: any[]) => void>, B extends Record<string, FireFN> = ExtractBaseEventMaps<T, string>> = {
call: (command: string, ...args: any[]) => any call: (command: string, ...args: any[]) => any
callLong: (command: string, ...args: any[]) => any callLong: (command: string, ...args: any[]) => any
callSync: (command: string, ...args: any[]) => any callSync: (command: string, ...args: any[]) => any
send: (command: string, ...argu: any[]) => any send: (command: string, ...argu: any[]) => any
sendSync: (command: string, ...argu: any[]) => any sendSync: (command: string, ...argu: any[]) => any
on: <S extends keyof T>(command: S, cb: (event: IpcRendererEventIpcRendererEvent, ...args: Parameters<T[S]>) => void) => () => void on: <K extends keyof B>(
once: <S extends keyof T>(command: S, cb: (event: IpcRendererEvent, ...args: Parameters<T[S]>) => void) => () => void scope: ExtractOriginalName<keyof T & string>,
off: <S extends keyof T>(command: S, cb: (event: IpcRendererEvent, ...args: Parameters<T[S]>) => void) => void command: K,
offAll: <S extends keyof T>(command: S) => void cb: (event: IpcRendererEvent, ...args: Parameters<B[K]>) => void,
) => () => void
once: <K extends keyof B>(
scope: ExtractOriginalName<keyof T & string>,
command: K,
cb: (event: IpcRendererEvent, ...args: Parameters<B[K]>) => void,
) => () => void
off: <K extends keyof B>(
scope: ExtractOriginalName<keyof T & string>,
command: K,
cb: (event: IpcRendererEvent, ...args: Parameters<B[K]>) => void,
) => void
offAll: <K extends keyof B>(scope: ExtractOriginalName<keyof T & string>, command: K) => void
popupMenu: (options: IPopupMenuOption) => void popupMenu: (options: IPopupMenuOption) => void
} }

Loading…
Cancel
Save