在前端技术迅猛发展的今天,JavaScript 不再只是浏览器中的脚本语言,它正在一步步打破平台的边界,深入各类系统开发。而 Electron,作为一个将 Web 技术与桌面开发完美融合的框架,正成为众多开发者打造跨平台桌面应用的不二选择。
本篇《Electron:看这一篇就够了》,旨在为你梳理 Electron 的核心理念、技术结构与实战要点。从架构原理到主渲染进程,从 IPC 通信到自动打包发布,力求一文读懂、直达实用,助你从 Web 跨入桌面,快速上手 Electron 开发。
无论你是前端工程师、全栈开发者,还是桌面开发的新探索者,相信这一篇文章,都能成为你走进 Electron 世界的最佳起点。
如果你还在寻找学习 Electron 的理由,AI 编辑器 Cursor 就是一个足够有说服力的答案 —— 它就是用 Electron 构建的。
在学习electron之前,需要具备JS的相关知识,可先阅读以下文档:
https://blog.csdn.net/weixin_43431593/article/details/156149817
一、快速开始
1、开发环境Cursor
cursor:看这一篇就够了_cursor pycharm-CSDN博客
2、搭建开发环境与示例应用开发
3、打包分发应用
4、Electron与Cursor
5、结合bootstrap5 (可选UI组件)
Bootstrap5:看这一篇就够了-CSDN博客
6、main.js解析
const { app, BrowserWindow } = require('electron')const path = require('path')function createWindow () { const win = new BrowserWindow({ width: 900, height: 700, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) // 加载 index.html win.loadFile('index.html') // 开发时打开开发者工具 // win.webContents.openDevTools()}app.whenReady().then(() => { createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } })})app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() }})
流程大概是这样:
其中,electron模块:主进程核心模块app:控制应用程序生命周期 BrowserWindow:创建和控制浏览器窗口path模块:Node.js 内置模块,用于处理文件路径。
7、index.html解析
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Electron 应用</title> <link href="node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> <style> body { background-color: #f8f9fa; } .version-card { transition: transform 0.2s; } .version-card:hover { transform: translateY(-5px); } </style></head><body> <nav > <div > <a href="#">Electron Study</a> </div> </nav> <div > <div > <div > <div > <div > <h1 >欢迎使用 Electron!</h1> <div > <div > <div > <div > <h5 >Node.js</h5> <p id="node-version">加载中...</p> </div> </div> </div> <div > <div > <div > <h5 >Chrome</h5> <p id="chrome-version">加载中...</p> </div> </div> </div> <div > <div > <div > <h5 >Electron</h5> <p id="electron-version">加载中...</p> </div> </div> </div> </div> <div > <button type="button" data-bs-toggle="collapse" data-bs-target="#systemInfo"> 显示系统信息 </button> </div> <div id="systemInfo"> <div > <h5 >系统信息</h5> <p >这是一个基于 Electron 和 Bootstrap 5 构建的现代化桌面应用。</p> </div> </div> </div> </div> </div> </div> </div> <script src="node_modules/@popperjs/core/dist/umd/popper.min.js"></script> <script src="node_modules/bootstrap/dist/js/bootstrap.min.js"></script> <script> window.addEventListener('DOMContentLoaded', () => { const replaceText = (selector, text) => { const element = document.getElementById(selector) if (element) element.innerText = text } for (const type of ['chrome', 'node', 'electron']) { replaceText(`${type}-version`, process.versions[type]) } }) </script></body></html>
如图,有一些功能在html没有显示的,因为它们是应用自带的 ,当然可以自定义。我们在后续的学习中,会接触到。
8、配置debug环境
9、 常见问题解决
(1)替换图标的方法
需要在网站生成ico图标
Online ICO converter
需要在 package.json中配置构建引用的图标
运行时使用的图标
(2)处理控制台输出中文乱码问题
"start": "set NODE_OPTIONS=--no-warnings --trace-warnings && chcp 65001 && electron .",
二、常用模块
1、Electron 模块
1. app - 应用程序生命周期const { app } = require('electron');// 核心功能app.on('ready', () => { // 应用初始化完成 console.log('Electron 应用已就绪');});app.on('window-all-closed', () => { // 所有窗口关闭 if (process.platform !== 'darwin') { app.quit(); // 非 macOS 直接退出 }});app.on('activate', () => { // macOS 点击 Dock 图标 if (BrowserWindow.getAllWindows().length === 0) { createWindow(); // 重新创建窗口 }});// 路径管理const userDataPath = app.getPath('userData'); // 用户数据目录const tempPath = app.getPath('temp'); // 临时目录const desktopPath = app.getPath('desktop'); // 桌面目录// 应用信息app.setName('MyApp'); // 设置应用名称const appVersion = app.getVersion(); // 获取应用版本const appName = app.getName(); // 获取应用名称// 单实例锁const gotTheLock = app.requestSingleInstanceLock();if (!gotTheLock) { app.quit(); // 已有一个实例在运行}底层原理:app 模块是 Electron 主进程的入口点,它:• 与操作系统原生应用程序 API 交互• 管理应用生命周期(启动、退出、激活)• 维护路径缓存映射表• 处理命令行参数解析• 实现单实例锁机制(通过文件锁或命名管道)2. BrowserWindow - 创建和管理窗口const { BrowserWindow } = require('electron');// 创建窗口const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, // 启用 Node.js contextIsolation: false, // 禁用上下文隔离 preload: path.join(__dirname, 'preload.js') // 预加载脚本 }});// 窗口事件mainWindow.on('closed', () => { // 窗口关闭 mainWindow = null;});mainWindow.on('focus', () => { // 窗口获得焦点 console.log('窗口获得焦点');});// 窗口控制mainWindow.loadURL('https://electronjs.org'); // 加载 URLmainWindow.loadFile('index.html'); // 加载本地文件mainWindow.webContents.openDevTools(); // 打开开发者工具mainWindow.maximize(); // 最大化mainWindow.minimize(); // 最小化mainWindow.restore(); // 恢复mainWindow.close(); // 关闭// 窗口信息const bounds = mainWindow.getBounds(); // 获取窗口位置和大小const isMaximized = mainWindow.isMaximized(); // 是否最大化底层架构:├── NativeWindow (C++层)│ ├── Windows: HWND│ ├── macOS: NSWindow│ └── Linux: GtkWindow├── WebContents (网页内容)│ ├── Chromium渲染进程│ ├── 页面加载器│ └── DevTools└── 窗口装饰 ├── 标题栏 ├── 边框 └── 控制按钮3. Menu - 应用程序菜单const { Menu, MenuItem } = require('electron');// 应用菜单模板const template = [ { label: '文件', submenu: [ { label: '打开', accelerator: 'CmdOrCtrl+O', // 快捷键 click: () => { console.log('打开文件'); } }, { type: 'separator' }, // 分隔线 { label: '退出', role: 'quit' } // 内置角色 ] }, { label: '编辑', submenu: [ { label: '撤销', role: 'undo' }, { label: '重做', role: 'redo' }, { type: 'separator' }, { label: '剪切', role: 'cut' }, { label: '复制', role: 'copy' }, { label: '粘贴', role: 'paste' } ] }];// 创建菜单const menu = Menu.buildFromTemplate(template);Menu.setApplicationMenu(menu); // 设置应用菜单// 上下文菜单const contextMenu = Menu.buildFromTemplate([ { label: '复制', click: () => {} }, { label: '粘贴', click: () => {} }]);// 显示上下文菜单window.addEventListener('contextmenu', (e) => { e.preventDefault(); contextMenu.popup();});操作系统适配:// 平台特定的菜单if (process.platform === 'darwin') { // macOS template.unshift({ // 添加 macOS 的应用菜单 label: app.getName(), submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideothers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' } ] });}4. dialog - 原生对话框const { dialog } = require('electron');// 1. 消息对话框dialog.showMessageBox(mainWindow, { type: 'info', // none/info/error/question/warning title: '提示', message: '操作成功', detail: '文件已保存', buttons: ['确定', '取消'], defaultId: 0, // 默认按钮索引 cancelId: 1 // 取消按钮索引}).then(result => { console.log('点击了按钮:', result.response);});// 2. 文件对话框dialog.showOpenDialog({ title: '选择文件', defaultPath: app.getPath('documents'), buttonLabel: '选择', filters: [ { name: '图片', extensions: ['jpg', 'png', 'gif'] }, { name: '所有文件', extensions: ['*'] } ], properties: ['openFile', 'multiSelections'] // 多选}).then(result => { if (!result.canceled) { console.log('选择的文件:', result.filePaths); }});// 3. 保存对话框dialog.showSaveDialog({ title: '保存文件', defaultPath: path.join(app.getPath('documents'), '未命名.txt'), filters: [{ name: '文本文件', extensions: ['txt'] }]}).then(result => { if (!result.canceled) { fs.writeFileSync(result.filePath, '文件内容'); }});系统原生调用链:Electron dialog API ↓ IPC主进程 dialog 模块 ↓调用操作系统原生 API: - Windows: IFileDialog (COM 组件) - macOS: NSSavePanel/NSPanel - Linux: GtkFileChooser/GtkMessageDialog5. ipcMain - 主进程通信const { ipcMain } = require('electron');// 1. 监听渲染进程消息ipcMain.on('message-from-renderer', (event, data) => { console.log('收到消息:', data); // 回复消息 event.reply('reply-to-renderer', { result: 'ok' }); // 或 event.sender.send('reply-to-renderer', { result: 'ok' });});// 2. 同步通信ipcMain.on('sync-message', (event, data) => { const result = processData(data); event.returnValue = result; // 同步返回});// 3. 一次性监听ipcMain.once('init-complete', (event) => { console.log('初始化完成');});// 4. 处理异步操作ipcMain.handle('read-file', async (event, filePath) => { try { const data = await fs.promises.readFile(filePath, 'utf8'); return { success: true, data }; } catch (error) { return { success: false, error: error.message }; }});底层 IPC 架构:主进程 (Node.js) ↔ 渲染进程 (Chromium) ↓ ↓ipcMain ipcRenderer ↓ ↓IPC 通道 (MessageChannel) ↓进程间通信 (IPC) ├── Windows: 命名管道/LPC ├── macOS: Mach Ports └── Linux: Unix Domain Sockets6. shell - 系统外壳集成const { shell } = require('electron');// 1. 打开外部应用/URLshell.openExternal('https://electronjs.org'); // 浏览器打开shell.openPath('/Users/name/Documents'); // 文件管理器打开// 2. 在文件管理器中显示shell.showItemInFolder('/path/to/file.txt');// 3. 回收站操作shell.trashItem('/path/to/file.txt').then(() => { console.log('文件已移动到回收站');});// 4. 剪切板操作const currentClipboard = shell.readText(); // 读取文本shell.writeText('要复制的文本'); // 写入文本// 5. 桌面集成shell.beep(); // 系统提示音系统调用映射:// Windowsshell.openExternal('...') // ShellExecuteWshell.showItemInFolder('...') // SHOpenFolderAndSelectItems// macOSshell.openExternal('...') // [[NSWorkspace sharedWorkspace] openURL:]shell.showItemInFolder('...') // [[NSWorkspace sharedWorkspace] selectFile:...]// Linuxshell.openExternal('...') // gtk_show_uri / xdg-open
2、Node.js 核心模块
1. fs - 文件系统const fs = require('fs');const fsPromises = fs.promises; // Promise API// 1. 同步操作(阻塞)try { const data = fs.readFileSync('file.txt', 'utf8'); fs.writeFileSync('output.txt', data); fs.appendFileSync('log.txt', 'n新的内容');} catch (error) { console.error('文件操作失败:', error);}// 2. 异步操作(回调)fs.readFile('file.txt', 'utf8', (err, data) => { if (err) throw err; console.log(data);});// 3. Promise API(推荐)async function processFile() { try { const stats = await fsPromises.stat('file.txt'); console.log('文件大小:', stats.size); console.log('修改时间:', stats.mtime); if (stats.isFile()) { const data = await fsPromises.readFile('file.txt', 'utf8'); console.log('文件内容:', data); } } catch (error) { console.error('错误:', error); }}// 4. 流式操作(大文件)const readStream = fs.createReadStream('largefile.txt', { encoding: 'utf8', highWaterMark: 64 * 1024 // 64KB 缓冲区});const writeStream = fs.createWriteStream('copy.txt');readStream.on('data', (chunk) => { console.log('读取到', chunk.length, '字节'); writeStream.write(chunk);});readStream.on('end', () => { writeStream.end(); console.log('文件复制完成');});// 5. 文件监视const watcher = fs.watch('file.txt', (eventType, filename) => { console.log(`${filename} 发生了 ${eventType} 事件`);});// 6. 目录操作fsPromises.mkdir('new-folder', { recursive: true }); // 创建目录const files = fsPromises.readdir('.'); // 读取目录fsPromises.rmdir('empty-folder'); // 删除目录文件系统底层:Node.js fs 模块 ↓libuv 文件系统操作 ↓操作系统文件系统 API: - Windows: Win32 API (CreateFile, ReadFile, WriteFile) - Unix: POSIX API (open, read, write, stat) ↓文件系统驱动程序 ↓磁盘 I/O2. path - 路径处理const path = require('path');// 1. 路径解析const fullPath = path.resolve('src', 'app.js'); // 解析为绝对路径const relativePath = path.relative('/data/orandea', '/data/orandea/test/aaa');// 2. 路径拼接const joinedPath = path.join(__dirname, 'assets', 'image.png');// 3. 路径信息const filePath = '/home/user/project/src/app.js';console.log(path.dirname(filePath)); // /home/user/project/srcconsole.log(path.basename(filePath)); // app.jsconsole.log(path.extname(filePath)); // .jsconsole.log(path.basename(filePath, '.js')); // app// 4. 路径解析对象const parsed = path.parse(filePath);// {// root: '/',// dir: '/home/user/project/src',// base: 'app.js',// ext: '.js',// name: 'app'// }// 5. 平台差异处理console.log(path.sep); // Windows: "", Unix: "/"console.log(path.delimiter); // Windows: ";", Unix: ":"// 6. 路径规范化console.log(path.normalize('/foo/bar//baz/asdf/quux/..')); // /foo/bar/baz/asdf路径解析算法:// path.resolve 的实现思路function resolve(...paths) { let resolvedPath = ''; let resolvedAbsolute = false; for (let i = paths.length - 1; i >= 0; i--) { const path = paths[i]; // 跳过空路径 if (path.length === 0) continue; resolvedPath = path + '/' + resolvedPath; resolvedAbsolute = path.charAt(0) === '/'; if (resolvedAbsolute) { break; } } // 规范化路径 resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute); if (resolvedAbsolute) { return '/' + resolvedPath; } return resolvedPath.length > 0 ? resolvedPath : '.';}3. os - 操作系统信息const os = require('os');// 1. 系统信息console.log('操作系统平台:', os.platform()); // win32, darwin, linuxconsole.log('系统架构:', os.arch()); // x64, arm, ia32console.log('系统版本:', os.version()); // Windows 10, Darwin 20.6.0console.log('主机名:', os.hostname()); // 计算机名称console.log('临时目录:', os.tmpdir()); // 临时文件目录// 2. 内存信息console.log('总内存:', formatBytes(os.totalmem()));console.log('可用内存:', formatBytes(os.freemem()));console.log('内存使用率:', (1 - os.freemem() / os.totalmem()) * 100 + '%');function formatBytes(bytes) { const units = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; } return bytes.toFixed(2) + ' ' + units[i];}// 3. CPU 信息const cpus = os.cpus();console.log('CPU 核心数:', cpus.length);cpus.forEach((cpu, i) => { console.log(`CPU ${i}: ${cpu.model} - 速度: ${cpu.speed}MHz`);});// 4. 网络信息const networkInterfaces = os.networkInterfaces();for (const [name, interfaces] of Object.entries(networkInterfaces)) { interfaces.forEach(iface => { if (iface.family === 'IPv4' && !iface.internal) { console.log(`接口 ${name}: ${iface.address}`); } });}// 5. 用户信息console.log('当前用户:', os.userInfo().username);console.log('家目录:', os.homedir());底层系统调用:// 不同平台的实现switch (process.platform) { case 'win32': // 使用 Windows API: GetSystemInfo, GlobalMemoryStatusEx break; case 'darwin': // 使用 sysctl, sysctlbyname break; case 'linux': // 读取 /proc 文件系统 // /proc/meminfo, /proc/cpuinfo, /proc/self/stat break;}
3、模块协同工作示例
const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron');const fs = require('fs').promises;const path = require('path');const os = require('os');class FileManager { constructor() { this.currentDir = os.homedir(); } // 打开文件选择对话框 async openFileDialog() { const { canceled, filePaths } = await dialog.showOpenDialog({ title: '选择文件', defaultPath: this.currentDir, properties: ['openFile', 'multiSelections'] }); if (!canceled && filePaths.length > 0) { this.currentDir = path.dirname(filePaths[0]); return await this.readFiles(filePaths); } return []; } // 读取多个文件 async readFiles(filePaths) { const files = []; for (const filePath of filePaths) { try { const stats = await fs.stat(filePath); if (stats.isFile()) { const content = await fs.readFile(filePath, 'utf8'); files.push({ name: path.basename(filePath), path: filePath, size: stats.size, modified: stats.mtime, content: content.substring(0, 1000) // 前1000字符 }); } } catch (error) { console.error(`读取文件失败 ${filePath}:`, error); } } return files; } // 保存文件 async saveFile(content, defaultPath) { const { canceled, filePath } = await dialog.showSaveDialog({ title: '保存文件', defaultPath: defaultPath || path.join(this.currentDir, '未命名.txt'), filters: [ { name: '文本文件', extensions: ['txt', 'md'] }, { name: '所有文件', extensions: ['*'] } ] }); if (!canceled && filePath) { await fs.writeFile(filePath, content, 'utf8'); // 在文件管理器中显示 if (process.platform !== 'darwin') { shell.showItemInFolder(filePath); } return filePath; } return null; }}// 应用初始化app.whenReady().then(() => { const fileManager = new FileManager(); const mainWindow = new BrowserWindow({ /* 配置 */ }); // 注册 IPC 处理器 ipcMain.handle('open-files', () => fileManager.openFileDialog()); ipcMain.handle('save-file', (event, content, defaultPath) => fileManager.saveFile(content, defaultPath) ); // 监听来自渲染进程的请求 ipcMain.on('show-in-folder', (event, filePath) => { if (fs.existsSync(filePath)) { shell.showItemInFolder(filePath); } }); // 窗口关闭处理 mainWindow.on('close', async (event) => { const choice = await dialog.showMessageBox(mainWindow, { type: 'question', buttons: ['保存并退出', '不保存退出', '取消'], defaultId: 0, cancelId: 2, message: '是否保存当前内容?' }); switch (choice.response) { case 0: // 保存并退出 event.preventDefault(); await saveCurrentContent(); mainWindow.destroy(); break; case 1: // 不保存退出 // 直接退出 break; case 2: // 取消 event.preventDefault(); break; } });});
4、性能优化与最佳实践
1. 模块懒加载// 需要时再加载模块let fs, path, os;async function processFile(filePath) { if (!fs) fs = await import('fs/promises'); if (!path) path = await import('path'); const ext = path.extname(filePath); return fs.readFile(filePath, 'utf8');}2. 错误处理// 统一的错误处理class AppError extends Error { constructor(message, code, module) { super(message); this.code = code; this.module = module; this.timestamp = new Date().toISOString(); }}function safeRequire(moduleName) { try { return require(moduleName); } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { throw new AppError(`模块 ${moduleName} 未找到`, 'MODULE_MISSING', 'core'); } throw error; }}3. 资源管理// 清理资源class ResourceManager { constructor() { this.resources = new Set(); } register(resource) { this.resources.add(resource); return resource; } cleanup() { for (const resource of this.resources) { if (resource.close) resource.close(); if (resource.destroy) resource.destroy(); if (resource.removeAllListeners) resource.removeAllListeners(); } this.resources.clear(); }}// 使用const resources = new ResourceManager();app.on('before-quit', () => resources.cleanup());
179