1. 为什么我们需要一个自己的截图工具说实话最开始冒出这个想法纯粹是因为被日常开发工作给“逼”的。不知道你有没有过这种体验产品经理丢过来一个原型图UI设计师发来一堆标注后端同事又更新了API文档。为了搞清楚一个按钮的交互逻辑我得在浏览器、设计稿软件、文档工具和代码编辑器之间来回切换眼睛都快看花了。有时候为了对比几个版本的UI差异恨不得把屏幕分成四块效率低得让人抓狂。后来我开始用一些现成的截图工具把关键信息“钉”在屏幕上确实方便了不少。但用着用着问题又来了要么功能太臃肿启动慢要么功能太简单连个简单的标注都没有要么就是快捷键冲突或者保存路径不符合我的习惯。每次截图后还得手动整理时间一长桌面就堆满了“截图1”、“截图2”这类文件找起来特别麻烦。就在某个加班的深夜我盯着满屏的窗口突然想为什么不自己做一个呢一个完全按照我工作流定制的、轻量快速的、能随手截图并“钉”住参考信息的工具。这不正是我们开发者最擅长的事吗——用代码解决自己的痛点。而且用Electron可以轻松做出跨平台的桌面应用用Vite又能获得闪电般的开发体验这个组合听起来就很有搞头。于是这个基于 Vite Electron 的区域截屏工具项目就正式启动了。我的目标很明确它要像系统原生工具一样快速响应要有足够实用的标注功能截图后能方便地预览和管理最好还能和我的其他工具链打通。接下来我就把自己从零开始搭建这个工具的过程、踩过的坑以及最终实现方案毫无保留地分享给你。2. 技术选型为什么是 Vite Electron在动手之前技术栈的选择至关重要。市面上构建 Electron 应用的方式不少比如经典的electron-vue、electron-react-boilerplate或者官方推荐的Electron Forge。我几乎都试了一遍最后才坚定地选择了Vite Electron这个组合。为什么听我慢慢道来。首先说说Electron。它的核心价值在于让我们能用熟悉的 Web 技术HTML, CSS, JavaScript来构建跨平台的桌面应用。你写的是一个网页但 Electron 通过集成 Chromium 和 Node.js给了你这个网页访问操作系统底层 API 的能力比如文件系统、系统托盘、原生菜单等。对于我们这个截图工具来说最关键的两个 Electron API 是desktopCapturer 这是实现截屏的基石。它能获取整个屏幕、单个窗口的实时图像流没有它网页可没法直接“看到”屏幕其他部分。ipcMain和ipcRenderer 这是连接 Electron 主进程Node.js 环境和渲染进程网页环境的桥梁。截图逻辑需要主进程去抓取屏幕而截图界面和操作在渲染进程它们之间的通信就靠它了。然后是Vite。我最初也用了基于 Webpack 的方案但开发体验真的被 Vite 秒杀。Vite 的核心优势是“快”。它利用现代浏览器原生支持 ES 模块的特性在开发环境下根本不需要打包而是直接按需编译和提供源码。这意味着冷启动极快 以前改点代码等 dev server 重启要十几秒现在几乎是秒开。热更新HMR迅猛 修改组件后页面更新几乎无感极大地提升了开发调试效率。构建优化 生产环境构建使用 Rollup打包出来的文件更小、更高效。对于需要频繁调试界面和交互的截图工具来说这种快速的反馈循环太重要了。我用的模板是electron-vite-react它已经帮我们做好了 Vite、React 和 Electron 的集成配置开箱即用省去了大量搭建环境的时间。这里有个小插曲我一开始确实尝试了Electron Forge但很快就放弃了。主要原因有两个一是它的编译速度在项目稍大后明显变慢我怀疑是 Webpack 的锅和 Vite 的流畅体验对比强烈二是它的打包配置虽然简单但不够灵活我想自定义一些打包行为时比较麻烦。而electron-vite的方案则更加透明和可控。3. 核心原理一个透明窗口如何“定格”整个屏幕理解了技术栈我们来看看这个工具最核心的工作原理。整个过程就像一场精心编排的“魔术”核心思想是用一个全屏的、完全透明的 Electron 窗口作为“取景框”在这个框里对屏幕画面进行交互式裁剪。3.1 流程拆解一步步看清魔术背后的机关整个截图流程可以分解为以下几个清晰的步骤发起请求 你在主窗口点击“截图”按钮。这个按钮通过ipcRenderer发送一个事件比如ss:open-win给 Electron 的主进程。召唤“取景框” 主进程收到事件立即创建一个新的 BrowserWindow浏览器窗口。这个窗口是关键它的配置必须精心设计fullscreen: true和fullscreenable: true 让它覆盖整个屏幕。transparent: true 窗口背景完全透明这样它本身不会遮挡任何内容。frame: false 去掉窗口边框和标题栏。resizable: false和movable: false 禁止用户调整大小和移动确保它稳定地覆盖全屏。alwaysOnTop: true可选 可以设置为始终在最顶层防止被其他窗口干扰。 这个窗口加载的就是我们的截图交互页面。“定格”屏幕画面 透明窗口创建好后它的渲染进程即我们的 React 截图页面会通过ipcRenderer.invoke调用一个主进程的处理器例如ss:get-shot-screen-img。在这个处理器里主进程使用desktopCapturer.getSources({ types: [screen] })来捕获当前整个屏幕的静态图像。这个图像会被转换成 Data URL一种 base64 格式的图片字符串并返回给渲染进程。绘制与交互 渲染进程拿到全屏图片的 Data URL 后将其设置为一个全屏canvas或img元素的背景。此时用户看到的虽然是“静止”的屏幕画面但实际上它是一个铺满全屏的图片。在这个图片之上我们通过 Canvas 绘制一个半透明的遮罩层以及一个高亮的、可拖拽调整的矩形选择框。用户通过鼠标拖拽就能在这个“定格”的画面上选择想要截取的区域。裁剪与输出 当用户确认选择区域后我们根据矩形框的坐标和尺寸使用canvas的drawImage和getImageData等方法从全屏图片中精确地裁剪出对应的部分生成一个新的图片 Blob 对象。传递与处理 生成的图片 Blob 可以通过URL.createObjectURL转换成临时链接然后通过 IPC 通信传回主进程。主进程可以将其保存为本地文件。使用clipboard.writeImage写入系统剪贴板方便直接粘贴。发送到另一个预览窗口进行展示。收尾工作 操作完成后关闭全屏透明的截图窗口回到主界面或进行下一步操作。3.2 关键 API 与代码片段这里给出最关键的几段代码帮你理解原理如何落地。主进程捕获屏幕// main.js (Electron 主进程) const { desktopCapturer, ipcMain } require(electron); ipcMain.handle(ss:get-shot-screen-img, async () { // 获取当前屏幕的尺寸 const { width, height } getScreenSize(); // 这是一个自定义函数返回屏幕宽高 // 获取屏幕源 const sources await desktopCapturer.getSources({ types: [screen], // 类型为屏幕 thumbnailSize: { width, height } // 指定缩略图尺寸为全屏保证清晰度 }); // 通常第一个源就是主屏幕更严谨的做法是根据 display API 匹配 const primarySource sources.find(source source.name Entire screen || source.id.includes(screen:0)); if (!primarySource) { throw new Error(未能找到屏幕源); } // 将 NativeImage 转换为 Data URL (base64) const dataURL primarySource.thumbnail.toDataURL(); return dataURL; });渲染进程请求图片并初始化截图组件// ShotScreen.jsx (React 截图页面) import { useEffect, useState } from react; import { ipcRenderer } from electron; import Screenshots from react-screenshots; // 一个现成的截图组件库 export default function ShotScreen() { const [screenShotImg, setScreenShotImg] useState(); useEffect(() { // 组件挂载后立即向主进程请求屏幕截图 const fetchScreenImage async () { const imgDataURL await ipcRenderer.invoke(ss:get-shot-screen-img); setScreenShotImg(imgDataURL); }; fetchScreenImage(); }, []); const handleOk (blob, bounds) { // 用户确认截图blob是裁剪后的图片数据 const downloadUrl URL.createObjectURL(blob); // 通知主进程保存或处理图片 ipcRenderer.send(ss:save-img, downloadUrl); }; const handleCancel () { // 用户取消关闭截图窗口 ipcRenderer.send(ss:close-win); }; return ( div style{{ width: 100vw, height: 100vh }} {/* 将获取到的全屏图片传递给截图组件作为背景 */} Screenshots url{screenShotImg} width{window.innerWidth} height{window.innerHeight} onOk{handleOk} onCancel{handleCancel} / /div ); }4. 项目实战从零搭建的详细步骤光说不练假把式我们一步步来把这个工具搭建出来。我会假设你已经有基本的 Node.js、React 和 Git 知识。4.1 环境初始化与项目创建首先我们需要一个项目骨架。使用electron-vite-react模板是最快的方式。# 1. 使用模板创建项目 npm create quick-start/electronlatest my-screenshot-tool -- --template react # 或使用官方示例仓库 git clone https://github.com/electron-vite/electron-vite-react cd electron-vite-react pnpm install # 推荐使用 pnpm速度更快创建完成后你的项目结构大致如下my-screenshot-tool/ ├── src/ │ ├── main/ # Electron 主进程代码 │ ├── preload/ # 预加载脚本安全隔离 │ └── renderer/ # React 渲染进程代码我们的前端页面 ├── electron.vite.config.ts # ViteElectron 配置 └── package.json进入项目先安装我们后续需要的核心依赖pnpm add react-router-dom antd ant-design/iconsreact-router-dom 用来管理我们不同的页面主窗口、截图窗口、预览窗口。antd和ant-design/icons 选用 Ant Design 组件库让我们能快速搭建出美观的界面。当然你也可以用其他 UI 库或者自己写样式。4.2 配置多窗口路由架构我们的应用有三个主要界面主界面、全屏截图界面、图片预览界面。在传统的 Electron 应用中可能会为每个窗口创建单独的 HTML 文件。但这里我们采用单页面应用SPA的思路利用 React Router 在一个 Electron 窗口内根据哈希Hash路由来切换不同视图。这样做的好处是代码结构清晰资源共享方便。关键点必须使用 Hash 模式。因为在打包后的 Electron 应用中文件协议file://下BrowserRouter 的历史模式会遇到路径问题而 Hash 模式兼容性最好。首先在src/renderer/App.tsx中配置路由import { HashRouter as Router, Routes, Route } from react-router-dom; import Home from ./pages/Home; import ShotScreen from ./pages/ShotScreen; import ViewImage from ./pages/ViewImage; import ./App.css; function App() { return ( Router Routes Route path/ element{Home /} / Route path/shotScreen element{ShotScreen /} / Route path/viewImage element{ViewImage /} / /Routes /Router ); } export default App;然后我们需要修改 Electron 主进程的创建窗口逻辑让不同的窗口加载不同的路由。4.3 主进程窗口管理与通信枢纽主进程src/main/main.js或index.js是应用的大脑负责创建窗口和处理核心逻辑。1. 创建主窗口主窗口是我们工具的“控制中心”通常比较小可以常驻在桌面角落。// main.js import { app, BrowserWindow, ipcMain } from electron; import path from path; let mainWindow; function createMainWindow() { mainWindow new BrowserWindow({ width: 400, height: 300, frame: true, // 主窗口可以有边框 resizable: false, webPreferences: { preload: path.join(__dirname, preload.js), // 注意在最新版 Electron 中出于安全考虑默认禁用 nodeIntegration // 我们需要通过 preload 脚本暴露有限的 API 给渲染进程 contextIsolation: true, nodeIntegration: false, }, }); // 开发环境加载本地服务器生产环境加载文件 if (process.env.VITE_DEV_SERVER_URL) { mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL); mainWindow.webContents.openDevTools(); // 打开开发者工具 } else { mainWindow.loadFile(path.join(__dirname, ../renderer/index.html)); } } app.whenReady().then(() { createMainWindow(); });2. 创建全屏截图窗口这是实现截图功能的核心窗口配置参数非常关键。// main.js 中继续添加 let shotScreenWindow null; function createShotScreenWindow() { const { screen } require(electron); const primaryDisplay screen.getPrimaryDisplay(); const { width, height } primaryDisplay.workAreaSize; // 获取工作区大小排除任务栏 shotScreenWindow new BrowserWindow({ width, height, x: 0, y: 0, frame: false, // 无边框 transparent: true, // 透明背景 resizable: false, movable: false, fullscreenable: true, skipTaskbar: true, // 不在任务栏显示 alwaysOnTop: true, // 置于顶层 webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false, }, }); // 加载截图页并附带 hash 路由 if (process.env.VITE_DEV_SERVER_URL) { shotScreenWindow.loadURL(${process.env.VITE_DEV_SERVER_URL}#/shotScreen); } else { shotScreenWindow.loadFile(path.join(__dirname, ../renderer/index.html), { hash: shotScreen, }); } // 窗口显示后最大化并进入简单全屏模式确保覆盖整个屏幕 shotScreenWindow.once(ready-to-show, () { shotScreenWindow.maximize(); shotScreenWindow.setSimpleFullScreen(true); // macOS 上更好的全屏体验 }); } // 对应的打开和关闭函数 function openShotScreenWindow() { if (!shotScreenWindow || shotScreenWindow.isDestroyed()) { shotScreenWindow createShotScreenWindow(); } // 先隐藏主窗口避免干扰 if (mainWindow !mainWindow.isDestroyed()) mainWindow.hide(); shotScreenWindow.show(); } function closeShotScreenWindow() { if (shotScreenWindow !shotScreenWindow.isDestroyed()) { shotScreenWindow.close(); } shotScreenWindow null; // 截图结束后显示主窗口 if (mainWindow !mainWindow.isDestroyed()) mainWindow.show(); }3. 进程间通信IPC注册在主进程中我们需要监听和处理来自渲染进程的请求。// main.js ipcMain.on(ss:open-win, () { openShotScreenWindow(); }); ipcMain.on(ss:close-win, () { closeShotScreenWindow(); }); ipcMain.handle(ss:get-shot-screen-img, async () { // 这里调用前面提到的捕获屏幕的函数 return await captureFullScreen(); }); ipcMain.on(ss:save-img, async (event, imageDataURL) { // imageDataURL 是渲染进程传来的图片数据 // 1. 可以保存到文件 const { dialog } require(electron); const { writeFile } require(fs/promises); const path require(path); const { filePath } await dialog.showSaveDialog({ defaultPath: path.join(app.getPath(pictures), screenshot-${Date.now()}.png), filters: [{ name: PNG Image, extensions: [png] }], }); if (filePath) { // 将 Data URL 中的 base64 数据提取并写入文件 const base64Data imageDataURL.replace(/^data:image\/\w;base64,/, ); const buffer Buffer.from(base64Data, base64); await writeFile(filePath, buffer); } // 2. 也可以写入剪贴板 const { clipboard, nativeImage } require(electron); const image nativeImage.createFromDataURL(imageDataURL); clipboard.writeImage(image); // 3. 关闭截图窗口打开预览窗口如果需要 closeShotScreenWindow(); openPreviewWindow(imageDataURL); });4.4 渲染进程构建用户交互界面渲染进程就是我们用 React 写的页面部分。主界面Home这里很简单就是一个触发截图的按钮。// src/renderer/pages/Home.jsx import { Button } from antd; import { ScissorOutlined } from ant-design/icons; import { ipcRenderer } from electron; // 注意需要通过 preload 脚本暴露 const Home () { const handleScreenshot () { // 发送事件给主进程请求打开截图窗口 ipcRenderer.send(ss:open-win); }; return ( div style{{ padding: 20px, textAlign: center }} h1我的高效截图工具/h1 Button typeprimary icon{ScissorOutlined /} sizelarge onClick{handleScreenshot} 一键截图 (CtrlShiftS) /Button p style{{ marginTop: 20px, color: #666 }} 点击按钮或使用快捷键开始截取屏幕区域 /p /div ); }; export default Home;截图界面ShotScreen这个页面负责展示“定格”的屏幕和提供裁剪交互。为了不重复造轮子我强烈推荐使用成熟的 React 截图组件库比如react-screenshots或js-web-screen-shot。它们已经实现了矩形选择、拖拽、调整大小、标注工具矩形、箭头、文字、马赛克等全套功能我们只需要集成即可。// src/renderer/pages/ShotScreen.jsx import { useEffect, useState, useCallback } from react; import Screenshots from react-screenshots; import { ipcRenderer } from electron; import react-screenshots/lib/style.css; // 引入组件样式 const ShotScreen () { const [screenImage, setScreenImage] useState(); useEffect(() { // 页面加载后立即获取屏幕图像 const getImage async () { const img await ipcRenderer.invoke(ss:get-shot-screen-img); setScreenImage(img); }; getImage(); }, []); const onOk useCallback((blob, bounds) { // 用户点击“确认” const imageUrl URL.createObjectURL(blob); ipcRenderer.send(ss:save-img, imageUrl); // 释放创建的 URL 对象避免内存泄漏 URL.revokeObjectURL(imageUrl); }, []); const onCancel useCallback(() { // 用户点击“取消”或按 ESC ipcRenderer.send(ss:close-win); }, []); // 如果图片还没加载好显示加载状态 if (!screenImage) { return div style{{ width: 100vw, height: 100vh, background: #000 }}加载中.../div; } return ( Screenshots url{screenImage} // 传入全屏图片 width{window.innerWidth} height{window.innerHeight} onOk{onOk} onCancel{onCancel} // 可以传递更多配置如语言、工具栏配置等 lang{{ operation_ok: 确认, operation_cancel: 取消, operation_undo: 撤销, // ... 其他语言项 }} / ); }; export default ShotScreen;4.5 安全与预加载脚本Preload在最新的 Electron 版本中出于安全考虑默认禁用了渲染进程的nodeIntegration并启用了contextIsolation上下文隔离。这意味着渲染进程的window对象和主进程是隔离的你不能直接在渲染进程里require(electron)。正确的做法是通过预加载脚本Preload Script向渲染进程暴露有限的、安全的 API。// src/main/preload.js const { contextBridge, ipcRenderer } require(electron); // 暴露一个安全的 API 给渲染进程 contextBridge.exposeInMainWorld(electronAPI, { // 截图相关 openShotScreenWindow: () ipcRenderer.send(ss:open-win), closeShotScreenWindow: () ipcRenderer.send(ss:close-win), getScreenImage: () ipcRenderer.invoke(ss:get-shot-screen-img), saveImage: (imageDataURL) ipcRenderer.send(ss:save-img, imageDataURL), // 可以暴露更多安全的 API... });然后在渲染进程中我们通过window.electronAPI来调用这些方法// 在 React 组件中 const handleScreenshot () { window.electronAPI.openShotScreenWindow(); }; const getImage async () { const img await window.electronAPI.getScreenImage(); setScreenImage(img); };5. 功能增强与优化实践基础功能跑通后我们可以让它变得更强大、更好用。这里分享几个我实际添加的功能和优化点。5.1 全局快捷键与托盘图标一个专业的桌面工具应该支持全局快捷键和后台运行。我们可以让用户在不打开主窗口的情况下通过快捷键如CtrlShiftS直接触发截图。注册全局快捷键在主进程中应用 ready 后注册。// main.js const { globalShortcut } require(electron); app.whenReady().then(() { createMainWindow(); // 注册快捷键 const ret globalShortcut.register(CommandOrControlShiftS, () { openShotScreenWindow(); }); if (!ret) { console.log(快捷键注册失败); } // 应用退出时注销所有快捷键 app.on(will-quit, () { globalShortcut.unregisterAll(); }); });添加系统托盘图标托盘图标可以让应用最小化到后台而不是直接关闭。// main.js const { Tray, Menu } require(electron); const path require(path); let tray null; function createTray() { const iconPath path.join(__dirname, assets, icon.png); // 准备一个图标 tray new Tray(iconPath); const contextMenu Menu.buildFromTemplate([ { label: 截图, click: () openShotScreenWindow() }, { label: 显示主窗口, click: () mainWindow.show() }, { type: separator }, { label: 退出, click: () app.quit() }, ]); tray.setToolTip(我的截图工具); tray.setContextMenu(contextMenu); // 点击托盘图标也可以显示/隐藏主窗口 tray.on(click, () { if (mainWindow.isVisible()) { mainWindow.hide(); } else { mainWindow.show(); } }); } // 在 app.whenReady() 中调用 createTray()5.2 截图后的工作流预览、编辑与分享截图完成不是终点如何高效处理截图同样重要。即时预览窗口截图确认后可以立即弹出一个预览窗口展示刚刚截取的图片并提供简单的二次编辑如再次裁剪、添加标注和分享选项复制到剪贴板、保存到指定文件夹、分享到其他应用。历史记录与管理在主窗口中增加一个历史记录面板以缩略图网格的形式展示最近截取的图片。点击可以放大预览右键提供删除、重命名、打开所在文件夹等操作。数据可以存储在本地 IndexedDB 或直接以文件形式管理。与剪贴板深度集成除了自动复制到剪贴板还可以监听剪贴板变化如果发现是图片询问用户是否要保存到历史记录中。这样也能管理从其他工具复制过来的图片。自定义保存路径与命名规则在设置中允许用户自定义截图保存的默认文件夹以及文件命名规则例如截图-{YYYY-MM-DD}-{HHmmss}.png让文件管理更加自动化。5.3 性能优化与打包部署开发体验优化利用 Vite 的 Hot Module Replacement (HMR)我们在修改渲染进程的 React 组件时页面会即时更新无需重启整个 Electron 应用。但对于主进程代码的修改通常需要重启应用。可以使用nodemon等工具监听主进程文件变化并自动重启。打包配置使用electron-vite提供的打包命令非常简单。# 构建渲染进程和主进程代码 pnpm run build # 打包成可执行文件 pnpm run electron:build在electron.vite.config.ts中我们可以详细配置打包行为比如设置应用图标、版权信息、打包目标格式dmg、exe、deb等、是否压缩、是否生成安装程序等。减小应用体积Electron 应用体积一直是个痛点。我们可以通过以下方式优化使用electron-builder的asar打包保护源码并略微减小体积。仔细检查package.json中的依赖将只在开发中使用的库如types开头的类型定义移到devDependencies。考虑使用electron-updater实现自动更新这样用户第一次下载后后续更新只需要下载增量包。6. 避坑指南我踩过的那些“坑”在开发过程中我遇到了不少问题这里列出来希望能帮你节省时间。1. 透明窗口的点击穿透问题全屏透明截图窗口创建后理论上它应该能拦截所有鼠标事件。但有时你会发现鼠标点击“穿透”了你的窗口激活了底下的其他应用。这通常是因为窗口没有获得焦点或者某些平台上的特殊行为。解决方案是确保窗口创建后调用focus()方法并设置alwaysOnTop: true。如果还不行可以尝试设置ignoreMouseEvents为false默认就是 false表示不忽略。2. 多显示器适配desktopCapturer.getSources()会返回所有显示器的源。如果你的用户有多个显示器你需要决定是截取所有屏幕拼接成一张大图还是让用户选择某一个屏幕。更专业的做法是先通过screen.getAllDisplays()获取所有显示器信息然后为每个显示器创建一个全屏透明窗口或者提供一个界面让用户选择要截取的显示器。3. 高DPIRetina屏幕下的模糊问题在高分辨率屏幕上如果不做处理截取的图片可能会模糊。这是因为desktopCapturer捕获的thumbnail尺寸可能和屏幕的逻辑像素CSS像素不一致。我们需要根据设备的devicePixelRatio来调整捕获的尺寸。例如如果devicePixelRatio是 2那么thumbnailSize的宽高应该是屏幕逻辑宽高的 2 倍这样才能获得清晰的图片。4. 安全策略与 CSP在加载本地图片或 Data URL 时可能会遇到 Content Security Policy (CSP) 错误。你需要在 HTML 的meta标签或 HTTP 头中适当放宽 CSP 策略例如允许data:和blob:协议。在electron-vite模板中通常已经配置好了开发环境的 CSP。5. 进程间通信的数据量限制当截取高分辨率屏幕时生成的 Data URL 字符串会非常长可能几 MB 甚至十几 MB。虽然 IPC 通信通常能处理但过大的数据频繁传递可能影响性能。对于非常大的图片可以考虑在主进程中将图片先保存为临时文件然后将文件路径传递给渲染进程或者使用shared memory等更高效的方式。6. 内存管理全屏图片和 Canvas 操作比较消耗内存。在截图操作完成后要及时释放资源。例如使用URL.revokeObjectURL()释放通过createObjectURL创建的 Blob URL在组件卸载时清理 Canvas 上下文等。走到这一步一个功能完整、体验流畅的个人专属截图工具就已经诞生了。从被多窗口切换困扰到亲手打造出提升效率的利器这个过程充满了挑战和成就感。技术本身不是目的解决真实问题、优化工作流才是。这个项目里用到的 Vite 构建、Electron 多窗口通信、Canvas 图像处理等知识完全可以迁移到其他桌面应用创意中比如录屏工具、桌面便签、自动化脚本助手等等。最重要的是你开始用开发者的思维去解决日常问题了这才是最大的收获。我的项目代码已经放在 GitHub 上如果你在实现过程中遇到任何问题或者有更好的想法欢迎一起交流探讨。