关于 electron-base
electron-base 是面向 Electron 桌面应用开发者的技能指南,涵盖 Electron 33、Vite、React 和 TypeScript 的项目搭建。它重点说明进程隔离、类型安全 IPC、OAuth、安全配置及打包流程。
主要功能
- 01Electron 与 Vite 项目结构
- 02主进程与渲染进程分离
- 03类型安全的 IPC 模式
- 04安全设置与 OAuth 示例
- 05原生模块兼容与应用打包
适用场景
- 01创建 React 与 TypeScript 桌面应用
- 02为渲染进程设计安全 IPC 接口
- 03实现自定义协议 OAuth 回调
- 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.jsonreferences/security-checklist.mdrules/electron-base.mdtemplates/electron-builder.jsonmain.tspreload.tsvite.config.tstemplates/ipc-handlers/auth.tsstore.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 设置样式