electron系列2:搭建专业Electron开发环境

一、为什么需要专业架构?

很多Electron教程的刚开始都是这样:

javascript 复制代码
npm init -y
npm install electron
# 然后直接写代码,用 electron . 运行

这种方式对于"Hello World"没问题,但当你开始做真正的项目时,会遇到一堆问题:

我们来搭建一个企业级Electron项目架构,解决上述所有问题。

二、整体架构设计

2.1 三层架构图

2.2 项目目录结构

javascript 复制代码
my-electron-app/
├── packages/                          # Monorepo 子包
│   ├── main/                          # 主进程
│   │   ├── src/
│   │   │   ├── index.ts               # 入口文件
│   │   │   ├── window/                # 窗口管理
│   │   │   │   ├── MainWindow.ts
│   │   │   │   └── WindowManager.ts
│   │   │   ├── ipc/                   # IPC 处理器
│   │   │   │   ├── fileHandlers.ts
│   │   │   │   └── systemHandlers.ts
│   │   │   ├── native/                # 原生能力
│   │   │   │   ├── tray.ts
│   │   │   │   └── shortcut.ts
│   │   │   └── utils/                 # 工具函数
│   │   │       └── logger.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   ├── preload/                       # 预加载脚本
│   │   ├── src/
│   │   │   ├── index.ts               # 入口
│   │   │   └── api/                   # 暴露的API
│   │   │       ├── file.ts
│   │   │       ├── system.ts
│   │   │       └── types.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   └── renderer/                      # 渲染进程(Vue3/React)
│       ├── src/
│       │   ├── main.ts                # 入口
│       │   ├── App.vue                # 根组件
│       │   ├── components/            # UI组件
│       │   ├── views/                 # 页面
│       │   ├── router/                # 路由
│       │   ├── store/                 # 状态管理
│       │   └── styles/                # 全局样式
│       ├── index.html
│       ├── package.json
│       └── tsconfig.json
│
├── electron-builder.json              # 打包配置
├── package.json                       # 根配置(workspace)
├── pnpm-workspace.yaml                # pnpm workspace配置
├── tsconfig.base.json                 # 共享TS配置
└── .eslintrc.json                     # 代码规范

三、环境搭建(分步详解)

3.1 初始化 Monorepo

我们使用 pnpm workspace 来管理多包结构(比npm和yarn更快,磁盘占用更少)。

javascript 复制代码
# 创建项目目录
mkdir my-electron-app && cd my-electron-app

# 初始化根package.json
pnpm init

# 创建子包目录
mkdir -p packages/main packages/preload packages/renderer

根目录 package.json

javascript 复制代码
{
  "name": "my-electron-app",
  "version": "1.0.0",
  "private": true,
  "description": "Enterprise Electron Application",
  "scripts": {
    "dev": "pnpm run -F main dev",
    "build": "pnpm run -F main build",
    "build:all": "pnpm run -F main build && pnpm run -F preload build && pnpm run -F renderer build",
    "lint": "eslint . --ext .ts,.tsx,.vue",
    "type-check": "pnpm run -F main type-check && pnpm run -F preload type-check && pnpm run -F renderer type-check"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "eslint": "^8.55.0",
    "@typescript-eslint/eslint-plugin": "^6.13.0",
    "@typescript-eslint/parser": "^6.13.0"
  },
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=8.0.0"
  }
}

pnpm-workspace.yaml

javascript 复制代码
packages:
  - 'packages/*'

3.2 主进程配置(TypeScript + Electron)

packages/main/package.json

javascript 复制代码
{
  "name": "main",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "dev": "electron-vite dev --watch",
    "build": "electron-vite build",
    "type-check": "tsc --noEmit",
    "start": "electron dist/index.js"
  },
  "dependencies": {
    "electron": "^28.0.0",
    "electron-updater": "^6.1.0"
  },
  "devDependencies": {
    "electron-vite": "^2.0.0",
    "vite": "^5.0.0",
    "@types/node": "^20.10.0"
  }
}

packages/main/tsconfig.json

javascript 复制代码
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "./dist",
    "rootDir": "./src",
    "noEmit": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

packages/main/src/index.ts(主进程入口):

javascript 复制代码
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import { initTray } from './native/tray'
import { initShortcuts } from './native/shortcut'
import { setupFileHandlers } from './ipc/fileHandlers'
import { setupSystemHandlers } from './ipc/systemHandlers'

// 开发环境判断
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged

// 全局窗口引用
let mainWindow: BrowserWindow | null = null

/**
 * 创建主窗口
 */
function createMainWindow(): BrowserWindow {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    show: false,  // 先隐藏,ready-to-show再显示
    frame: true,
    titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
    webPreferences: {
      // 预加载脚本路径(生产/开发环境不同)
      preload: path.join(__dirname, '../preload/dist/index.js'),
      contextIsolation: true,   // 必须开启:上下文隔离
      nodeIntegration: false,   // 必须关闭:禁止渲染进程直接访问Node
      sandbox: false,           // 可选的沙箱模式
    },
  })

  // 加载页面
  if (isDev) {
    // 开发环境:加载Vite开发服务器
    win.loadURL('http://localhost:5173')
    win.webContents.openDevTools()
  } else {
    // 生产环境:加载打包后的文件
    win.loadFile(path.join(__dirname, '../renderer/dist/index.html'))
  }

  // 窗口准备就绪后显示
  win.once('ready-to-show', () => {
    win.show()
    
    // 如果是macOS,将窗口置于前台
    if (process.platform === 'darwin') {
      app.dock.show()
    }
  })

  // 窗口关闭时清理引用
  win.on('closed', () => {
    mainWindow = null
  })

  return win
}

/**
 * 应用启动
 */
app.whenReady().then(() => {
  // 1. 创建主窗口
  mainWindow = createMainWindow()
  
  // 2. 注册IPC处理器
  setupFileHandlers()
  setupSystemHandlers()
  
  // 3. 初始化系统托盘
  initTray(mainWindow)
  
  // 4. 注册全局快捷键
  initShortcuts(mainWindow)
  
  // 5. macOS:点击dock图标时重新创建窗口
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      mainWindow = createMainWindow()
    }
  })
})

/**
 * 所有窗口关闭时的行为
 */
app.on('window-all-closed', () => {
  // 除了macOS,其他平台退出应用
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

/**
 * 开发环境:安装Vue Devtools
 */
if (isDev) {
  import('electron-devtools-installer').then(({ default: installExtension, VUEJS_DEVTOOLS }) => {
    installExtension(VUEJS_DEVTOOLS)
      .then((name) => console.log(`Added Extension: ${name}`))
      .catch((err) => console.log('An error occurred: ', err))
  })
}

3.3 预加载脚本(安全桥接层)

packages/preload/package.json

javascript 复制代码
{
  "name": "preload",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch",
    "type-check": "tsc --noEmit"
  },
  "devDependencies": {
    "vite": "^5.0.0",
    "@types/node": "^20.10.0"
  }
}

packages/preload/vite.config.ts

javascript 复制代码
import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      formats: ['cjs'],
      fileName: () => 'index.js',
    },
    rollupOptions: {
      external: ['electron'],
      output: {
        inlineDynamicImports: true,
      },
    },
    outDir: 'dist',
    emptyOutDir: true,
    sourcemap: process.env.NODE_ENV === 'development',
  },
})

packages/preload/src/index.ts

javascript 复制代码
import { contextBridge, ipcRenderer } from 'electron'

// ============== 定义API类型 ==============
export interface ElectronAPI {
  // 文件操作
  file: {
    readTextFile: (path: string) => Promise<string>
    writeTextFile: (path: string, content: string) => Promise<void>
    readBinaryFile: (path: string) => Promise<ArrayBuffer>
    selectFile: (options?: FileSelectOptions) => Promise<string | null>
    selectDirectory: () => Promise<string | null>
  }
  
  // 系统信息
  system: {
    getPlatform: () => 'win32' | 'darwin' | 'linux'
    getVersion: () => string
    getScreenSize: () => { width: number; height: number }
    openExternal: (url: string) => Promise<void>
    showNotification: (title: string, body: string) => void
  }
  
  // 窗口操作
  window: {
    minimize: () => void
    maximize: () => void
    close: () => void
    isMaximized: () => Promise<boolean>
  }
  
  // 事件监听
  onThemeChange: (callback: (theme: 'light' | 'dark') => void) => () => void
}

interface FileSelectOptions {
  filters?: { name: string; extensions: string[] }[]
  defaultPath?: string
}

// ============== 实现API ==============
const electronAPI: ElectronAPI = {
  // 文件操作
  file: {
    readTextFile: (path: string) => ipcRenderer.invoke('file:read-text', path),
    
    writeTextFile: (path: string, content: string) => 
      ipcRenderer.invoke('file:write-text', path, content),
    
    readBinaryFile: (path: string) => ipcRenderer.invoke('file:read-binary', path),
    
    selectFile: (options?: FileSelectOptions) => 
      ipcRenderer.invoke('dialog:select-file', options),
    
    selectDirectory: () => ipcRenderer.invoke('dialog:select-directory'),
  },
  
  // 系统信息
  system: {
    getPlatform: () => process.platform as 'win32' | 'darwin' | 'linux',
    
    getVersion: () => process.versions.electron,
    
    getScreenSize: () => {
      const { screen } = require('electron')
      const { width, height } = screen.getPrimaryDisplay().workAreaSize
      return { width, height }
    },
    
    openExternal: (url: string) => ipcRenderer.invoke('system:open-external', url),
    
    showNotification: (title: string, body: string) => {
      new Notification(title, { body })
    },
  },
  
  // 窗口操作
  window: {
    minimize: () => ipcRenderer.send('window:minimize'),
    maximize: () => ipcRenderer.send('window:maximize'),
    close: () => ipcRenderer.send('window:close'),
    isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
  },
  
  // 事件监听
  onThemeChange: (callback) => {
    const handler = (_event: any, theme: 'light' | 'dark') => callback(theme)
    ipcRenderer.on('system:theme-changed', handler)
    // 返回取消监听的函数
    return () => ipcRenderer.removeListener('system:theme-changed', handler)
  },
}

// 安全地暴露API给渲染进程
contextBridge.exposeInMainWorld('electronAPI', electronAPI)

// 类型声明(让TypeScript识别window.electronAPI)
declare global {
  interface Window {
    electronAPI: ElectronAPI
  }
}

3.4 渲染进程(Vue 3 + Vite)

packages/renderer/package.json

javascript 复制代码
{
  "name": "renderer",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "type-check": "vue-tsc --noEmit"
  },
  "dependencies": {
    "vue": "^3.4.0",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.5.0",
    "vite": "^5.0.0",
    "vue-tsc": "^1.8.0",
    "typescript": "^5.3.0"
  }
}

packages/renderer/vite.config.ts

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  base: './',  // 使用相对路径,适配Electron
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    strictPort: true,  // 端口被占用时直接退出
  },
  build: {
    outDir: 'dist',
    emptyOutDir: true,
    sourcemap: true,
    rollupOptions: {
      input: path.resolve(__dirname, 'index.html'),
    },
  },
})

packages/renderer/index.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Electron + Vue 3 桌面应用</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

packages/renderer/src/main.ts

javascript 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './styles/global.css'

// 创建应用
const app = createApp(App)

// 使用插件
app.use(createPinia())
app.use(router)

// 挂载
app.mount('#app')

// 开发环境下输出Electron API是否可用
if (import.meta.env.DEV) {
  console.log('Electron API available:', !!window.electronAPI)
}

packages/renderer/src/App.vue

html 复制代码
<template>
  <div class="app-container">
    <!-- 自定义标题栏(无边框窗口时使用) -->
    <div class="title-bar" v-if="isFullScreen">
      <div class="title-bar-drag-area"></div>
      <div class="window-controls">
        <button @click="minimizeWindow" class="control-btn">─</button>
        <button @click="maximizeWindow" class="control-btn">□</button>
        <button @click="closeWindow" class="control-btn close">✕</button>
      </div>
    </div>

    <!-- 主要内容区 -->
    <main class="main-content">
      <aside class="sidebar">
        <nav>
          <router-link to="/">首页</router-link>
          <router-link to="/files">文件管理</router-link>
          <router-link to="/settings">设置</router-link>
        </nav>
      </aside>
      
      <div class="content">
        <router-view />
      </div>
    </main>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const isFullScreen = ref(false)

// 窗口控制方法
const minimizeWindow = () => {
  window.electronAPI?.window.minimize()
}

const maximizeWindow = async () => {
  window.electronAPI?.window.maximize()
  const maximized = await window.electronAPI?.window.isMaximized()
  isFullScreen.value = maximized || false
}

const closeWindow = () => {
  window.electronAPI?.window.close()
}

// 监听系统主题变化
onMounted(() => {
  if (window.electronAPI?.onThemeChange) {
    window.electronAPI.onThemeChange((theme) => {
      document.documentElement.setAttribute('data-theme', theme)
    })
  }
  
  // 获取平台信息
  const platform = window.electronAPI?.system.getPlatform()
  console.log('Running on:', platform)
})
</script>

<style scoped>
.app-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: var(--bg-primary);
  color: var(--text-primary);
}

.title-bar {
  height: 32px;
  display: flex;
  justify-content: space-between;
  background: var(--bg-secondary);
  -webkit-app-region: drag;
}

.title-bar-drag-area {
  flex: 1;
  -webkit-app-region: drag;
}

.window-controls {
  display: flex;
  -webkit-app-region: no-drag;
}

.control-btn {
  width: 46px;
  height: 32px;
  border: none;
  background: transparent;
  color: var(--text-primary);
  cursor: pointer;
  font-size: 14px;
}

.control-btn:hover {
  background: var(--hover-bg);
}

.control-btn.close:hover {
  background: #e81123;
  color: white;
}

.main-content {
  display: flex;
  flex: 1;
  overflow: hidden;
}

.sidebar {
  width: 200px;
  background: var(--bg-secondary);
  padding: 16px;
  border-right: 1px solid var(--border-color);
}

.sidebar nav {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.sidebar a {
  color: var(--text-primary);
  text-decoration: none;
  padding: 8px 12px;
  border-radius: 6px;
}

.sidebar a:hover {
  background: var(--hover-bg);
}

.sidebar a.router-link-active {
  background: var(--primary-color);
  color: white;
}

.content {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}
</style>

3.5 IPC 处理器实现(主进程)

packages/main/src/ipc/fileHandlers.ts

javascript 复制代码
import { ipcMain, dialog } from 'electron'
import fs from 'fs/promises'
import path from 'path'

export function setupFileHandlers() {
  // 读取文本文件
  ipcMain.handle('file:read-text', async (_event, filePath: string) => {
    try {
      const content = await fs.readFile(filePath, 'utf-8')
      return content
    } catch (error) {
      console.error('Read file error:', error)
      throw new Error(`无法读取文件: ${error.message}`)
    }
  })

  // 写入文本文件
  ipcMain.handle('file:write-text', async (_event, filePath: string, content: string) => {
    try {
      await fs.writeFile(filePath, content, 'utf-8')
    } catch (error) {
      console.error('Write file error:', error)
      throw new Error(`无法写入文件: ${error.message}`)
    }
  })

  // 读取二进制文件
  ipcMain.handle('file:read-binary', async (_event, filePath: string) => {
    try {
      const buffer = await fs.readFile(filePath)
      return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
    } catch (error) {
      console.error('Read binary error:', error)
      throw new Error(`无法读取文件: ${error.message}`)
    }
  })

  // 选择文件对话框
  ipcMain.handle('dialog:select-file', async (_event, options) => {
    const result = await dialog.showOpenDialog({
      properties: ['openFile'],
      filters: options?.filters,
      defaultPath: options?.defaultPath,
    })
    
    if (result.canceled) return null
    return result.filePaths[0]
  })

  // 选择目录对话框
  ipcMain.handle('dialog:select-directory', async () => {
    const result = await dialog.showOpenDialog({
      properties: ['openDirectory'],
    })
    
    if (result.canceled) return null
    return result.filePaths[0]
  })
}

packages/main/src/ipc/systemHandlers.ts

javascript 复制代码
import { ipcMain, shell, nativeTheme } from 'electron'

export function setupSystemHandlers() {
  // 打开外部链接
  ipcMain.handle('system:open-external', async (_event, url: string) => {
    await shell.openExternal(url)
  })

  // 监听系统主题变化(主动推送)
  nativeTheme.on('updated', () => {
    const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
    // 广播给所有渲染进程
    BrowserWindow.getAllWindows().forEach(win => {
      win.webContents.send('system:theme-changed', theme)
    })
  })
}

packages/main/src/ipc/windowHandlers.ts

javascript 复制代码
import { ipcMain, BrowserWindow } from 'electron'

export function setupWindowHandlers() {
  // 最小化窗口
  ipcMain.on('window:minimize', (event) => {
    const win = BrowserWindow.fromWebContents(event.sender)
    win?.minimize()
  })

  // 最大化/还原窗口
  ipcMain.on('window:maximize', (event) => {
    const win = BrowserWindow.fromWebContents(event.sender)
    if (win?.isMaximized()) {
      win.unmaximize()
    } else {
      win?.maximize()
    }
  })

  // 关闭窗口
  ipcMain.on('window:close', (event) => {
    const win = BrowserWindow.fromWebContents(event.sender)
    win?.close()
  })

  // 查询是否最大化
  ipcMain.handle('window:is-maximized', (event) => {
    const win = BrowserWindow.fromWebContents(event.sender)
    return win?.isMaximized() || false
  })
}

四、开发工作流配置

4.1 开发模式启动流程

4.2 调试配置(VSCode)

.vscode/launch.json

javascript 复制代码
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "args": ["${workspaceFolder}/packages/main/dist/index.js"],
      "outFiles": ["${workspaceFolder}/packages/main/dist/**/*.js"],
      "sourceMaps": true,
      "console": "integratedTerminal"
    },
    {
      "name": "Debug Renderer Process",
      "type": "chrome",
      "request": "attach",
      "port": 9222,
      "webRoot": "${workspaceFolder}/packages/renderer/src",
      "sourceMapPathOverrides": {
        "webpack:///./src/*": "${webRoot}/*"
      }
    }
  ],
  "compounds": [
    {
      "name": "Debug Full App",
      "configurations": ["Debug Main Process", "Debug Renderer Process"]
    }
  ]
}

4.3 环境变量管理

packages/main/src/utils/env.ts

javascript 复制代码
// 环境变量类型
export interface EnvConfig {
  NODE_ENV: 'development' | 'production' | 'test'
  APP_NAME: string
  APP_VERSION: string
  API_BASE_URL?: string
  LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error'
}

// 加载环境变量
export function loadEnv(): EnvConfig {
  const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
  
  return {
    NODE_ENV: isDev ? 'development' : 'production',
    APP_NAME: process.env.APP_NAME || 'MyElectronApp',
    APP_VERSION: process.env.npm_package_version || '1.0.0',
    API_BASE_URL: process.env.API_BASE_URL,
    LOG_LEVEL: (process.env.LOG_LEVEL as EnvConfig['LOG_LEVEL']) || (isDev ? 'debug' : 'info'),
  }
}

.env.development

javascript 复制代码
NODE_ENV=development
APP_NAME=MyElectronAppDev
API_BASE_URL=http://localhost:3000
LOG_LEVEL=debug

.env.production

javascript 复制代码
NODE_ENV=production
APP_NAME=MyElectronApp
LOG_LEVEL=info

五、TypeScript 配置详解

5.1 共享基础配置

tsconfig.base.json

javascript 复制代码
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

5.2 全局类型声明

packages/renderer/src/types/global.d.ts

javascript 复制代码
// 扩展 Window 接口
declare global {
  interface Window {
    electronAPI: import('preload').ElectronAPI
  }
  
  // 环境变量类型
  interface ImportMetaEnv {
    VITE_API_URL?: string
    VITE_APP_TITLE?: string
  }
  
  interface ImportMeta {
    readonly env: ImportMetaEnv
  }
}

// 声明 Vue 文件模块
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

// 声明静态资源模块
declare module '*.css'
declare module '*.scss'
declare module '*.png'
declare module '*.jpg'
declare module '*.svg'

六、代码规范与Git Hooks

6.1 ESLint配置

.eslintrc.json

javascript 复制代码
{
  "root": true,
  "env": {
    "node": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:vue/vue3-recommended"
  ],
  "parser": "vue-eslint-parser",
  "parserOptions": {
    "parser": "@typescript-eslint/parser",
    "ecmaVersion": 2021,
    "sourceType": "module"
  },
  "rules": {
    "@typescript-eslint/no-explicit-any": "warn",
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "vue/multi-word-component-names": "off",
    "vue/no-v-html": "warn"
  },
  "overrides": [
    {
      "files": ["packages/main/**/*.ts"],
      "env": {
        "node": true
      }
    },
    {
      "files": ["packages/preload/**/*.ts"],
      "env": {
        "node": true
      }
    },
    {
      "files": ["packages/renderer/**/*.ts", "packages/renderer/**/*.vue"],
      "env": {
        "browser": true
      }
    }
  ]
}

6.2 Husky + lint-staged

.husky/pre-commit

javascript 复制代码
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

package.json(添加lint-staged配置):

javascript 复制代码
{
  "lint-staged": {
    "*.{ts,tsx,vue}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

七、打包配置(electron-builder)

electron-builder.json

javascript 复制代码
{
  "appId": "com.yourcompany.yourapp",
  "productName": "MyElectronApp",
  "directories": {
    "output": "release"
  },
  "files": [
    "packages/main/dist/**/*",
    "packages/preload/dist/**/*",
    "packages/renderer/dist/**/*"
  ],
  "win": {
    "target": ["nsis", "portable"],
    "icon": "build/icon.ico"
  },
  "mac": {
    "target": ["dmg", "zip"],
    "icon": "build/icon.icns",
    "category": "public.app-category.productivity"
  },
  "linux": {
    "target": ["AppImage", "deb"],
    "icon": "build/icon.png",
    "category": "Utility"
  },
  "nsis": {
    "oneClick": false,
    "allowToChangeInstallationDirectory": true,
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true
  },
  "publish": {
    "provider": "github",
    "releaseType": "release"
  }
}

八、完整命令速查表

命令 作用 使用场景
pnpm dev 启动开发环境 日常开发
pnpm build:all 构建所有包 准备打包前
pnpm type-check 类型检查 提交代码前
pnpm lint 代码规范检查 CI/CD流程
pnpm run -F main dev 单独开发主进程 调试主进程
pnpm run -F renderer dev 单独开发渲染进程 调试UI
npm run dist:win 打包Windows安装包 发布Windows版
npm run dist:mac 打包macOS安装包 发布Mac版

九、常见问题与解决方案

问题1:预加载脚本路径在生产环境找不到

解决方案:

javascript 复制代码
// 使用动态路径解析
const getPreloadPath = () => {
  if (process.env.NODE_ENV === 'development') {
    return path.join(__dirname, '../../preload/dist/index.js')
  }
  // 生产环境:打包后的路径
  return path.join(process.resourcesPath, 'preload/dist/index.js')
}

问题2:渲染进程无法访问window.electronAPI

检查清单:

  • 确保 contextIsolation: true

  • 确保预加载脚本正确编译并存在

  • 检查控制台是否有报错

  • 确认 contextBridge.exposeInMainWorld 调用成功

问题3:Vite HMR 不生效

解决方案:

javascript 复制代码
// 在 main/index.ts 中添加
if (isDev) {
  // 监听Vite重载事件
  win.webContents.on('did-fail-load', () => {
    win.loadURL('http://localhost:5173')
  })
}
相关推荐
酒鼎2 小时前
学习笔记(12-02)事件循环 - 实战案例 —⭐
前端·javascript
小恰学逆向3 小时前
【爬虫JS逆向之旅】某球网参数“md5__1038”逆向
javascript·爬虫
竹林8183 小时前
从“连接失败”到丝滑登录:我用 ethers.js v6 搞定 MetaMask 钱包连接的全过程
前端·javascript
前端那点事3 小时前
前端必看!JS高频实用案例(单行代码+实战场景+十大排序)
javascript
前端Hardy4 小时前
前端开发效率翻倍:15个超级实用的工具函数,直接复制进项目(建议收藏)
前端·javascript·面试
前端Hardy4 小时前
Vue 项目必备:10 个高频实用自定义指令,直接复制即用(Vue2 / Vue3 通用)
前端·javascript·vue.js
h_jQuery4 小时前
uniapp使用canvas实现逐字书写任意文字内容,后合成一张图片提交
前端·javascript·uni-app
懒大王95274 小时前
Vue 2 与 Vue 3 的区别
前端·javascript·vue.js
xuankuxiaoyao4 小时前
vue.js 实践--侦听器和样式绑定
前端·javascript·vue.js