mcpField
E
electron-base

electron-base

github.com/jezweb开发1.1k263 次安装3 周前更新
代码仓库

关于 electron-base

electron-base 是面向 Electron 桌面应用开发者的技能指南,涵盖 Electron 33、Vite、React 和 TypeScript 的项目搭建。它重点说明进程隔离、类型安全 IPC、OAuth、安全配置及打包流程。

主要功能

  1. 01Electron 与 Vite 项目结构
  2. 02主进程与渲染进程分离
  3. 03类型安全的 IPC 模式
  4. 04安全设置与 OAuth 示例
  5. 05原生模块兼容与应用打包

适用场景

  1. 01创建 React 与 TypeScript 桌面应用
  2. 02为渲染进程设计安全 IPC 接口
  3. 03实现自定义协议 OAuth 回调
  4. 04排查原生模块 ABI 或沙箱问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill electron-base

安装后在 Claude Code 中输入/electron-base确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
references/
security-checklist.md
rules/
electron-base.md
templates/
electron-builder.json
main.ts
preload.ts
vite.config.ts
templates/ipc-handlers/
auth.ts
store.ts

详细介绍

Electron 基础技能

使用 Electron 33、Vite、React 和 TypeScript 构建安全、现代的桌面应用程序。

快速开始

1. 初始化项目
# Create Vite project
npm create vite@latest my-app -- --template react-ts
cd my-app

# Install Electron dependencies
npm install electron electron-store
npm install -D vite-plugin-electron vite-plugin-electron-renderer electron-builder
2. 项目结构
my-app/
├── electron/
│   ├── main.ts           # Main process entry
│   ├── preload.ts        # Preload script (contextBridge)
│   └── ipc-handlers/     # Modular IPC handlers
│       ├── auth.ts
│       └── store.ts
├── src/                  # React app (renderer)
├── vite.config.ts        # Dual-entry Vite config
├── electron-builder.json # Build config
└── package.json
3. 更新 Package.json
{
  "main": "dist-electron/main.mjs",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "electron .",
    "package": "electron-builder"
  }
}

架构模式

主进程与渲染进程分离
┌─────────────────────────────────────────────────────────────┐
│                     MAIN PROCESS                            │
│  (Node.js + Electron APIs)                                  │
│  - File system access                                       │
│  - Native modules (better-sqlite3)                          │
│  - System dialogs                                           │
│  - Protocol handlers                                        │
└─────────────────────┬───────────────────────────────────────┘
                      │ IPC (invoke/handle)
                      │ Events (send/on)
┌─────────────────────▼───────────────────────────────────────┐
│                   PRELOAD SCRIPT                            │
│  (contextBridge.exposeInMainWorld)                          │
│  - Type-safe API exposed to renderer                        │
│  - No direct ipcRenderer exposure                           │
└─────────────────────┬───────────────────────────────────────┘
                      │ window.electron.*
┌─────────────────────▼───────────────────────────────────────┐
│                  RENDERER PROCESS                           │
│  (Browser context - React app)                              │
│  - No Node.js APIs                                          │
│  - Uses window.electron.* for IPC                           │
└─────────────────────────────────────────────────────────────┘
类型安全的 IPC 模式

预加载脚本向渲染进程公开类型化 API:

// electron/preload.ts
export interface ElectronAPI {
  auth: {
    startOAuth: (provider: 'google' | 'github') => Promise<void>;
    getSession: () => Promise<Session | null>;
    logout: () => Promise<void>;
    onSuccess: (callback: (session: Session) => void) => () => void;
    onError: (callback: (error: string) => void) => () => void;
  };
  app: {
    getVersion: () => Promise<string>;
    openExternal: (url: string) => Promise<void>;
  };
}

// Expose to renderer
contextBridge.exposeInMainWorld('electron', electronAPI);

// Global type declaration
declare global {
  interface Window {
    electron: ElectronAPI;
  }
}

安全最佳实践

必须:启用上下文隔离

始终启用上下文隔离,并禁用 Node 集成:

// electron/main.ts
const mainWindow = new BrowserWindow({
  webPreferences: {
    preload: join(__dirname, 'preload.cjs'),
    contextIsolation: true,    // REQUIRED - isolates preload from renderer
    nodeIntegration: false,    // REQUIRED - no Node.js in renderer
    sandbox: false,            // May need to disable for native modules
  },
});
切勿:硬编码加密密钥
// WRONG - hardcoded key is a security vulnerability
const store = new Store({
  encryptionKey: 'my-secret-key',  // DO NOT DO THIS
});

// CORRECT - derive from machine ID
import { machineIdSync } from 'node-machine-id';

const store = new Store({
  encryptionKey: machineIdSync().slice(0, 32),  // Machine-unique key
});
沙盒权衡

better-sqlite3 等原生模块需要设置 sandbox: false。请记录此项权衡:

webPreferences: {
  sandbox: false, // Required for better-sqlite3 - document security trade-off
}

需要设置 sandbox: false 的模块:

  • better-sqlite3
  • node-pty
  • native-keymap

可在 sandbox: true 下运行的模块:

  • electron-store(纯 JS)
  • keytar(使用 Electron 的 safeStorage)

使用自定义协议处理器实现 OAuth

1. 注册协议(main.ts)
// In development, need to pass executable path
if (process.defaultApp) {
  if (process.argv.length >= 2) {
    app.setAsDefaultProtocolClient('myapp', process.execPath, [process.argv[1]]);
  }
} else {
  app.setAsDefaultProtocolClient('myapp');
}
2. 处理协议 URL
// Single instance lock (required for reliable protocol handling)
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', (_event, commandLine) => {
    const url = commandLine.find((arg) => arg.startsWith('myapp://'));
    if (url) handleProtocolUrl(url);
    if (mainWindow?.isMinimized()) mainWindow.restore();
    mainWindow?.focus();
  });
}

// macOS handles protocol differently
app.on('open-url', (_event, url) => {
  handleProtocolUrl(url);
});

function handleProtocolUrl(url: string) {
  const parsedUrl = new URL(url);

  if (parsedUrl.pathname.includes('/auth/callback')) {
    const token = parsedUrl.searchParams.get('token');
    const state = parsedUrl.searchParams.get('state');
    const error = parsedUrl.searchParams.get('error');

    if (error) {
      mainWindow?.webContents.send('auth:error', error);
    } else if (token && state) {
      handleAuthCallback(token, state)
        .then((session) => mainWindow?.webContents.send('auth:success', session))
        .catch((err) => mainWindow?.webContents.send('auth:error', err.message));
    }
  }
}
3. 通过状态验证防范 CSRF
// Start OAuth - generate and store state
ipcMain.handle('auth:start-oauth', async (_event, provider) => {
  const state = crypto.randomUUID();
  store.set('pendingState', state);

  const authUrl = `${BACKEND_URL}/api/auth/signin/${provider}?state=${state}`;
  await shell.openExternal(authUrl);
});

// Verify state on callback
export async function handleAuthCallback(token: string, state: string): Promise<Session> {
  const pendingState = store.get('pendingState');

  if (state !== pendingState) {
    throw new Error('State mismatch - possible CSRF attack');
  }

  store.set('pendingState', null);
  // ... rest of auth flow
}

原生模块兼容性

better-sqlite3

需要针对 Electron 的 Node ABI 重新构建:

# Install
npm install better-sqlite3

# Rebuild for Electron
npm install -D electron-rebuild
npx electron-rebuild -f -w better-sqlite3

Vite 配置——将原生模块设为外部依赖:

// vite.config.ts
electron({
  main: {
    entry: 'electron/main.ts',
    vite: {
      build: {
        rollupOptions: {
          external: ['electron', 'better-sqlite3', 'electron-store'],
        },
      },
    },
  },
});
electron-store

可在启用沙盒时运行,但加密密钥应根据机器信息派生:

import Store from 'electron-store';
import { machineIdSync } from 'node-machine-id';

interface StoreSchema {
  session: Session | null;
  settings: Settings;
}

const store = new Store<StoreSchema>({
  name: 'myapp-data',
  encryptionKey: machineIdSync().slice(0, 32),
  defaults: {
    session: null,
    settings: { theme: 'system' },
  },
});

构建与打包

electron-builder.json
{
  "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
  "appId": "com.yourcompany.myapp",
  "productName": "MyApp",
  "directories": {
    "output": "release"
  },
  "files": [
    "dist/**/*",
    "dist-electron/**/*"
  ],
  "mac": {
    "category": "public.app-category.productivity",
    "icon": "build/icon.icns",
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "entitlements": "build/entitlements.mac.plist",
    "entitlementsInherit": "build/entitlements.mac.plist",
    "target": [
      { "target": "dmg", "arch": ["x64", "arm64"] }
    ],
    "protocols": [
      { "name": "MyApp", "schemes": ["myapp"] }
    ]
  },
  "win": {
    "icon": "build/icon.ico",
    "target": [
      { "target": "nsis", "arch": ["x64"] }
    ]
  },
  "linux": {
    "icon": "build/icons",
    "target": ["AppImage"],
    "category": "Office"
  }
}
macOS 权限配置(build/entitlements.mac.plist)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
  <key>com.apple.security.cs.disable-library-validation</key>
  <true/>
</dict>
</plist>
构建命令
# Development
npm run dev

# Production build
npm run build

# Package for current platform
npm run package

# Package for specific platform
npx electron-builder --mac
npx electron-builder --win
npx electron-builder --linux

已知问题及预防方法

1. 预加载脚本中出现“Cannot read properties of undefined”

原因: 预加载脚本执行完成前就访问了 window.electron。

解决方法: 使用可选链或检查其是否存在:

// In React component
useEffect(() => {
  if (!window.electron?.auth) return;

  const unsubscribe = window.electron.auth.onSuccess((session) => {
    setSession(session);
  });

  return unsubscribe;
}, []);
2. NODE_MODULE_VERSION 不匹配

原因: 原生模块是针对与 Electron 所用版本不同的 Node.js 版本编译的。

解决方法:

# Rebuild native modules for Electron
npx electron-rebuild -f -w better-sqlite3

# Or add to package.json postinstall
"scripts": {
  "postinstall": "electron-rebuild"
}
3. OAuth 状态不匹配

原因: 状态未持久化,或在 OAuth 开始与回调之间丢失。

解决方法: 使用持久化存储(electron-store),而非内存:

// WRONG - state lost if app restarts
let pendingState: string | null = null;

// CORRECT - persisted storage
const store = new Store({ ... });
store.set('pendingState', state);
4. 沙盒与原生模块冲突

原因: 沙盒阻止加载原生 .node 文件。

解决方法: 禁用沙盒(并记录相关权衡),或使用纯 JS 替代方案:

webPreferences: {
  sandbox: false, // Required for better-sqlite3
  // Alternative: Use sql.js (WASM) if sandbox required
}
5. 维护双重身份验证系统的负担

原因: 配置了 better-auth,却使用手动 OAuth,容易造成混淆。

解决方法: 选择一种方案:

  • 完全使用 better-auth,或
  • 仅使用手动 OAuth(移除 better-auth)
6. 令牌过期但没有刷新机制

原因: 过期时间被硬编码,且没有刷新机制。

解决方法: 实现令牌刷新或滑动会话:

// Check expiration with buffer
const session = store.get('session');
const expiresAt = new Date(session.expiresAt);
const bufferMs = 5 * 60 * 1000; // 5 minutes

if (Date.now() > expiresAt.getTime() - bufferMs) {
  await refreshToken(session.token);
}
7. 空的 Catch 块掩盖故障

原因: 静默吞掉错误。

解决方法: 记录错误并区分错误类型:

// WRONG
try {
  await fetch(url);
} catch {
  // Silent failure - user has no idea what happened
}

// CORRECT
try {
  await fetch(url);
} catch (err) {
  if (err instanceof TypeError) {
    console.error('[Network] Offline or DNS failure:', err.message);
  } else {
    console.error('[Auth] Unexpected error:', err);
  }
  throw err; // Re-throw for caller to handle
}
8. 硬编码加密密钥

原因: 使用字符串字面量进行加密。

解决方法: 根据机器标识符派生密钥:

import { machineIdSync } from 'node-machine-id';

const store = new Store({
  encryptionKey: machineIdSync().slice(0, 32),
});

Vite 配置

完整的 vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import electron from 'vite-plugin-electron/simple';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
    electron({
      main: {
        entry: 'electron/main.ts',
        vite: {
          build: {
            outDir: 'dist-electron',
            rollupOptions: {
              external: ['electron', 'better-sqlite3', 'electron-store'],
              output: {
                format: 'es',
                entryFileNames: '[name].mjs',
              },
            },
          },
        },
      },
      preload: {
        input: 'electron/preload.ts',
        vite: {
          build: {
            outDir: 'dist-electron',
            rollupOptions: {
              output: {
                format: 'cjs',
                entryFileNames: '[name].cjs',
              },
            },
          },
        },
      },
      renderer: {},
    }),
  ],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  build: {
    outDir: 'dist',
  },
  optimizeDeps: {
    include: ['react', 'react-dom'],
  },
});

依赖项参考

生产依赖项
{
  "dependencies": {
    "electron-store": "^10.0.0",
    "electron-updater": "^6.3.0"
  },
  "optionalDependencies": {
    "better-sqlite3": "^11.0.0",
    "node-machine-id": "^1.1.12"
  }
}
开发依赖项
{
  "devDependencies": {
    "electron": "^33.0.0",
    "electron-builder": "^25.0.0",
    "electron-rebuild": "^3.2.9",
    "vite-plugin-electron": "^0.28.0",
    "vite-plugin-electron-renderer": "^0.14.0"
  }
}

安全检查清单

发布前,请确认:

  • webPreferences 中的 contextIsolation: true
  • webPreferences 中的 nodeIntegration: false
  • 没有硬编码的加密密钥
  • 已实现 OAuth 状态验证
  • IPC 通道名称中没有敏感数据
  • 外部链接在系统浏览器中打开(shell.openExternal)
  • 已为生产环境配置 CSP 标头
  • 已启用 macOS 强化运行时
  • 已配置代码签名(如需分发)
  • 没有掩盖错误的空 Catch 块

相关技能

  • better-auth - 用于与 Electron OAuth 配合的后端身份验证
  • cloudflare-worker-base - 用于构建后端 API
  • tailwind-v4-shadcn - 用于为渲染器 UI 设置样式

评论

支持 Markdown