欢迎光临
我们一直在努力

前端人别卷网页了!7天用Electron搞定桌面应用,工资翻倍不是梦

前端人别卷网页了!7天用Electron搞定桌面应用,工资翻倍不是梦

  • 前端人别卷网页了!7天用Electron搞定桌面应用,工资翻倍不是梦
    • 瞎扯淡的开场白:为什么你的简历还缺个"桌面端"项目
    • 扒一扒Electron这货的底裤
      • 套壳浏览器?这么说对也不对
      • 除了Electron,这几个备胎也得认识
      • 为啥大厂都爱用Electron?
    • 手把手教你把网页"塞"进桌面图标里
      • 脚手架一把梭,别傻傻敲命令
      • main.js里的那些破事儿
      • preload脚本:别把它当摆设
    • 这框架虽好,但这几个坑踩进去就拔不出来
      • 打包体积:塞了整个Chrome进去?
      • 内存占用:低配电脑杀手
      • 自动更新:配置起来想砸键盘
      • 安全性:别让应用变成筛子
    • 实战环节:做个能摸鱼的本地记事本
    • 程序崩了别只会重启,这几招能让你少掉几根头发
      • 主进程挂了怎么查?
      • 渲染进程白屏了?
      • 内存泄漏排查
      • 版本对齐很重要
    • 几个让代码看起来像"老手"写的骚操作
      • 别把逻辑都堆在渲染进程
      • 上下文隔离一定要开
      • 多窗口架构
      • 静态资源压缩
    • 最后唠两句:这玩意儿也不是万能药

前端人别卷网页了!7天用Electron搞定桌面应用,工资翻倍不是梦

瞎扯淡的开场白:为什么你的简历还缺个"桌面端"项目

说实话,现在前端这行卷到什么程度了?打开BOSS直聘,十个岗位九个要求"全栈",剩下那个写着"精通Vue/React,有小程序经验优先"——点进去一聊,人家还问你"会不会搞点桌面端的东西"。

我就纳了闷了,当年咱们入坑前端的时候,不就是说好了"一次编写,到处运行"吗?结果现在倒好,H5卷不动了卷小程序,小程序卷不动了又开始卷桌面端。前两天我一哥们儿面试,面试官原话是:"你们前端现在不都会Electron吗?我们后台管理系统想做个客户端版本。"哥们儿当场懵逼,回来就问我:“这玩意儿不是搞VS Code那帮大佬玩的吗?咱这水平能碰?”

能碰,太能碰了。而且我跟你说,这玩意儿上手比你想象得简单十倍,但简历上写出来逼格能高十倍。你想啊,别人简历上都是"开发了XX管理系统",你写的是"独立开发了跨平台桌面客户端应用,支持Windows/macOS双平台"——这差距,就像你穿拖鞋去面试和穿皮鞋去面试的区别,懂我意思吧?

更关键的是,现在老板们也不知道从哪听来的,总觉得"客户端"比"网页"高级。上周我们产品经理突然在群里@我:"能不能把咱们的后台搞成软件那种,双击图标就能打开,别老是用浏览器。"我当时就想回他:"您知道浏览器就是个软件吗?"但转念一想,算了,跟钱过不去干嘛。花三天时间用Electron套了个壳,老板看完演示直接说:“这个好,专业!”

所以啊,别觉得桌面端离你很远。你现在的HTML、CSS、JavaScript功底完全够用,甚至那些让你头秃的浏览器兼容性问题在这里都不存在了——因为Electron里面就是个Chrome,版本还是你定的。不用现学C#,不用啃C++,用你最熟悉的那套技术栈,就能打包出来一个.exe文件,双击就能跑。这羊毛,不薅白不薅。

不过话说回来,虽然我说得轻松,但"跨平台"这三个字确实容易把人忽悠瘸了。很多人一听"跨平台"就以为是银弹,结果进去才发现,Windows上的路径分隔符和macOS不一样,系统通知的API调用方式也有差异,甚至有些功能在某个平台上干脆就不支持。所以咱们得先扒开Electron的底裤,看看这货到底是个什么东西,别稀里糊涂就往里跳。

扒一扒Electron这货的底裤

套壳浏览器?这么说对也不对

你要是去GitHub上看Electron的源码,或者跟后端同事解释你在做什么,最简单粗暴的说法就是:"就是个套了壳的Chrome浏览器,里面塞了个Node.js。"这话虽然糙,但理儿是这么个理儿。

Electron的核心架构其实就两部分:Chromium负责渲染界面,Node.js负责操作系统级别的操作。Chromium这部分好理解,就是你写的那堆HTML、CSS、JavaScript跑的地方,跟你写网页没什么两样。但Node.js在里面干嘛呢?它让你能在"网页"里直接调用fs模块读写文件,能用child_process启动系统命令,能访问数据库——这些在普通浏览器里想都不敢想的事情,在Electron里就是家常便饭。

但这里有个特别有意思的设计,也是新手最容易懵圈的地方:Electron把这两个东西分成了"主进程"(Main Process)和"渲染进程"(Renderer Process)。主进程就是那个跑Node.js的,它负责创建窗口、管理应用生命周期、搞系统级别的操作。渲染进程就是每个窗口里的Chromium实例,负责展示界面、响应用户交互。

这俩进程怎么通信呢?官方给了一套IPC(进程间通信)机制。简单来说,渲染进程想读文件?行,但它不能直接读,得发消息给主进程:"哥,帮我读一下C盘这个文件。"主进程读完了再发回去:"给,内容在这儿。"听起来挺绕的,但这设计其实是为了安全——要是渲染进程能随便访问系统API,那用户打开个第三方页面不就完蛋了?

// 主进程 main.js – 这就是整个应用的入口,Node.js环境
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');

// 保持窗口对象的全局引用,不然会被垃圾回收搞没
let mainWindow;

function createWindow() {
// 创建一个浏览器窗口,这玩意儿就是渲染进程的容器
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
// 这个很关键,把预加载脚本挂上去,打通主进程和渲染进程的任督二脉
preload: path.join(__dirname, 'preload.js'),
// 一定要开上下文隔离,后面讲安全的时候再说为啥
contextIsolation: true,
// 别允许远程模块,这玩意儿坑过太多人,官方都弃用了
enableRemoteModule: false,
// 渲染进程里直接用Node.js?想啥呢,关了关了
nodeIntegration: false
}
});

// 加载本地HTML文件,也可以加载远程URL,但生产环境别这么干
mainWindow.loadFile('index.html');

// 打开开发者工具,调试神器,打包的时候记得注释掉
// mainWindow.webContents.openDevTools();

// 窗口关闭时的清理工作
mainWindow.on('closed', () => {
mainWindow = null;
});
}

// Electron初始化完成,准备创建窗口
app.whenReady().then(createWindow);

// 所有窗口都关了,应用退出(Windows/Linux上)
app.on('window-all-closed', () => {
// macOS上习惯不一样,点了红叉应用还在后台跑
if (process.platform !== 'darwin') {
app.quit();
}
});

// macOS特有:点击dock图标重新创建窗口
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

// 处理来自渲染进程的读文件请求
ipcMain.handle('read-file', async (event, filePath) => {
try {
// 这里可以做路径校验,别让用户读到不该读的东西
const content = await fs.promises.readFile(filePath, 'utf-8');
return { success: true, content };
} catch (error) {
return { success: false, error: error.message };
}
});

// 处理写文件请求,带个简单的备份逻辑,显得专业
ipcMain.handle('write-file', async (event, filePath, content) => {
try {
// 先备份原文件,万一写崩了还能恢复
if (fs.existsSync(filePath)) {
const backupPath = `${filePath}.backup.${Date.now()}`;
await fs.promises.copyFile(filePath, backupPath);
}

await fs.promises.writeFile(filePath, content, 'utf-8');
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});

// preload.js – 预加载脚本,这是渲染进程和主进程之间的"翻译官"
const { contextBridge, ipcRenderer } = require('electron');

// 用contextBridge把API暴露给渲染进程,这样比较安全
contextBridge.exposeInMainWorld('electronAPI', {
// 暴露读文件的方法,渲染进程调用这个就能间接用到Node.js的fs模块
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),

// 暴露写文件的方法
writeFile: (filePath, content) => ipcRenderer.invoke('write-file', filePath, content),

// 还可以暴露一些平台信息,让渲染进程知道自己在啥系统上跑
platform: process.platform,

// 版本信息有时候也挺有用
versions: {
node: process.versions.node,
electron: process.versions.electron,
chrome: process.versions.chrome
}
});

<!– index.html – 这就是普通的HTML,跟写网页一模一样 –>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的第一个Electron应用</title>
<style>
/* 这里随便写CSS,反正里面是个Chrome,ES6+随便用 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
textarea {
width: 100%;
height: 300px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: 'Consolas', monospace;
resize: vertical;
}
button {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
margin-top: 10px;
}
button:hover {
background: #0052a3;
}
.status {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
display: none;
}
.status.success {
background: #d4edda;
color: #155724;
display: block;
}
.status.error {
background: #f8d7da;
color: #721c24;
display: block;
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 Electron 文件编辑器</h1>
<p>当前平台:<span id="platform"></span> | Electron版本:<span id="version"></span></p>

<textarea id="editor" placeholder="在这里输入内容…"></textarea>

<div>
<button onclick="openFile()">📂 打开文件</button>
<button onclick="saveFile()">💾 保存文件</button>
</div>

<div id="status" class="status"></div>
</div>

<script>
// 页面加载时显示系统信息,这些数据是从preload.js里暴露出来的
document.getElementById('platform').textContent = window.electronAPI.platform;
document.getElementById('version').textContent = window.electronAPI.versions.electron;

const editor = document.getElementById('editor');
const statusDiv = document.getElementById('status');
let currentFilePath = null;

// 显示状态消息,成功失败不同颜色
function showStatus(message, isError = false) {
statusDiv.textContent = message;
statusDiv.className = 'status ' + (isError ? 'error' : 'success');
setTimeout(() => {
statusDiv.className = 'status';
}, 3000);
}

// 打开文件,这里调用的其实是主进程的fs.readFile
async function openFile() {
// 实际项目中应该用对话框让用户选文件,这里简化一下直接写死路径演示
const path = 'test.txt';
const result = await window.electronAPI.readFile(path);

if (result.success) {
editor.value = result.content;
currentFilePath = path;
showStatus('文件加载成功!');
} else {
showStatus('打开失败:' + result.error, true);
}
}

// 保存文件,同样是通过IPC交给主进程处理
async function saveFile() {
const path = currentFilePath || 'test.txt';
const content = editor.value;

const result = await window.electronAPI.writeFile(path, content);

if (result.success) {
currentFilePath = path;
showStatus('保存成功!');
} else {
showStatus('保存失败:' + result.error, true);
}
}
</script>
</body>
</html>

除了Electron,这几个备胎也得认识

虽然我现在在教Electron,但咱得客观。这玩意儿不是唯一的选项,甚至有些场景下它不是最好的选择。作为技术人员,最怕的就是手里只有一把锤子,看什么都像钉子。

Tauri 这两年火得一塌糊涂,用Rust写的,前端还是用你熟悉的HTML/CSS/JS,但后端不是Node.js了,是Rust。它的最大卖点是打包体积小,一般也就几MB,比Electron那动辄上百MB的体型优雅多了。而且内存占用也低,毕竟不用带个完整的Chromium。但缺点是Rust学习曲线陡,生态也不如Node.js丰富。如果你要做个简单的工具类应用,对体积敏感,Tauri确实值得看看。

NW.js 算是Electron的前辈了,当年叫node-webkit。它的架构和Electron不太一样,Node.js和Chromium运行在同一个上下文里,代码写起来更直接,不用搞什么IPC通信。但这也带来了安全隐患,而且社区活跃度现在不如Electron,大厂背书也少。

还有Flutter Desktop、React Native Windows这些,但那些已经不算"前端技术栈"了,得学Dart或者原生开发那一套,咱们今天就不展开说了。

为啥大厂都爱用Electron?

你可能要问了,既然Electron有这么多缺点(后面我会详细吐槽),为啥VS Code、Discord、Slack、Figma这些大牌都用它?

答案很简单:开发效率。对于这些公司来说,招一个会JavaScript的前端工程师太容易了,招一个会C++还会Qt的桌面端开发?那难度和成本都高得多。而且Electron的跨平台能力是真的香,一套代码打三个包(Windows、macOS、Linux),虽然细节上还是要做适配,但总比写三份代码强。

VS Code团队曾经分享过,他们用Electron能快速迭代,每周都能发版本,这在原生开发里是很难想象的。而且现代电脑的配置越来越高,用户其实没那么在意那几百MB的体积和几百MB的内存占用——只要你的软件好用。Discord和Slack也是这个道理,它们的核心竞争力在于网络通信和用户体验,而不是极致的性能优化。

但你要注意,这些大厂用Electron,不代表你也要无脑选。他们有一整个团队来优化性能、处理兼容性、维护更新机制。你一个人搞项目,要是做个简单的计算器也用Electron,用户下载个200MB的安装包,打开一看就是个加减乘除,不骂娘才怪。

手把手教你把网页"塞"进桌面图标里

脚手架一把梭,别傻傻敲命令

我知道很多教程喜欢从npm init开始,一步步教你配webpack、配babel、配这配那。我呸,都2024年了,谁还这么干啊?直接上脚手架,五分钟跑起来Hello World,不香吗?

官方推荐的electron-forge就挺好用,或者社区里流行的electron-vite(如果你喜欢用Vite的话)。我个人推荐electron-forge,因为它封装好了打包、签名、自动更新这些脏活累活,省得你后面踩坑。

# 全局安装electron-forge的CLI工具
npm install -g @electron-forge/cli

# 创建新项目,my-app是你的项目名
electron-forge init my-app

# 进入目录
cd my-app

# 启动开发模式,带热重载的,改代码自动刷新
npm start

就这么三行命令,一个完整的Electron项目就搭好了。目录结构长这样:

my-app/
├── src/ # 源代码目录
│ ├── index.html # 主界面
│ ├── index.js # 主进程入口(就是前面的main.js)
│ ├── preload.js # 预加载脚本
│ └── renderer.js # 渲染进程的JS(如果你不想把JS写在HTML里)
├── forge.config.js # Electron Forge的配置文件
├── package.json # 依赖管理
└── assets/ # 放图标、图片等静态资源

forge.config.js里可以配置打包选项,比如应用图标、安装包格式、签名证书这些。开发阶段基本不用动它,默认配置就能跑。

main.js里的那些破事儿

前面我已经给了一个基础的main.js示例,但实际项目里,你要考虑的事情还多着呢。比如窗口大小怎么记?用户上次把窗口拖成啥样了,下次打开应该保持吧?还有系统托盘,很多应用点了关闭其实是最小化到托盘,而不是真的退出。

// 一个更完整的main.js,带窗口状态记忆和系统托盘
const { app, BrowserWindow, Tray, Menu, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
const Store = require('electron-store'); // 需要npm install electron-store

// 用这个库来存用户配置,比直接读写文件方便多了
const store = new Store();

let mainWindow;
let tray;

function createWindow() {
// 从store里读取上次保存的窗口状态,没有就用默认值
const windowState = store.get('windowState', {
width: 1200,
height: 800,
x: undefined,
y: undefined,
maximized: false
});

mainWindow = new BrowserWindow({
width: windowState.width,
height: windowState.height,
x: windowState.x,
y: windowState.y,
minWidth: 800, // 限制最小尺寸,别让用户缩得太小
minHeight: 600,
titleBarStyle: 'hiddenInset', // macOS上好看一点的标题栏
show: false, // 先不显示,等加载完了再show,避免白屏尴尬
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
enableRemoteModule: false,
nodeIntegration: false,
// 安全相关:禁止eval,禁止外部内容
allowEval: false,
webSecurity: true
}
});

// 加载页面
mainWindow.loadFile('src/index.html');

// 页面加载完成后再显示窗口,体验更好
mainWindow.once('ready-to-show', () => {
mainWindow.show();
if (windowState.maximized) {
mainWindow.maximize();
}
});

// 窗口关闭前保存状态
mainWindow.on('close', (event) => {
// 如果配置了最小化到托盘,就不真正关闭
if (store.get('minimizeToTray', true) && !app.isQuiting) {
event.preventDefault();
mainWindow.hide();
return;
}

// 保存窗口状态
const bounds = mainWindow.getBounds();
store.set('windowState', {
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
maximized: mainWindow.isMaximized()
});
});

// 真正关闭时的清理
mainWindow.on('closed', () => {
mainWindow = null;
});
}

// 创建系统托盘图标
function createTray() {
// 不同平台图标格式不一样,Windows用ico,macOS用png/template
const iconPath = process.platform === 'win32'
? path.join(__dirname, 'assets', 'tray.ico')
: path.join(__dirname, 'assets', 'trayTemplate.png');

tray = new Tray(iconPath);

const contextMenu = Menu.buildFromTemplate([
{
label: '显示应用',
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
}
},
{
label: '退出',
click: () => {
app.isQuiting = true;
app.quit();
}
}
]);

tray.setToolTip('我的Electron应用');
tray.setContextMenu(contextMenu);

// 点击托盘图标显示/隐藏窗口
tray.on('click', () => {
if (mainWindow) {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
}
});
}

// 应用初始化
app.whenReady().then(() => {
createWindow();
createTray();

// macOS上点击dock图标重新创建窗口
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});

// 所有窗口关闭时退出(Windows/Linux)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

// 处理打开文件的对话框,比直接写路径安全多了
ipcMain.handle('show-open-dialog', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: '文本文件', extensions: ['txt', 'md'] },
{ name: '所有文件', extensions: ['*'] }
]
});

if (!result.canceled && result.filePaths.length > 0) {
try {
const content = await fs.promises.readFile(result.filePaths[0], 'utf-8');
return { success: true, content, path: result.filePaths[0] };
} catch (error) {
return { success: false, error: error.message };
}
}
return { canceled: true };
});

// 处理保存对话框
ipcMain.handle('show-save-dialog', async (event, content, defaultPath) => {
const result = await dialog.showSaveDialog(mainWindow, {
defaultPath: defaultPath || 'untitled.txt',
filters: [
{ name: '文本文件', extensions: ['txt'] },
{ name: 'Markdown', extensions: ['md'] }
]
});

if (!result.canceled) {
try {
await fs.promises.writeFile(result.filePath, content, 'utf-8');
return { success: true, path: result.filePath };
} catch (error) {
return { success: false, error: error.message };
}
}
return { canceled: true };
});

preload脚本:别把它当摆设

很多人写Electron,preload.js里就写两行代码,把IPC暴露出去就完事了。这其实挺浪费的,preload脚本是个绝佳的"安全层"和"适配层"。

你想啊,渲染进程里的代码理论上是可以被用户篡改的(虽然桌面应用没浏览器那么危险,但万一呢?)。如果你在preload里做好参数校验、权限控制,就能避免很多安全问题。而且,如果以后你要迁移到Tauri或者其他框架,只需要改preload里的实现,渲染进程的代码可以完全不动。

// 一个更完善的preload.js示例
const { contextBridge, ipcRenderer, shell } = require('electron');

// 白名单机制,只允许访问特定的文件路径
const ALLOWED_PATHS = [
app.getPath('userData'), // 应用数据目录
app.getPath('documents'), // 用户文档目录
// 其他允许的路径…
];

function isPathAllowed(filePath) {
// 简单的路径校验,防止目录遍历攻击
const normalizedPath = path.normalize(filePath);
return ALLOWED_PATHS.some(allowedPath =>
normalizedPath.startsWith(allowedPath)
);
}

contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作,带路径白名单校验
file: {
read: async (filePath) => {
if (!isPathAllowed(filePath)) {
throw new Error('Access denied: Path not allowed');
}
return ipcRenderer.invoke('read-file', filePath);
},

write: async (filePath, content) => {
if (!isPathAllowed(filePath)) {
throw new Error('Access denied: Path not allowed');
}
// 内容大小校验,别让用户写个几个G的文件把硬盘撑爆
if (Buffer.byteLength(content, 'utf-8') > 10 * 1024 * 1024) {
throw new Error('File too large (max 10MB)');
}
return ipcRenderer.invoke('write-file', filePath, content);
},

// 打开文件对话框,安全的方式让用户选文件
openDialog: () => ipcRenderer.invoke('show-open-dialog'),
saveDialog: (content, defaultPath) =>
ipcRenderer.invoke('show-save-dialog', content, defaultPath)
},

// 系统信息
system: {
platform: process.platform,
versions: process.versions,
// 获取特殊目录路径,比如下载文件夹、桌面路径
getPath: (name) => ipcRenderer.invoke('get-path', name)
},

// 窗口控制
window: {
minimize: () => ipcRenderer.send('window-minimize'),
maximize: () => ipcRenderer.send('window-maximize'),
close: () => ipcRenderer.send('window-close'),
// 设置是否最小化到托盘
setMinimizeToTray: (value) => {
ipcRenderer.send('set-minimize-to-tray', value);
}
},

// 用系统默认浏览器打开外链,别在应用里直接跳,不安全
openExternal: (url) => {
// 简单的URL校验,只允许http/https
if (!/^https?:\\/\\//.test(url)) {
console.error('Invalid URL protocol');
return;
}
shell.openExternal(url);
},

// 监听主进程发来的事件,比如自动更新通知
onUpdateAvailable: (callback) => {
ipcRenderer.on('update-available', callback);
},

// 移除监听器,防止内存泄漏
removeAllListeners: (channel) => {
ipcRenderer.removeAllListeners(channel);
}
});

这框架虽好,但这几个坑踩进去就拔不出来

打包体积:塞了整个Chrome进去?

这是Electron被吐槽最多的点。你写了个简单的记事本应用,代码可能就几十KB,结果打包出来一个exe,200多MB。用户下载的时候还以为你塞了什么高清大片进去。

为啥这么大?因为Electron把Chromium和Node.js的运行时都打包进去了。Chromium本身就几十MB,Node.js也有几十MB,再加上各种依赖,体积就上去了。

怎么优化?首先,用electron-builder或者electron-forge的时候,配置一下files字段,别把node_modules里所有东西都塞进去。很多开发依赖(比如eslint、webpack、babel)生产环境根本用不到。

// forge.config.js 示例,控制打包内容
module.exports = {
packagerConfig: {
// 忽略这些文件和目录,减小体积
ignore: [
/^\\/src\\/assets\\/raw\\//, // 原始资源文件
/^\\/tests?\\//, // 测试文件
/^\\/\\.git\\//, // git历史
/^\\/\\.vscode\\//, // 编辑器配置
/README\\.md/, // 文档
/\\.map$/ // source map文件
],
// 压缩级别,9是最高压缩,但打包时间会长
compression: 'maximum',
// 去掉调试符号,减小体积
prune: true
},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
// Windows安装包配置
name: 'my_electron_app',
// 图标路径
setupIcon: 'assets/icon.ico',
// 跳过创建Delta包,如果你不需要增量更新的话
skipUpdateIcon: true
}
},
{
name: '@electron-forge/maker-dmg',
config: {
// macOS DMG配置
icon: 'assets/icon.icns',
// 背景图,显得专业
background: 'assets/dmg-background.png',
// 窗口大小和图标位置,自己调调看
windowSize: { width: 600, height: 400 }
}
}
]
};

其次,考虑用electron-updater做增量更新,这样用户不用每次都下载完整包。还有,如果你的应用真的特别简单,可以看看前面提到的Tauri,那个体积确实小。

内存占用:低配电脑杀手

Electron应用的内存占用确实比原生应用高。Chromium本身就是个内存大户,再加上Node.js,开个应用占用几百MB内存是常态。如果你再不注意代码质量,内存泄漏起来,几个GB都能给你吃掉。

怎么监控?主进程和渲染进程都可以用Node.js的process.memoryUsage()来查看内存使用情况。渲染进程还可以在Chrome开发者工具的Memory面板里做Heap Snapshot分析。

// 在main.js里定期打印内存使用情况,调试用
setInterval(() => {
const usage = process.memoryUsage();
console.log('主进程内存使用:', {
rss: `${(usage.rss / 1024 / 1024).toFixed(2)} MB`, // 常驻内存
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`, // V8总堆内存
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`, // V8已用堆内存
external: `${(usage.external / 1024 / 1024).toFixed(2)} MB` // C++对象内存
});
}, 30000); // 每30秒打印一次

// 渲染进程里也可以用,但要在preload里暴露process对象
// 或者在控制台直接输入
setInterval(() => {
const usage = performance.memory; // Chrome特有的API
console.log('渲染进程内存:', {
usedJSHeapSize: `${(usage.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
totalJSHeapSize: `${(usage.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
jsHeapSizeLimit: `${(usage.jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB`
});
}, 30000);

自动更新:配置起来想砸键盘

Electron的自动更新功能,官方文档写得云里雾里,实际配置起来一堆坑。Windows上要用Squirrel.Windows,macOS上要用Squirrel.Mac,Linux更麻烦,还得自己配服务器。

简单来说,你需要:

  • 一个静态文件服务器,存放更新包(可以用GitHub Releases、S3、或者自己的服务器)
  • 签名证书(macOS和Windows都需要,不然更新会失败)
  • 在main.js里集成electron-updater
  • // main.js里集成自动更新
    const { autoUpdater } = require('electron-updater');
    const log = require('electron-log');

    // 配置日志
    autoUpdater.logger = log;
    autoUpdater.logger.transports.file.level = 'info';

    // 检查更新
    function checkForUpdates() {
    // 开发环境不检查更新,不然调试的时候烦死
    if (process.env.NODE_ENV === 'development') {
    return;
    }

    autoUpdater.checkForUpdatesAndNotify();
    }

    // 各种更新事件处理
    autoUpdater.on('checking-for-update', () => {
    console.log('正在检查更新…');
    });

    autoUpdater.on('update-available', (info) => {
    console.log('发现新版本:', info.version);
    // 可以在这里给渲染进程发消息,显示更新提示
    if (mainWindow) {
    mainWindow.webContents.send('update-available', info);
    }
    });

    autoUpdater.on('update-not-available', () => {
    console.log('当前已是最新版本');
    });

    autoUpdater.on('error', (err) => {
    console.error('更新出错:', err);
    });

    autoUpdater.on('download-progress', (progressObj) => {
    let logMessage = `下载速度: ${progressObj.bytesPerSecond}`;
    logMessage += ` – 已下载 ${progressObj.percent}%`;
    logMessage += ` (${progressObj.transferred}/${progressObj.total})`;
    console.log(logMessage);
    });

    autoUpdater.on('update-downloaded', (info) => {
    console.log('更新下载完成');
    // 可以提示用户重启应用,或者静默安装
    // autoUpdater.quitAndInstall(); // 强制重启安装
    });

    // 应用启动后检查更新
    app.whenReady().then(() => {
    createWindow();
    // 延迟5秒检查更新,避免启动时卡死
    setTimeout(checkForUpdates, 5000);
    });

    安全性:别让应用变成筛子

    Electron应用因为能直接访问系统API,如果被攻击者利用,危害比普通网页大得多。几个必须做的安全措施:

  • 开启上下文隔离(contextIsolation):前面代码里一直强调这个,一定要设为true,不然渲染进程能直接访问Node.js API,XSS攻击直接变RCE(远程代码执行)。
  • 禁用Node集成(nodeIntegration):设为false,通过preload脚本按需暴露API。
  • 内容安全策略(CSP):在HTML里设置CSP头,限制能加载的资源。
  • 校验所有输入:特别是文件路径、URL这些,防止目录遍历和协议劫持。
  • <!– 在index.html的head里加CSP –>
    <meta http-equiv="Content-Security-Policy"
    content="default-src 'self';
    script-src 'self' 'unsafe-inline';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
    connect-src 'self' https://api.example.com;"
    >

    实战环节:做个能摸鱼的本地记事本

    好了,理论说得差不多了,咱们来实战一个项目。做个极简的本地记事本,功能不多:能打字、能保存到本地文件、能全局快捷键快速隐藏——对,就是给摸鱼用的。

    界面就用Tailwind CSS随便画画,主打一个性冷淡风。核心功能是调用本地文件系统,数据存在用户电脑上,不用担心隐私泄露。

    <!– src/index.html –>
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!– CSP安全头 –>
    <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; font-src 'self' https://fonts.gstatic.com;">

    <title>摸鱼记事本</title>
    <!– 用Tailwind CDN快速开发,生产环境建议换成构建后的CSS –>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
    /* 自定义滚动条,显得精致一点 */
    ::-webkit-scrollbar {
    width: 8px;
    height: 8px;
    }
    ::-webkit-scrollbar-track {
    background: transparent;
    }
    ::-webkit-scrollbar-thumb {
    background: #cbd5e1;
    border-radius: 4px;
    }
    ::-webkit-scrollbar-thumb:hover {
    background: #94a3b8;
    }

    /* 文本域聚焦时去掉蓝色边框,用灰色更低调 */
    textarea:focus {
    outline: none;
    }

    /* 拖拽区域样式,用于拖拽打开文件 */
    .drag-over {
    background-color: #f0f9ff !important;
    border: 2px dashed #3b82f6 !important;
    }
    </style>
    </head>
    <body class="bg-gray-50 h-screen flex flex-col overflow-hidden">
    <!– 标题栏区域,macOS上隐藏原生标题栏后自己画一个 –>
    <div class="h-10 bg-white border-b border-gray-200 flex items-center px-4 select-none drag-region">
    <div class="flex items-center space-x-2">
    <div class="w-3 h-3 rounded-full bg-red-400"></div>
    <div class="w-3 h-3 rounded-full bg-yellow-400"></div>
    <div class="w-3 h-3 rounded-full bg-green-400"></div>
    </div>
    <div class="flex-1 text-center text-sm text-gray-600 font-medium" id="title">
    摸鱼记事本 – 未命名
    </div>
    <div class="text-xs text-gray-400" id="save-status">
    已就绪
    </div>
    </div>

    <!– 工具栏 –>
    <div class="bg-white border-b border-gray-200 px-4 py-2 flex items-center space-x-2">
    <button onclick="newFile()" class="px-3 py-1.5 text-sm bg-gray-100 hover:bg-gray-200 rounded-md transition-colors flex items-center space-x-1">
    <span>📝</span>
    <span>新建</span>
    </button>
    <button onclick="openFile()" class="px-3 py-1.5 text-sm bg-gray-100 hover:bg-gray-200 rounded-md transition-colors flex items-center space-x-1">
    <span>📂</span>
    <span>打开</span>
    </button>
    <button onclick="saveFile()" class="px-3 py-1.5 text-sm bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors flex items-center space-x-1">
    <span>💾</span>
    <span>保存</span>
    </button>
    <div class="flex-1"></div>
    <span class="text-xs text-gray-400" id="word-count">0 字</span>
    </div>

    <!– 编辑器区域,支持拖拽打开文件 –>
    <div class="flex-1 relative" id="drop-zone">
    <textarea
    id="editor"
    class="w-full h-full p-6 resize-none bg-white text-gray-800 leading-relaxed text-base"
    placeholder="在这里开始你的摸鱼大业…&#10;&#10;快捷键:&#10;Ctrl/Cmd + N 新建&#10;Ctrl/Cmd + O 打开&#10;Ctrl/Cmd + S 保存&#10;Ctrl/Cmd + H 老板键(隐藏窗口)"
    >
    </textarea>

    <!– 拖拽提示遮罩 –>
    <div id="drag-overlay" class="absolute inset-0 bg-blue-50 bg-opacity-90 border-4 border-blue-400 border-dashed hidden flex items-center justify-center">
    <div class="text-center">
    <div class="text-4xl mb-2">📄</div>
    <div class="text-lg text-blue-600 font-medium">松开以打开文件</div>
    </div>
    </div>
    </div>

    <!– 底部状态栏 –>
    <div class="bg-gray-100 border-t border-gray-200 px-4 py-1.5 flex items-center justify-between text-xs text-gray-500">
    <div class="flex items-center space-x-4">
    <span id="file-path">未选择文件</span>
    <span id="encoding">UTF-8</span>
    </div>
    <div class="flex items-center space-x-4">
    <span id="cursor-pos">行 1, 列 1</span>
    <span>Electron + ❤️</span>
    </div>
    </div>

    <script>
    const editor = document.getElementById('editor');
    const title = document.getElementById('title');
    const saveStatus = document.getElementById('save-status');
    const wordCount = document.getElementById('word-count');
    const filePathDisplay = document.getElementById('file-path');
    const cursorPos = document.getElementById('cursor-pos');
    const dropZone = document.getElementById('drop-zone');
    const dragOverlay = document.getElementById('drag-overlay');

    let currentFilePath = null;
    let isModified = false;
    let saveTimeout = null;

    // 初始化:加载上次打开的文件(如果有的话)
    window.addEventListener('DOMContentLoaded', async () => {
    const lastFile = localStorage.getItem('lastOpenedFile');
    if (lastFile) {
    // 这里简化处理,实际应该用IPC询问主进程文件是否还存在
    console.log('上次打开的文件:', lastFile);
    }
    });

    // 监听输入,自动统计字数和标记修改状态
    editor.addEventListener('input', () => {
    updateWordCount();
    markAsModified();

    // 防抖自动保存(如果已经保存过文件)
    if (currentFilePath && saveTimeout) {
    clearTimeout(saveTimeout);
    saveTimeout = setTimeout(() => {
    autoSave();
    }, 2000); // 停止输入2秒后自动保存
    }
    });

    // 监听光标位置变化
    editor.addEventListener('keyup', updateCursorPos);
    editor.addEventListener('click', updateCursorPos);

    function updateWordCount() {
    const text = editor.value;
    // 中文字符和英文单词分别统计
    const cnCount = (text.match(/[\\u4e00-\\u9fa5]/g) || []).length;
    const enCount = (text.match(/[a-zA-Z]+/g) || []).length;
    const total = cnCount + enCount + (text.length cnCount (text.match(/[a-zA-Z]/g) || []).length);
    wordCount.textContent = `${total}`;
    }

    function updateCursorPos() {
    const pos = editor.selectionStart;
    const textUpToCursor = editor.value.substring(0, pos);
    const lines = textUpToCursor.split('\\n');
    const currentLine = lines.length;
    const currentCol = lines[lines.length 1].length + 1;
    cursorPos.textContent = `${currentLine}, 列 ${currentCol}`;
    }

    function markAsModified() {
    if (!isModified) {
    isModified = true;
    updateTitle();
    saveStatus.textContent = '未保存';
    saveStatus.className = 'text-xs text-orange-500';
    }
    }

    function updateTitle() {
    const fileName = currentFilePath ?
    currentFilePath.split(/[\\\\/]/).pop() : '未命名';
    title.textContent = `摸鱼记事本 – ${fileName}${isModified ? ' *' : ''}`;
    }

    // 新建文件
    function newFile() {
    if (isModified) {
    // 实际项目这里应该弹窗询问是否保存
    if (!confirm('当前文件未保存,确定要新建吗?')) {
    return;
    }
    }
    editor.value = '';
    currentFilePath = null;
    isModified = false;
    updateTitle();
    updateWordCount();
    filePathDisplay.textContent = '未选择文件';
    saveStatus.textContent = '已就绪';
    saveStatus.className = 'text-xs text-gray-400';
    localStorage.removeItem('lastOpenedFile');
    }

    // 打开文件
    async function openFile() {
    try {
    const result = await window.electronAPI.file.openDialog();
    if (result.canceled) return;

    if (result.success) {
    editor.value = result.content;
    currentFilePath = result.path;
    isModified = false;
    updateTitle();
    updateWordCount();
    filePathDisplay.textContent = currentFilePath;
    saveStatus.textContent = '已保存';
    saveStatus.className = 'text-xs text-green-600';
    localStorage.setItem('lastOpenedFile', currentFilePath);
    } else {
    alert('打开文件失败:' + result.error);
    }
    } catch (error) {
    console.error('打开文件出错:', error);
    alert('打开文件时发生错误');
    }
    }

    // 保存文件
    async function saveFile() {
    try {
    const content = editor.value;
    const result = await window.electronAPI.file.saveDialog(content, currentFilePath);

    if (result.canceled) return;

    if (result.success) {
    currentFilePath = result.path;
    isModified = false;
    updateTitle();
    filePathDisplay.textContent = currentFilePath;
    saveStatus.textContent = '已保存';
    saveStatus.className = 'text-xs text-green-600';
    localStorage.setItem('lastOpenedFile', currentFilePath);

    // 显示个短暂的保存成功提示
    const originalText = saveStatus.textContent;
    saveStatus.textContent = '保存成功!';
    setTimeout(() => {
    saveStatus.textContent = originalText;
    }, 2000);
    } else {
    alert('保存失败:' + result.error);
    }
    } catch (error) {
    console.error('保存文件出错:', error);
    alert('保存文件时发生错误');
    }
    }

    // 自动保存(静默保存,不弹提示)
    async function autoSave() {
    if (!currentFilePath || !isModified) return;

    try {
    // 这里应该调用一个后台保存的IPC,不弹对话框
    // 简化起见,复用saveDialog,但传入当前路径
    const result = await window.electronAPI.file.saveDialog(editor.value, currentFilePath);
    if (result.success) {
    isModified = false;
    updateTitle();
    saveStatus.textContent = '自动保存于 ' + new Date().toLocaleTimeString();
    saveStatus.className = 'text-xs text-blue-500';
    }
    } catch (error) {
    console.error('自动保存失败:', error);
    }
    }

    // 键盘快捷键
    document.addEventListener('keydown', (e) => {
    const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
    const ctrlKey = isMac ? e.metaKey : e.ctrlKey;

    if (ctrlKey) {
    switch(e.key.toLowerCase()) {
    case 'n':
    e.preventDefault();
    newFile();
    break;
    case 'o':
    e.preventDefault();
    openFile();
    break;
    case 's':
    e.preventDefault();
    saveFile();
    break;
    case 'h':
    e.preventDefault();
    // 老板键:通知主进程隐藏窗口
    window.electronAPI.window.hide();
    break;
    }
    }
    });

    // 拖拽文件打开功能
    dropZone.addEventListener('dragover', (e) => {
    e.preventDefault();
    dropZone.classList.add('drag-over');
    dragOverlay.classList.remove('hidden');
    });

    dropZone.addEventListener('dragleave', (e) => {
    e.preventDefault();
    dropZone.classList.remove('drag-over');
    dragOverlay.classList.add('hidden');
    });

    dropZone.addEventListener('drop', async (e) => {
    e.preventDefault();
    dropZone.classList.remove('drag-over');
    dragOverlay.classList.add('hidden');

    const files = e.dataTransfer.files;
    if (files.length > 0) {
    const file = files[0];
    // 检查文件类型,只接受文本文件
    if (!file.type.match('text.*') && !file.name.match(/\\.(txt|md|json|js|html|css)$/i)) {
    alert('暂不支持该文件类型,请拖拽文本文件');
    return;
    }

    // 读取拖拽的文件,这里简化处理,实际应该用IPC传给主进程读取
    const reader = new FileReader();
    reader.onload = (event) => {
    editor.value = event.target.result;
    currentFilePath = file.path; // Electron里file对象有path属性
    isModified = false;
    updateTitle();
    updateWordCount();
    filePathDisplay.textContent = currentFilePath;
    };
    reader.readAsText(file);
    }
    });

    // 监听主进程发来的事件(比如全局快捷键触发)
    window.electronAPI.onCommand?.((event, command) => {
    switch(command) {
    case 'new-file':
    newFile();
    break;
    case 'open-file':
    openFile();
    break;
    case 'save-file':
    saveFile();
    break;
    case 'hide-window':
    // 窗口隐藏前自动保存
    if (isModified && currentFilePath) {
    autoSave();
    }
    break;
    }
    });
    </script>
    </body>
    </html>

    // 更新后的main.js,加上全局快捷键支持
    const { app, BrowserWindow, globalShortcut, ipcMain } = require('electron');
    // … 其他引入和之前的代码类似

    function createWindow() {
    // … 窗口创建代码

    // 注册全局快捷键(老板键)
    const registerShortcuts = () => {
    // Ctrl/Cmd + Shift + H 全局隐藏/显示
    globalShortcut.register('CommandOrControl+Shift+H', () => {
    if (mainWindow.isVisible()) {
    mainWindow.hide();
    // 通知渲染进程做保存等清理工作
    mainWindow.webContents.send('command', 'hide-window');
    } else {
    mainWindow.show();
    mainWindow.focus();
    }
    });

    // 还可以加其他全局快捷键,比如快速新建
    globalShortcut.register('CommandOrControl+Shift+N', () => {
    mainWindow.webContents.send('command', 'new-file');
    });
    };

    // 窗口准备好后注册快捷键
    mainWindow.once('ready-to-show', () => {
    mainWindow.show();
    registerShortcuts();
    });

    // … 其他事件监听
    }

    // 应用退出前注销所有全局快捷键
    app.on('will-quit', () => {
    globalShortcut.unregisterAll();
    });

    // 处理渲染进程发来的隐藏窗口请求
    ipcMain.on('hide-window', () => {
    if (mainWindow) {
    mainWindow.hide();
    }
    });

    这个记事本虽然功能简单,但涵盖了Electron开发的核心要素:窗口管理、IPC通信、文件系统访问、全局快捷键、拖拽交互。你可以在此基础上加更多功能,比如Markdown预览、多标签页、云同步等等。

    最后打包成安装包,用npm run make(如果你用electron-forge的话),然后在out目录里找到你的.exe或者.dmg文件,发给死党炫耀吧。虽然这玩意儿就是个套壳的网页,但双击图标打开的那一刻,逼格确实比打开浏览器输入localhost:3000高多了。

    程序崩了别只会重启,这几招能让你少掉几根头发

    主进程挂了怎么查?

    主进程崩了最麻烦,因为开发者工具是跟着渲染进程的,主进程报错你根本看不见。这时候日志系统就很重要了。推荐用electron-log这个库,它能把日志写到本地文件里,崩溃了去日志文件里找线索。

    // 在main.js最前面就引入日志
    const log = require('electron-log');

    // 配置日志路径,存在用户数据目录里
    log.transports.file.resolvePath = () => {
    return path.join(app.getPath('userData'), 'logs/main.log');
    };

    // 把console也重定向到日志文件,这样console.log也能被记录
    Object.assign(console, log.functions);

    // 捕获未处理的异常
    process.on('uncaughtException', (error) => {
    log.error('未捕获的异常:', error);
    // 可以在这里弹个错误对话框,或者上报到服务器
    dialog.showErrorBox('应用出错', error.message);
    });

    // 捕获Promise未处理的rejection
    process.on('unhandledRejection', (reason, promise) => {
    log.error('未处理的Promise rejection:', reason);
    });

    渲染进程白屏了?

    有时候窗口打开了,但里面是白的,开发者工具也打不开。这通常是渲染进程崩溃了或者加载失败了。可能的原因:HTML路径写错了、preload脚本报错了、CSP拦截了资源。

    调试方法:在main.js里监听render-process-gone事件,能看到崩溃原因。

    mainWindow.webContents.on('render-process-gone', (event, details) => {
    console.error('渲染进程崩溃:', details.reason);
    log.error('渲染进程崩溃:', details);

    // 可以在这里尝试重新加载,或者提示用户重启
    dialog.showErrorBox('页面崩溃', '应用页面发生错误,即将重启');
    app.relaunch();
    app.quit();
    });

    内存泄漏排查

    如果你发现应用越跑越慢,内存占用越来越高,那可能是内存泄漏了。Chrome开发者工具的Memory面板是神器,可以拍Heap Snapshot对比分析。

    在Electron里,你可以这样打开开发者工具:

    // 主进程里打开渲染进程的开发者工具
    mainWindow.webContents.openDevTools();

    // 或者只打开特定的面板,比如Memory
    mainWindow.webContents.openDevTools({ mode: 'detach' });
    mainWindow.webContents.devToolsWebContents?.executeJavaScript(`
    UI.inspectorView.showPanel('heap_profiler');
    `
    );

    版本对齐很重要

    Electron社区里那些奇奇怪怪的报错,十有八九是版本问题。Electron版本更新很快,API经常有变动。比如remote模块在Electron 12之后默认禁用,14之后彻底移除;contextIsolation默认值也改过。

    所以,一定要:

  • 锁定package.json里的Electron版本,别随便升级
  • 查文档的时候看清楚版本号,别拿着v28的文档去调v20的API
  • 原生模块(比如sqlite3、sharp)需要针对Electron版本重新编译,用electron-rebuild工具
  • # 安装electron-rebuild
    npm install –save-dev electron-rebuild

    # 重新编译所有原生模块
    npx electron-rebuild

    # 或者指定Electron版本
    npx electron-rebuild -v 28.0.0

    几个让代码看起来像"老手"写的骚操作

    别把逻辑都堆在渲染进程

    渲染进程里跑太多计算密集型任务,界面会卡成PPT。比如你要处理个大JSON文件、生成个PDF、压缩图片,这些都应该扔给主进程或者Worker线程。

    // 主进程里处理耗时任务,通过IPC返回结果
    const { Worker } = require('worker_threads');

    ipcMain.handle('heavy-task', async (event, data) => {
    return new Promise((resolve, reject) => {
    const worker = new Worker('./src/worker.js');

    worker.postMessage(data);

    worker.on('message', (result) => {
    resolve(result);
    worker.terminate();
    });

    worker.on('error', reject);
    });
    });

    上下文隔离一定要开

    前面说了无数遍了,contextIsolation: true是保命设置。别为了图方便关了它,那样做等于把Node.js API直接暴露在网页里,XSS攻击直接变系统级攻击。

    多窗口架构

    如果你的应用需要多个窗口(比如主窗口+设置窗口+关于窗口),别傻乎乎地每个窗口都新建一个BrowserWindow然后加载不同HTML。可以做个窗口管理器,统一管理窗口状态、通信、生命周期。

    // window-manager.js
    class WindowManager {
    constructor() {
    this.windows = new Map();
    }

    create(name, options) {
    if (this.windows.has(name)) {
    const win = this.windows.get(name);
    win.focus();
    return win;
    }

    const win = new BrowserWindow(options);
    this.windows.set(name, win);

    win.on('closed', () => {
    this.windows.delete(name);
    });

    return win;
    }

    get(name) {
    return this.windows.get(name);
    }

    broadcast(channel, args) {
    this.windows.forEach(win => {
    if (!win.isDestroyed()) {
    win.webContents.send(channel, args);
    }
    });
    }
    }

    module.exports = new WindowManager();

    静态资源压缩

    打包的时候把图片压缩一下,CSS和JS用webpack或者vite压缩,别让安装包胖得像个球。Electron-builder支持asar打包,能把资源文件打包成归档格式,减小体积还能防篡改。

    // forge.config.js里配置asar
    packagerConfig: {
    asar: true,
    // 排除某些大文件不打包进asar,按需加载
    asarUnpack: [
    'node_modules/sharp/**',
    'assets/large-files/**'
    ]
    }

    最后唠两句:这玩意儿也不是万能药

    说了这么多Electron的好,也得泼点冷水。这玩意儿确实不是银弹,有些场景下用它就是给自己找不痛快。

    如果你只是写个简单的内部工具,比如给运营同学用的数据录入工具、给测试同学用的接口调试工具,Electron确实爽翻天。开发快、跨平台、界面好看,还能直接调用本地资源,比用Python写个Tkinter界面强多了。

    但如果你要做的是性能敏感型应用,比如视频剪辑软件、大型游戏、专业音频处理,趁早换赛道。Electron的性能瓶颈在Chromium和Node.js的架构上,不是优化能解决的。给十年前的老电脑用?别想了,Electron应用最低配置要求就不低。

    还有,如果你的应用需要深度系统集成,比如杀毒软件、系统优化工具、驱动程序,Electron也搞不定。它毕竟是个套壳浏览器,系统底层的东西碰不了。

    技术选型这事儿,最忌讳的就是"为了用而用"。我见过太多人,明明就是个简单的表单页面,非要用Electron打包成客户端,结果用户下载个200MB的安装包,打开一看就是个网页,骂声一片。也见过人明明要做个高性能的图像处理工具,非要用Electron写,结果处理个大图卡成狗。

    所以啊,7天学会Electron是真的,用它搞个项目放简历上加分也是真的,但别神话它。工具就是工具,合适的场景用合适的工具,这才是老手的做法。

    好了,絮叨了这么多,代码也给了不少,剩下的就是你自己动手折腾了。记住,看十遍不如写一遍,遇到报错先查版本,内存爆了记得打快照,打包体积太大就精简依赖。祝你早日搞出自己的桌面应用,工资翻倍那天记得回来请我喝咖啡——虽然咱们这是文字交流,咖啡就心领了吧。

    开工!

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 前端人别卷网页了!7天用Electron搞定桌面应用,工资翻倍不是梦
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址