重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
desktop-apps by miles990/claude-software-skills
npx skills add https://github.com/miles990/claude-software-skills --skill desktop-apps使用 Electron 和 Tauri 等 Web 技术构建跨平台桌面应用程序。
// main.ts
import { app, BrowserWindow, ipcMain, dialog, Menu } from 'electron';
import path from 'path';
let mainWindow: BrowserWindow | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
titleBarStyle: 'hiddenInset', // macOS
frame: process.platform !== 'darwin',
});
// 加载应用
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:3000');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
// 窗口事件
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// 应用生命周期
app.whenReady().then(() => {
createWindow();
createMenu();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// IPC 处理器
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog(mainWindow!, {
properties: ['openFile'],
filters: [
{ name: 'Documents', extensions: ['txt', 'md', 'json'] },
],
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('dialog:saveFile', async (_, content: string) => {
const result = await dialog.showSaveDialog(mainWindow!, {
filters: [{ name: 'JSON', extensions: ['json'] }],
});
if (!result.canceled && result.filePath) {
await fs.writeFile(result.filePath, content);
return result.filePath;
}
return null;
});
ipcMain.handle('app:getVersion', () => app.getVersion());
// 自动更新器
import { autoUpdater } from 'electron-updater';
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on('update-available', () => {
mainWindow?.webContents.send('update-available');
});
autoUpdater.on('update-downloaded', () => {
mainWindow?.webContents.send('update-downloaded');
});
ipcMain.handle('app:installUpdate', () => {
autoUpdater.quitAndInstall();
});
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
// 向渲染进程暴露安全的 API
contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作
openFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content: string) => ipcRenderer.invoke('dialog:saveFile', content),
readFile: (path: string) => ipcRenderer.invoke('fs:readFile', path),
writeFile: (path: string, content: string) =>
ipcRenderer.invoke('fs:writeFile', path, content),
// 应用信息
getVersion: () => ipcRenderer.invoke('app:getVersion'),
getPlatform: () => process.platform,
// 更新
installUpdate: () => ipcRenderer.invoke('app:installUpdate'),
onUpdateAvailable: (callback: () => void) => {
ipcRenderer.on('update-available', callback);
return () => ipcRenderer.removeListener('update-available', callback);
},
onUpdateDownloaded: (callback: () => void) => {
ipcRenderer.on('update-downloaded', callback);
return () => ipcRenderer.removeListener('update-downloaded', callback);
},
// 窗口控制
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
// 原生通知
showNotification: (title: string, body: string) =>
ipcRenderer.invoke('notification:show', title, body),
});
// 渲染进程的 TypeScript 类型声明
declare global {
interface Window {
electronAPI: {
openFile: () => Promise<string | null>;
saveFile: (content: string) => Promise<string | null>;
readFile: (path: string) => Promise<string>;
writeFile: (path: string, content: string) => Promise<void>;
getVersion: () => Promise<string>;
getPlatform: () => string;
installUpdate: () => Promise<void>;
onUpdateAvailable: (callback: () => void) => () => void;
onUpdateDownloaded: (callback: () => void) => () => void;
minimize: () => void;
maximize: () => void;
close: () => void;
showNotification: (title: string, body: string) => Promise<void>;
};
}
}
// App.tsx
function App() {
const [updateAvailable, setUpdateAvailable] = useState(false);
const [updateReady, setUpdateReady] = useState(false);
useEffect(() => {
const removeAvailable = window.electronAPI.onUpdateAvailable(() => {
setUpdateAvailable(true);
});
const removeDownloaded = window.electronAPI.onUpdateDownloaded(() => {
setUpdateReady(true);
});
return () => {
removeAvailable();
removeDownloaded();
};
}, []);
const handleOpenFile = async () => {
const filePath = await window.electronAPI.openFile();
if (filePath) {
const content = await window.electronAPI.readFile(filePath);
// 处理文件内容
}
};
const handleSaveFile = async () => {
const content = JSON.stringify(data, null, 2);
await window.electronAPI.saveFile(content);
};
return (
<div className="app">
{/* 无边框窗口的自定义标题栏 */}
<TitleBar />
<main>
<button onClick={handleOpenFile}>打开文件</button>
<button onClick={handleSaveFile}>保存文件</button>
{updateReady && (
<button onClick={() => window.electronAPI.installUpdate()}>
安装更新并重启
</button>
)}
</main>
</div>
);
}
// 自定义标题栏组件
function TitleBar() {
const platform = window.electronAPI.getPlatform();
return (
<div className="title-bar" style={{ WebkitAppRegion: 'drag' }}>
<span className="title">我的应用</span>
{platform !== 'darwin' && (
<div className="window-controls" style={{ WebkitAppRegion: 'no-drag' }}>
<button onClick={() => window.electronAPI.minimize()}>−</button>
<button onClick={() => window.electronAPI.maximize()}>□</button>
<button onClick={() => window.electronAPI.close()}>×</button>
</div>
)}
</div>
);
}
// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
use std::fs;
// 可从前端调用的命令
#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path).map_err(|e| e.to_string())
}
#[tauri::command]
fn write_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, &content).map_err(|e| e.to_string())
}
#[tauri::command]
fn get_app_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[tauri::command]
async fn perform_heavy_task(input: String) -> Result<String, String> {
// 在后台运行 CPU 密集型工作
tokio::task::spawn_blocking(move || {
// 此处进行繁重计算
format!("已处理: {}", input)
})
.await
.map_err(|e| e.to_string())
}
fn main() {
let menu = Menu::new()
.add_submenu(Submenu::new(
"File",
Menu::new()
.add_item(CustomMenuItem::new("open", "Open").accelerator("CmdOrCtrl+O"))
.add_item(CustomMenuItem::new("save", "Save").accelerator("CmdOrCtrl+S"))
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit),
))
.add_submenu(Submenu::new(
"Edit",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste),
));
tauri::Builder::default()
.menu(menu)
.on_menu_event(|event| {
match event.menu_item_id() {
"open" => {
event.window().emit("menu-open", {}).unwrap();
}
"save" => {
event.window().emit("menu-save", {}).unwrap();
}
_ => {}
}
})
.invoke_handler(tauri::generate_handler![
read_file,
write_file,
get_app_version,
perform_heavy_task,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// src-tauri/tauri.conf.json
{
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devPath": "http://localhost:3000",
"distDir": "../dist"
},
"package": {
"productName": "My App",
"version": "1.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"dialog": {
"all": true
},
"fs": {
"all": true,
"scope": ["$DOCUMENT/*", "$DOWNLOAD/*"]
},
"shell": {
"open": true
},
"notification": {
"all": true
}
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.example.myapp",
"targets": "all"
},
"windows": [
{
"title": "My App",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false
}
],
"updater": {
"active": true,
"endpoints": ["https://releases.example.com/{{target}}/{{current_version}}"],
"pubkey": "YOUR_PUBLIC_KEY"
}
}
}
// 使用 Tauri API
import { invoke } from '@tauri-apps/api/tauri';
import { open, save } from '@tauri-apps/api/dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/api/fs';
import { sendNotification } from '@tauri-apps/api/notification';
import { listen } from '@tauri-apps/api/event';
// 调用 Rust 命令
async function readFile(path: string): Promise<string> {
return invoke('read_file', { path });
}
async function writeFile(path: string, content: string): Promise<void> {
return invoke('write_file', { path, content });
}
// 使用 Tauri 对话框
async function openFileDialog() {
const selected = await open({
multiple: false,
filters: [{ name: 'Documents', extensions: ['txt', 'md', 'json'] }],
});
if (selected && typeof selected === 'string') {
const content = await readTextFile(selected);
return { path: selected, content };
}
return null;
}
async function saveFileDialog(content: string) {
const filePath = await save({
filters: [{ name: 'JSON', extensions: ['json'] }],
});
if (filePath) {
await writeTextFile(filePath, content);
return filePath;
}
return null;
}
// 监听菜单事件
listen('menu-open', async () => {
const file = await openFileDialog();
if (file) {
// 处理文件
}
});
// 发送通知
async function notify(title: string, body: string) {
await sendNotification({ title, body });
}
// 使用 better-sqlite3 的 SQLite (Electron)
import Database from 'better-sqlite3';
const db = new Database('app.db');
// 初始化模式
db.exec(`
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// CRUD 操作
const insertDoc = db.prepare(
'INSERT INTO documents (id, title, content) VALUES (?, ?, ?)'
);
const getDoc = db.prepare('SELECT * FROM documents WHERE id = ?');
const getAllDocs = db.prepare('SELECT * FROM documents ORDER BY updated_at DESC');
const updateDoc = db.prepare(
'UPDATE documents SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
);
const deleteDoc = db.prepare('DELETE FROM documents WHERE id = ?');
// 使用示例
insertDoc.run(uuid(), 'New Document', '');
const doc = getDoc.get('doc-id');
const docs = getAllDocs.all();
每周安装次数
47
代码仓库
GitHub 星标数
11
首次出现
2026年1月24日
安全审计
安装于
opencode45
gemini-cli41
codex39
github-copilot36
claude-code34
cursor32
Building cross-platform desktop applications using web technologies with Electron and Tauri.
// main.ts
import { app, BrowserWindow, ipcMain, dialog, Menu } from 'electron';
import path from 'path';
let mainWindow: BrowserWindow | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
titleBarStyle: 'hiddenInset', // macOS
frame: process.platform !== 'darwin',
});
// Load the app
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:3000');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
// Window events
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// App lifecycle
app.whenReady().then(() => {
createWindow();
createMenu();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// IPC handlers
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog(mainWindow!, {
properties: ['openFile'],
filters: [
{ name: 'Documents', extensions: ['txt', 'md', 'json'] },
],
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('dialog:saveFile', async (_, content: string) => {
const result = await dialog.showSaveDialog(mainWindow!, {
filters: [{ name: 'JSON', extensions: ['json'] }],
});
if (!result.canceled && result.filePath) {
await fs.writeFile(result.filePath, content);
return result.filePath;
}
return null;
});
ipcMain.handle('app:getVersion', () => app.getVersion());
// Auto-updater
import { autoUpdater } from 'electron-updater';
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on('update-available', () => {
mainWindow?.webContents.send('update-available');
});
autoUpdater.on('update-downloaded', () => {
mainWindow?.webContents.send('update-downloaded');
});
ipcMain.handle('app:installUpdate', () => {
autoUpdater.quitAndInstall();
});
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
// Expose safe APIs to renderer
contextBridge.exposeInMainWorld('electronAPI', {
// File operations
openFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content: string) => ipcRenderer.invoke('dialog:saveFile', content),
readFile: (path: string) => ipcRenderer.invoke('fs:readFile', path),
writeFile: (path: string, content: string) =>
ipcRenderer.invoke('fs:writeFile', path, content),
// App info
getVersion: () => ipcRenderer.invoke('app:getVersion'),
getPlatform: () => process.platform,
// Updates
installUpdate: () => ipcRenderer.invoke('app:installUpdate'),
onUpdateAvailable: (callback: () => void) => {
ipcRenderer.on('update-available', callback);
return () => ipcRenderer.removeListener('update-available', callback);
},
onUpdateDownloaded: (callback: () => void) => {
ipcRenderer.on('update-downloaded', callback);
return () => ipcRenderer.removeListener('update-downloaded', callback);
},
// Window controls
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
// Native notifications
showNotification: (title: string, body: string) =>
ipcRenderer.invoke('notification:show', title, body),
});
// TypeScript types for renderer
declare global {
interface Window {
electronAPI: {
openFile: () => Promise<string | null>;
saveFile: (content: string) => Promise<string | null>;
readFile: (path: string) => Promise<string>;
writeFile: (path: string, content: string) => Promise<void>;
getVersion: () => Promise<string>;
getPlatform: () => string;
installUpdate: () => Promise<void>;
onUpdateAvailable: (callback: () => void) => () => void;
onUpdateDownloaded: (callback: () => void) => () => void;
minimize: () => void;
maximize: () => void;
close: () => void;
showNotification: (title: string, body: string) => Promise<void>;
};
}
}
// App.tsx
function App() {
const [updateAvailable, setUpdateAvailable] = useState(false);
const [updateReady, setUpdateReady] = useState(false);
useEffect(() => {
const removeAvailable = window.electronAPI.onUpdateAvailable(() => {
setUpdateAvailable(true);
});
const removeDownloaded = window.electronAPI.onUpdateDownloaded(() => {
setUpdateReady(true);
});
return () => {
removeAvailable();
removeDownloaded();
};
}, []);
const handleOpenFile = async () => {
const filePath = await window.electronAPI.openFile();
if (filePath) {
const content = await window.electronAPI.readFile(filePath);
// Handle file content
}
};
const handleSaveFile = async () => {
const content = JSON.stringify(data, null, 2);
await window.electronAPI.saveFile(content);
};
return (
<div className="app">
{/* Custom title bar for frameless window */}
<TitleBar />
<main>
<button onClick={handleOpenFile}>Open File</button>
<button onClick={handleSaveFile}>Save File</button>
{updateReady && (
<button onClick={() => window.electronAPI.installUpdate()}>
Install Update & Restart
</button>
)}
</main>
</div>
);
}
// Custom title bar component
function TitleBar() {
const platform = window.electronAPI.getPlatform();
return (
<div className="title-bar" style={{ WebkitAppRegion: 'drag' }}>
<span className="title">My App</span>
{platform !== 'darwin' && (
<div className="window-controls" style={{ WebkitAppRegion: 'no-drag' }}>
<button onClick={() => window.electronAPI.minimize()}>−</button>
<button onClick={() => window.electronAPI.maximize()}>□</button>
<button onClick={() => window.electronAPI.close()}>×</button>
</div>
)}
</div>
);
}
// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
use std::fs;
// Commands callable from frontend
#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path).map_err(|e| e.to_string())
}
#[tauri::command]
fn write_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, &content).map_err(|e| e.to_string())
}
#[tauri::command]
fn get_app_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[tauri::command]
async fn perform_heavy_task(input: String) -> Result<String, String> {
// Run CPU-intensive work in background
tokio::task::spawn_blocking(move || {
// Heavy computation here
format!("Processed: {}", input)
})
.await
.map_err(|e| e.to_string())
}
fn main() {
let menu = Menu::new()
.add_submenu(Submenu::new(
"File",
Menu::new()
.add_item(CustomMenuItem::new("open", "Open").accelerator("CmdOrCtrl+O"))
.add_item(CustomMenuItem::new("save", "Save").accelerator("CmdOrCtrl+S"))
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit),
))
.add_submenu(Submenu::new(
"Edit",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste),
));
tauri::Builder::default()
.menu(menu)
.on_menu_event(|event| {
match event.menu_item_id() {
"open" => {
event.window().emit("menu-open", {}).unwrap();
}
"save" => {
event.window().emit("menu-save", {}).unwrap();
}
_ => {}
}
})
.invoke_handler(tauri::generate_handler![
read_file,
write_file,
get_app_version,
perform_heavy_task,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// src-tauri/tauri.conf.json
{
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devPath": "http://localhost:3000",
"distDir": "../dist"
},
"package": {
"productName": "My App",
"version": "1.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"dialog": {
"all": true
},
"fs": {
"all": true,
"scope": ["$DOCUMENT/*", "$DOWNLOAD/*"]
},
"shell": {
"open": true
},
"notification": {
"all": true
}
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.example.myapp",
"targets": "all"
},
"windows": [
{
"title": "My App",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false
}
],
"updater": {
"active": true,
"endpoints": ["https://releases.example.com/{{target}}/{{current_version}}"],
"pubkey": "YOUR_PUBLIC_KEY"
}
}
}
// Using Tauri APIs
import { invoke } from '@tauri-apps/api/tauri';
import { open, save } from '@tauri-apps/api/dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/api/fs';
import { sendNotification } from '@tauri-apps/api/notification';
import { listen } from '@tauri-apps/api/event';
// Call Rust commands
async function readFile(path: string): Promise<string> {
return invoke('read_file', { path });
}
async function writeFile(path: string, content: string): Promise<void> {
return invoke('write_file', { path, content });
}
// Use Tauri dialog
async function openFileDialog() {
const selected = await open({
multiple: false,
filters: [{ name: 'Documents', extensions: ['txt', 'md', 'json'] }],
});
if (selected && typeof selected === 'string') {
const content = await readTextFile(selected);
return { path: selected, content };
}
return null;
}
async function saveFileDialog(content: string) {
const filePath = await save({
filters: [{ name: 'JSON', extensions: ['json'] }],
});
if (filePath) {
await writeTextFile(filePath, content);
return filePath;
}
return null;
}
// Listen for menu events
listen('menu-open', async () => {
const file = await openFileDialog();
if (file) {
// Handle file
}
});
// Send notification
async function notify(title: string, body: string) {
await sendNotification({ title, body });
}
// SQLite with better-sqlite3 (Electron)
import Database from 'better-sqlite3';
const db = new Database('app.db');
// Initialize schema
db.exec(`
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// CRUD operations
const insertDoc = db.prepare(
'INSERT INTO documents (id, title, content) VALUES (?, ?, ?)'
);
const getDoc = db.prepare('SELECT * FROM documents WHERE id = ?');
const getAllDocs = db.prepare('SELECT * FROM documents ORDER BY updated_at DESC');
const updateDoc = db.prepare(
'UPDATE documents SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
);
const deleteDoc = db.prepare('DELETE FROM documents WHERE id = ?');
// Usage
insertDoc.run(uuid(), 'New Document', '');
const doc = getDoc.get('doc-id');
const docs = getAllDocs.all();
Weekly Installs
47
Repository
GitHub Stars
11
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykPass
Installed on
opencode45
gemini-cli41
codex39
github-copilot36
claude-code34
cursor32
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
127,000 周安装