electron-base by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill electron-base使用 Electron 33、Vite、React 和 TypeScript 构建安全、现代的桌面应用程序。
# 创建 Vite 项目
npm create vite@latest my-app -- --template react-ts
cd my-app
# 安装 Electron 依赖
npm install electron electron-store
npm install -D vite-plugin-electron vite-plugin-electron-renderer electron-builder
my-app/
├── electron/
│ ├── main.ts # 主进程入口
│ ├── preload.ts # 预加载脚本 (contextBridge)
│ └── ipc-handlers/ # 模块化 IPC 处理器
│ ├── auth.ts
│ └── store.ts
├── src/ # React 应用 (渲染器)
├── vite.config.ts # 双入口 Vite 配置
├── electron-builder.json # 构建配置
└── package.json
{
"main": "dist-electron/main.mjs",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "electron .",
"package": "electron-builder"
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
┌─────────────────────────────────────────────────────────────┐
│ 主进程 │
│ (Node.js + Electron APIs) │
│ - 文件系统访问 │
│ - 原生模块 (better-sqlite3) │
│ - 系统对话框 │
│ - 协议处理器 │
└─────────────────────┬───────────────────────────────────────┘
│ IPC (invoke/handle)
│ 事件 (send/on)
┌─────────────────────▼───────────────────────────────────────┐
│ 预加载脚本 │
│ (contextBridge.exposeInMainWorld) │
│ - 暴露给渲染器的类型安全 API │
│ - 不直接暴露 ipcRenderer │
└─────────────────────┬───────────────────────────────────────┘
│ window.electron.*
┌─────────────────────▼───────────────────────────────────────┐
│ 渲染器进程 │
│ (浏览器上下文 - React 应用) │
│ - 无 Node.js APIs │
│ - 使用 window.electron.* 进行 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>;
};
}
// 暴露给渲染器
contextBridge.exposeInMainWorld('electron', electronAPI);
// 全局类型声明
declare global {
interface Window {
electron: ElectronAPI;
}
}
始终启用上下文隔离并禁用 Node 集成:
// electron/main.ts
const mainWindow = new BrowserWindow({
webPreferences: {
preload: join(__dirname, 'preload.cjs'),
contextIsolation: true, // 必需 - 将预加载脚本与渲染器隔离
nodeIntegration: false, // 必需 - 渲染器中无 Node.js
sandbox: false, // 对于原生模块可能需要禁用
},
});
// 错误 - 硬编码密钥是安全漏洞
const store = new Store({
encryptionKey: 'my-secret-key', // 切勿这样做
});
// 正确 - 从机器 ID 派生
import { machineIdSync } from 'node-machine-id';
const store = new Store({
encryptionKey: machineIdSync().slice(0, 32), // 机器唯一密钥
});
像 better-sqlite3 这样的原生模块需要 sandbox: false。请记录此权衡:
webPreferences: {
sandbox: false, // better-sqlite3 所需 - 记录安全权衡
}
需要 sandbox: false 的模块:
可在 sandbox: true 下工作的模块:
// 在开发环境中,需要传递可执行文件路径
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('myapp', process.execPath, [process.argv[1]]);
}
} else {
app.setAsDefaultProtocolClient('myapp');
}
// 单实例锁 (可靠协议处理所需)
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 以不同方式处理协议
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));
}
}
}
// 启动 OAuth - 生成并存储状态
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);
});
// 在回调时验证状态
export async function handleAuthCallback(token: string, state: string): Promise<Session> {
const pendingState = store.get('pendingState');
if (state !== pendingState) {
throw new Error('状态不匹配 - 可能是 CSRF 攻击');
}
store.set('pendingState', null);
// ... 认证流程的其余部分
}
需要为 Electron 的 Node ABI 重新构建:
# 安装
npm install better-sqlite3
# 为 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'],
},
},
},
},
});
可在启用沙箱的情况下工作,但加密密钥应从机器派生:
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' },
},
});
{
"$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"
}
}
<?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>
# 开发
npm run dev
# 生产构建
npm run build
# 为当前平台打包
npm run package
# 为特定平台打包
npx electron-builder --mac
npx electron-builder --win
npx electron-builder --linux
原因: 在预加载完成前访问 window.electron。
修复: 使用可选链或检查是否存在:
// 在 React 组件中
useEffect(() => {
if (!window.electron?.auth) return;
const unsubscribe = window.electron.auth.onSuccess((session) => {
setSession(session);
});
return unsubscribe;
}, []);
原因: 原生模块为与 Electron 使用的 Node.js 版本不同的版本编译。
修复:
# 为 Electron 重新构建原生模块
npx electron-rebuild -f -w better-sqlite3
# 或添加到 package.json 的 postinstall
"scripts": {
"postinstall": "electron-rebuild"
}
原因: 状态未持久化或在 OAuth 启动和回调之间丢失。
修复: 使用持久化存储 (electron-store) 而非内存:
// 错误 - 如果应用重启,状态会丢失
let pendingState: string | null = null;
// 正确 - 持久化存储
const store = new Store({ ... });
store.set('pendingState', state);
原因: 沙箱阻止加载原生 .node 文件。
修复: 禁用沙箱(并记录权衡)或使用纯 JS 替代方案:
webPreferences: {
sandbox: false, // better-sqlite3 所需
// 替代方案:如果需要沙箱,使用 sql.js (WASM)
}
原因: 配置 better-auth 但使用手动 OAuth 会造成混淆。
修复: 选择一种方法:
原因: 硬编码过期时间且无刷新机制。
修复: 实现令牌刷新或滑动会话:
// 使用缓冲检查过期时间
const session = store.get('session');
const expiresAt = new Date(session.expiresAt);
const bufferMs = 5 * 60 * 1000; // 5 分钟
if (Date.now() > expiresAt.getTime() - bufferMs) {
await refreshToken(session.token);
}
原因: 静默吞没错误。
修复: 记录错误,区分错误类型:
// 错误
try {
await fetch(url);
} catch {
// 静默失败 - 用户不知道发生了什么
}
// 正确
try {
await fetch(url);
} catch (err) {
if (err instanceof TypeError) {
console.error('[网络] 离线或 DNS 失败:', err.message);
} else {
console.error('[认证] 意外错误:', err);
}
throw err; // 重新抛出供调用者处理
}
原因: 使用字符串字面量进行加密。
修复: 从机器标识符派生:
import { machineIdSync } from 'node-machine-id';
const store = new Store({
encryptionKey: machineIdSync().slice(0, 32),
});
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"
}
}
发布前,请验证:
contextIsolation: truenodeIntegration: falseshell.openExternal)每周安装数
262
代码仓库
GitHub 星标数
656
首次出现
2026年1月29日
安全审计
安装于
claude-code201
opencode183
gemini-cli179
codex167
cursor146
antigravity143
Build secure, modern desktop applications with Electron 33, Vite, React, and TypeScript.
# 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
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
{
"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 │
└─────────────────────────────────────────────────────────────┘
The preload script exposes a typed API to the renderer:
// 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;
}
}
Always enable context isolation and disable node integration:
// 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
});
Native modules like better-sqlite3 require sandbox: false. Document this trade-off:
webPreferences: {
sandbox: false, // Required for better-sqlite3 - document security trade-off
}
Modules requiring sandbox: false:
Modules working with sandbox: true:
// 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');
}
// 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));
}
}
}
// 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
}
Requires rebuilding for Electron's Node ABI:
# Install
npm install better-sqlite3
# Rebuild for Electron
npm install -D electron-rebuild
npx electron-rebuild -f -w better-sqlite3
Vite config - externalize native modules:
// vite.config.ts
electron({
main: {
entry: 'electron/main.ts',
vite: {
build: {
rollupOptions: {
external: ['electron', 'better-sqlite3', 'electron-store'],
},
},
},
},
});
Works with sandbox enabled, but encryption key should be machine-derived:
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' },
},
});
{
"$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"
}
}
<?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
Cause: Accessing window.electron before preload completes.
Fix: Use optional chaining or check for existence:
// In React component
useEffect(() => {
if (!window.electron?.auth) return;
const unsubscribe = window.electron.auth.onSuccess((session) => {
setSession(session);
});
return unsubscribe;
}, []);
Cause: Native module compiled for different Node.js version than Electron uses.
Fix:
# Rebuild native modules for Electron
npx electron-rebuild -f -w better-sqlite3
# Or add to package.json postinstall
"scripts": {
"postinstall": "electron-rebuild"
}
Cause: State not persisted or lost between OAuth start and callback.
Fix: Use persistent storage (electron-store) not memory:
// WRONG - state lost if app restarts
let pendingState: string | null = null;
// CORRECT - persisted storage
const store = new Store({ ... });
store.set('pendingState', state);
Cause: Sandbox prevents loading native .node files.
Fix: Disable sandbox (with documented trade-off) or use pure-JS alternatives:
webPreferences: {
sandbox: false, // Required for better-sqlite3
// Alternative: Use sql.js (WASM) if sandbox required
}
Cause: Configuring better-auth but using manual OAuth creates confusion.
Fix: Choose one approach:
Cause: Hardcoded expiration with no refresh mechanism.
Fix: Implement token refresh or sliding sessions:
// 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);
}
Cause: Swallowing errors silently.
Fix: Log errors, distinguish error types:
// 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
}
Cause: Using string literal for encryption.
Fix: Derive from machine identifier:
import { machineIdSync } from 'node-machine-id';
const store = new Store({
encryptionKey: machineIdSync().slice(0, 32),
});
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"
}
}
Before release, verify:
contextIsolation: true in webPreferencesnodeIntegration: false in webPreferencesshell.openExternal)Weekly Installs
262
Repository
GitHub Stars
656
First Seen
Jan 29, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code201
opencode183
gemini-cli179
codex167
cursor146
antigravity143
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装