feat: 项目结构调整|新增依赖

This commit is contained in:
duanshuwen
2025-11-22 21:17:40 +08:00
parent 38b6a4b4a3
commit 6013c38fe7
40 changed files with 535 additions and 115 deletions

View File

@@ -1,8 +1,9 @@
import { app, BrowserWindow, ipcMain, shell } from "electron";
import path from "node:path";
import started from "electron-squirrel-startup";
import { TabManager } from './controller/tab-manager'
import "./controller/window-size-controll";
import { TabManager } from '@modules/tab-manager'
import { logger } from '@modules/logger'
import "@modules/window-size";
if (started) {
app.quit();
@@ -17,6 +18,7 @@ class AppMain {
this.registerLifecycle()
this.registerCommonIPC()
this.registerAppIPC()
this.registerLogIPC()
}
private createWindow(options?: { frameless?: boolean; route?: string }): BrowserWindow {
@@ -34,8 +36,8 @@ class AppMain {
webPreferences: {
devTools: this.isDev,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
contextIsolation: true, // 同时启动上下文隔离
sandbox: true, // 启动沙箱模式
preload: path.join(__dirname, "preload.js"),
},
})
@@ -55,6 +57,12 @@ class AppMain {
} else {
win.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`))
}
// 暴露安全 API 示例:通过 IPC 处理文件读取
ipcMain.handle('read-file', async (event, filePath) => {
const fs = require('fs');
return fs.promises.readFile(filePath, 'utf-8'); // 主进程处理敏感操作
});
}
private initTabsIfNeeded(options?: { frameless?: boolean; route?: string }) {
@@ -118,7 +126,33 @@ class AppMain {
ipcMain.handle('tab:switch', (_e, tabId: string) => tabs.switch(tabId))
ipcMain.handle('tab:close', (_e, tabId: string) => tabs.close(tabId))
}
private registerLogIPC() {
ipcMain.handle('log-to-main', (_e, logLevel: string, message: string) => {
switch(logLevel) {
case 'trace':
logger.trace(message)
break
case 'debug':
logger.debug(message)
break
case 'info':
logger.info(message)
break
case 'warn':
logger.warn(message)
break
case 'error':
logger.error(message)
break
default:
logger.info(message)
break
}
})
}
}
new AppMain().init()

View File

@@ -0,0 +1,95 @@
import { ipcMain, BaseWindow } from 'electron'
type Handler = (...args:any[]) => any
type AsyncHandler = (...args:any[]) => Promise<any>
export class IPCManager {
private static instance: IPCManager
private handlers: Map<string, Handler>
private asyncHandlers: Map<string,AsyncHandler>
private constructor() {
this.handlers = new Map()
this.asyncHandlers =new Map()
this.initialize()
}
public static getInstance(): IPCManager {
if (!IPCManager.instance) {
IPCManager.instance =new IPCManager()
}
return IPCManager.instance
}
private initialize(): void {
//注册同步处理器
ipcMain.on('ipc:invoke',(event, channel,...args) => {
try {
const handler = this.handlers.get(channel)
if(handler){
event.returnValue = handler(...args)
} else {
event.returnValue = { success: false, error: `No handler for channel: ${channel}`}
}
} catch (error) {
event.returnValue = { success: false, error:(error as Error).message}
}
})
// 注册异步处理器
ipcMain.handle('ipc:invokeAsync', async (_event, channel, ...args) => {
try {
const handler = this.asyncHandlers.get(channel)
if(handler){
return await handler(...args)
}
throw new Error(`No async handler for channel: ${channel}`)
} catch (error) {
throw error
}
})
ipcMain.handle('get-window-id',(event) => {
event.returnValue = event.sender.id
})
}
// 注册同步处理器
public register(channel:string, handler:Handler):void {
this.handlers.set(channel, handler)
}
// 注册异步处理器
public registerAsync(channel:string, handler:AsyncHandler):void {
this.asyncHandlers.set(channel, handler)
}
// 广播消息给所有窗口
public broadcast(channel:string, ...args:any[]):void {
BaseWindow.getAllWindows().forEach(window => {
if (!window.isDestroyed()) {
window.webContents.send(channel, ...args)
}
})
}
// 发送消息给指定窗口
public sendToWindow(windowId: string, channel:string, ...args:any[]):void {
const window = BaseWindow.fromId(windowId)
if (window && !window.isDestroyed()) {
window.webContents.send(channel, ...args)
}
}
// 清理所有处理器
public clear():void {
this.handlers.clear()
this.asyncHandlers.clear()
}
}
export const ipcManager = IPCManager.getInstance()

View File

@@ -0,0 +1,29 @@
import * as log4js from 'log4js';
log4js.configure({
appenders: {
out: {
type: 'stdout'
},
app: {
type: 'file',
filename: 'logs/app.log',
backups: 3,
compress: false,
encoding: 'utf-8',
layout: {
type: 'pattern',
pattern: '[%d{yyyy-MM-dd hh:mm:ss.SSS}] [%p] %m'
},
keepFileExt: true
}
},
categories: {
default: {
appenders: ['out', 'app'],
level: 'debug'
}
}
});
export const logger = log4js.getLogger();

View File

@@ -1,22 +1,25 @@
import { contextBridge, ipcRenderer } from 'electron'
import { IPCChannel, IPCAPI } from '@/shared/types/ipc.types'
contextBridge.exposeInMainWorld('electronAPI', {
openBaidu: () => ipcRenderer.invoke('open-baidu')
})
const api: IPCAPI = {
openBaidu: () => ipcRenderer.invoke('open-baidu'),
contextBridge.exposeInMainWorld('api', {
versions: process.versions,
external: {
open: (url: string) => ipcRenderer.invoke('external-open', url)
},
window: {
minimize: () => ipcRenderer.send('window-min'),
maximize: () => ipcRenderer.send('window-max'),
close: () => ipcRenderer.send('window-close')
},
app: {
setFrameless: (route?: string) => ipcRenderer.invoke('app:set-frameless', route)
},
tabs: {
create: (url?: string) => ipcRenderer.invoke('tab:create', url),
list: () => ipcRenderer.invoke('tab:list'),
@@ -31,5 +34,38 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.on(event, listener)
return () => ipcRenderer.removeListener(event, listener)
}
}
})
},
// 通过 IPC 调用主进程
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
// 同步调用
invoke: (channel: IPCChannel, ...args: any[]) => ipcRenderer.sendSync('ipc:invoke', channel, ...args),
// 异步调用
invokeAsync: (channel: IPCChannel, ...args: any[]) => {
try {
ipcRenderer.invoke('ipc:invokeAsync', channel, ...args)
} catch (error) {
throw error
}
},
// 监听主进程消息
on: (event: IPCChannel, callback: (...args: any[]) => void) => {
const subscription = (_event: any, ...args: any[]) => callback(...args)
ipcRenderer.on(event, subscription)
return () => ipcRenderer.removeListener(event, subscription)
},
// 发送消息到主进程
send: (channel: IPCChannel, ...args: any[]) => ipcRenderer.send(channel, ...args),
// 获取窗口ID
getCurrentWindowId: () => ipcRenderer.sendSync(IPCChannel.GET_WINDOW_ID),
// 发送日志
logToMain: (logLevel: string, message: string) => ipcRenderer.send('log-to-main', logLevel, message),
}
contextBridge.exposeInMainWorld('ipcAPI', api)

2
src/env.d.ts vendored
View File

@@ -1,2 +0,0 @@
declare module "@stores/counter";
declare module "@utils/request";

View File

@@ -1,13 +0,0 @@
import { ipcMain } from 'electron'
import { TabManager } from '../controller/tab-manager'
export const registerTabIpc = (tabs: TabManager) => {
ipcMain.handle('tab:create', (_e, url?: string) => tabs.create(url))
ipcMain.handle('tab:list', () => tabs.list())
ipcMain.handle('tab:navigate', (_e, payload: { tabId: string; url: string }) => tabs.navigate(payload.tabId, payload.url))
ipcMain.handle('tab:reload', (_e, tabId: string) => tabs.reload(tabId))
ipcMain.handle('tab:back', (_e, tabId: string) => tabs.goBack(tabId))
ipcMain.handle('tab:forward', (_e, tabId: string) => tabs.goForward(tabId))
ipcMain.handle('tab:switch', (_e, tabId: string) => tabs.switch(tabId))
ipcMain.handle('tab:close', (_e, tabId: string) => tabs.close(tabId))
}

View File

@@ -0,0 +1,47 @@
import type { PluginOption } from 'vite'
import * as bytenode from 'bytenode'
import * as path from 'path'
import * as fs from 'fs-extra'
export interface ElectronBytecodeOptions {
entry?: string
keepSource?: boolean
}
export default function electronBytecode(options?: ElectronBytecodeOptions): PluginOption {
return {
name: 'vite-plugin-electron-encrypt',
apply: 'build',
async closeBundle() {
if (process.env.NODE_ENV !== 'production') return
const electronVersion = require('electron/package.json').version
const nodeVersion = process.version
const versionData = {
node: nodeVersion,
electron: electronVersion,
platform: process.platform,
arch: process.arch,
buildTime: new Date().toISOString()
}
const versionPath = path.resolve(process.cwd(), 'dist', 'version.json')
fs.ensureDirSync(path.dirname(versionPath))
fs.writeJsonSync(versionPath, versionData, { spaces: 2 })
const entryPath = path.resolve(process.cwd(), options?.entry || '.vite/build/main.js')
if (!fs.pathExistsSync(entryPath)) return
const outputPath = entryPath.replace(/\.js$/, '.jsc')
const backupPath = `${entryPath}.bak`
fs.copyFileSync(entryPath, backupPath)
await bytenode.compileFile({ filename: entryPath, output: outputPath })
if (!options?.keepSource) {
fs.removeSync(entryPath)
}
}
}
}

View File

@@ -1,45 +0,0 @@
/**
* This file will automatically be loaded by vite and run in the "renderer" context.
* To learn more about the differences between the "main" and the "renderer" context in
* Electron, visit:
*
* https://electronjs.org/docs/tutorial/process-model
*
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
* in a renderer process, please be aware of potential security implications. You can read
* more about security risks here:
*
* https://electronjs.org/docs/tutorial/security
*
* To enable Node.js integration in this file, open up `main.ts` and enable the `nodeIntegration`
* flag:
*
* ```
* // Create the browser window.
* mainWindow = new BrowserWindow({
* width: 800,
* height: 600,
* webPreferences: {
* nodeIntegration: true
* }
* });
* ```
*/
import "./index.css";
import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "./router";
import App from "./App.vue";
// 创建 Vue 应用实例
const app = createApp(App);
// 使用 Pinia 状态管理
app.use(createPinia());
// 使用 Vue Router
app.use(router);
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -5,7 +5,7 @@
</template>
<script setup lang="ts">
import { useCounterStore } from "@stores/counter";
import { useCounterStore } from "@store/counter";
// 使 Pinia store
const counterStore = useCounterStore();

2
src/renderer/env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare module "@store/counter";
declare module "@utils/request";

17
src/renderer/main.ts Normal file
View File

@@ -0,0 +1,17 @@
import "./index.css";
import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "./router";
import App from "./App.vue";
// 创建 Vue 应用实例
const app = createApp(App);
// 使用 Pinia 状态管理
app.use(createPinia());
// 使用 Vue Router
app.use(router);
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
import { isEqual } from 'lodash-es'
export const useSharedStore = defineStore('shared', {
state: () => ({
sharedData: {},
}),
actions: {
updateSharedData(data: any) {
this.sharedData = Object.assign(this.sharedData, data)
const cloneData = JSON.parse(JSON.stringify(this.sharedData))
if (!isEqual(this.sharedData, data)) {
// 同步状态到主进程
try {
} catch (error) {
}
}
}
}
})

View File

@@ -38,26 +38,26 @@ const refreshActiveAddress = () => {
}
const syncList = async () => {
const list: TabInfo[] = await (window as any).api.tabs.list()
const list: TabInfo[] = await (window as any).ipcAPI.tabs.list()
tabs.splice(0, tabs.length, ...list)
if (!activeId.value && list.length > 0) activeId.value = list[0].id
refreshActiveAddress()
}
const onNewTab = async () => {
const info: TabInfo = await (window as any).api.tabs.create('about:blank')
const info: TabInfo = await (window as any).ipcAPI.tabs.create('about:blank')
activeId.value = info.id
}
const onSwitch = async (id: string) => {
await (window as any).api.tabs.switch(id)
await (window as any).ipcAPI.tabs.switch(id)
activeId.value = id
refreshActiveAddress()
}
const onCloseTab = async () => {
if (!activeId.value) return
await (window as any).api.tabs.close(activeId.value)
await (window as any).ipcAPI.tabs.close(activeId.value)
}
const normalizeUrl = (u: string) => {
@@ -71,46 +71,46 @@ const onNavigate = async () => {
if (!activeId.value || !address.value) return
const url = normalizeUrl(address.value)
if (!url) return
await (window as any).api.tabs.navigate(activeId.value, url)
await (window as any).ipcAPI.tabs.navigate(activeId.value, url)
}
const onReload = async () => {
if (!activeId.value) return
await (window as any).api.tabs.reload(activeId.value)
await (window as any).ipcAPI.tabs.reload(activeId.value)
}
const onBack = async () => {
if (!activeId.value) return
await (window as any).api.tabs.back(activeId.value)
await (window as any).ipcAPI.tabs.back(activeId.value)
}
const onForward = async () => {
if (!activeId.value) return
await (window as any).api.tabs.forward(activeId.value)
await (window as any).ipcAPI.tabs.forward(activeId.value)
}
const onCloseTabId = async (id: string) => {
await (window as any).api.tabs.close(id)
await (window as any).ipcAPI.tabs.close(id)
}
const onCloseWindow = () => (window as any).api.window.close()
const onMinimizeWindow = () => (window as any).api.window.minimize()
const onMaximizeWindow = () => (window as any).api.window.maximize()
const onCloseWindow = () => (window as any).ipcAPI.window.close()
const onMinimizeWindow = () => (window as any).ipcAPI.window.minimize()
const onMaximizeWindow = () => (window as any).ipcAPI.window.maximize()
onMounted(async () => {
await syncList()
;(window as any).api.tabs.on('tab-created', (info: TabInfo) => {
;(window as any).ipcAPI.tabs.on('tab-created', (info: TabInfo) => {
const i = tabs.findIndex(t => t.id === info.id)
if (i === -1) tabs.push(info)
activeId.value = info.id
refreshActiveAddress()
})
;(window as any).api.tabs.on('tab-updated', (info: TabInfo) => {
;(window as any).ipcAPI.tabs.on('tab-updated', (info: TabInfo) => {
const i = tabs.findIndex(t => t.id === info.id)
if (i >= 0) tabs[i] = info
if (activeId.value === info.id) refreshActiveAddress()
})
;(window as any).api.tabs.on('tab-closed', ({ tabId }: { tabId: string }) => {
;(window as any).ipcAPI.tabs.on('tab-closed', ({ tabId }: { tabId: string }) => {
const i = tabs.findIndex(t => t.id === tabId)
if (i >= 0) tabs.splice(i, 1)
if (activeId.value === tabId) {
@@ -119,7 +119,7 @@ onMounted(async () => {
refreshActiveAddress()
}
})
;(window as any).api.tabs.on('tab-switched', ({ tabId }: { tabId: string }) => {
;(window as any).ipcAPI.tabs.on('tab-switched', ({ tabId }: { tabId: string }) => {
activeId.value = tabId
refreshActiveAddress()
})

View File

@@ -103,7 +103,7 @@ tab-switched: { tabId: TabId }
1. 核心能力(主进程 + IPC + 预加载)
- 实现 `TabManager` 与全部导航方法
- 注册 IPCtabs/bookmarks/plugins与广播
- 预加载扩展 `window.api.tabs/*`、事件订阅封装
- 预加载扩展 `window.ipcAPI.tabs/*`、事件订阅封装
2. 渲染层界面
- 新建 `BrowserLayout` 与基础组件TabBar、AddressBar、Controls
- 打通地址栏与导航;同步标题与加载状态

View File

@@ -14,7 +14,10 @@
<script setup lang="ts">
const openBaidu = () => {
(window as any).electronAPI?.openBaidu()
(window as any).ipcAPI?.openBaidu()
//
(window as any).ipcAPI?.logToMain('info', '打开百度')
}
</script>

View File

@@ -53,7 +53,7 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { login as apiLogin } from "@/api/login";
import { login as apiLogin } from "@/renderer/api/login";
const router = useRouter();
const form = reactive({ account: "", password: "" });
@@ -80,7 +80,7 @@ const onSubmit = async () => {
// const token = res && (res.token || res.data?.token || res.access_token);
// if (!token) throw new Error("");
// localStorage.setItem("token", token);
await (window as any).api.app.setFrameless('/browser')
await (window as any).ipcAPI.app.setFrameless('/browser')
} finally {
// loading.value = false;
}

View File

@@ -0,0 +1,56 @@
export enum IPCChannel {
APP_MINIMIZE ='app:minimize',
APP_MAXIMIZE ='app:maximize',
APP_QUIT ='app:quit',
FILE_READ = 'file:read',
FILE_WRITE = 'file:write',
GET_WINDOW_ID='get-window-id',
CUSTOM_EVENT ='custom:event',
TIME_UPDATE = 'time:update'
}
// 定义每个通道的参数和返回值类型
export interface IPCTypings {
// 同步通信
[IPCChannel.APP_MINIMIZE]: {
params: [window:number]
return: {success: boolean, error?: string}
}
[IPCChannel.APP_MAXIMIZE]: {
params: [window:number]
return: {success: boolean, error?: string}
}
[IPCChannel.GET_WINDOW_ID]: {
params: []
return: number
}
// 异步通信
[IPCChannel.FILE_READ]: {
params: [filePath: string]
return: Promise<{success: boolean, data?: string, error?: string}>
}
[IPCChannel.FILE_WRITE]: {
params: [filePath: string, content: string]
return: Promise<{success: boolean, error?: string}>
}
// 事件通信
[IPCChannel.TIME_UPDATE]: {
params: [time: string]
return: void
}
[IPCChannel.CUSTOM_EVENT]: {
params: [message: string]
return: void
}
}
// 定义IPC API 接口
export interface IPCAPI {
invoke<T extends keyof IPCTypings>(channel: T, ...args: IPCTypings[T]['params']): IPCTypings[T]['return'],
invokeAsync<T extends keyof IPCTypings>(channel: T, ...args: IPCTypings[T]['params']): IPCTypings[T]['return'],
on<T extends keyof IPCTypings>(channel: T, callback: (...args: IPCTypings[T]['params']) => void): () => void
send<T extends keyof IPCTypings>(channel: T, ...args: IPCTypings[T]['params']): void,
getCurrentWindowId(): number
}