前一阵有个做服务端开发的朋友问了我一个挺有意思的问题:他从 GitHub 拉了一个基于 Electron 的桌面工具源码,想改点行为,结果打开项目一看,里面全是 src/main、src/renderer、dist、public、package.json 之类的文件,跟他平时开发的 Web 项目不能说很像,只能说一模一样。他就很困惑:做桌面应用,目录怎么长得和 Web 项目一个样?
这事解释起来并不复杂,但背后牵扯到桌面开发十几年来的习惯变化。如果你以前写过 MFC、Qt、WinForms 这类传统桌面程序,刚接触现在的跨平台桌面框架时,多半也会有这种“走错片场”的感觉。反过来,如果你本来做 Web 前端,第一次打开现代桌面项目反倒会觉得亲切。这篇文章想把这种“错位”讲清楚:现代应用为什么要长成这种目录结构,以及当你自己准备新建或改造一个桌面项目时,这套结构到底要怎么拆。
1. 从“一个进程干到底”到“按进程分目录”:桌面项目组织方式变了
1.1 先给结论:目录结构本质是运行模型的投影
很多人第一次看到现代桌面项目的目录,会下意识以为这是“前端工程化”塞进了桌面开发。这个判断方向是对的,但不够准确。
更准确的说法是:目录结构是运行时模型的投影。项目里有哪些独立的运行单元,源码目录就会跟着拆出对应的顶层文件夹。
Electron 这类框架跑起来有两个完全独立的运行时:主进程和渲染进程。主进程就是那个掌管窗口、菜单、系统通知、文件读写的宿主进程;渲染进程则是承载页面的浏览器内核实例。因为两个进程的生命周期、权限、可调用的 API 完全不同,所以源码必须从一开始就分开管理。于是就有了 src/main 和 src/renderer 这种在传统桌面项目里几乎见不到的顶层目录。
传统桌面框架则不太一样。Qt 程序写完后就是一个进程,内部虽然有主线程、工作线程,但共享同一份内存、同一套 C++/QML 运行时。因为它不存在进程级隔离,目录结构自然也不会按“进程”来拆,而是按“模块”或“层次”拆:UI 层归 UI 层,业务层归业务层,数据层归数据层。
所以你会发现一个有意思的现象:老派桌面项目目录里经常出现的是 ui/、controller/、service/、model/ 这类“逻辑分层”,而新派桌面项目目录里常见的是 main/、renderer/、preload/ 这类“运行时分区”。差别背后不是谁比谁高级,而是底层运行模型变了。
1.2 我用 Qt 顺手写画线工具时是怎么组织代码的
拿我自己早年的经历类比一下。那时候我经常拿 Qt 写一些小工具,比如一个能加载图纸、支持鼠标拖动画线的桌面端小程序。
最开始的版本非常简单,十几个类全塞在一个平铺目录里,找起来靠 IDE 的文件树。后来功能多了,就自然拆成了这样:
code复制paint-tool/
├── ui/
│ ├── mainwindow.ui
│ └── drawcanvas.ui
├── controller/
│ └── drawcontroller.cpp
├── service/
│ ├── shapefactory.cpp
│ └── renderengine.cpp
├── model/
│ ├── shape.h
│ └── document.h
└── resources/
└── app.rc
这套结构在 Qt 项目里很典型:按职责切分,同一个模块的数据、界面、控制代码各归各位。它运行在同一个进程里,目录分层更多是给“人”看的,帮助开发者快速定位一段逻辑在哪个层。进程内并没有真正隔离访问权限,UI 层想要直接 new 一个 Model 对象,技术上完全可行,规范层面才会约束你不要这么干。
现代桌面框架走的是另一条路:主进程目录里的代码,根本不会和渲染进程共享同一个 JS 上下文。你在 main/ 里写错了代码,崩溃的只是宿主进程;你在 renderer/ 里写错了代码,白屏的只是页面区域。这种物理边界直接反映到目录上,所以目录结构天然比传统 Qt 项目更强调“分区”而不是“分层”。
1.3 为什么说“跨平台”是目录结构变化的导火索
还有一个角度很容易被忽略:真正的导火索是跨平台。
以前写 Windows 桌面软件,需求就是跑在 Windows 上,你只需要编译出一个 exe,跟系统 API、注册表、服务打交道都是单平台的。Qt 虽然跨平台,但项目里仍然要维护大量 #ifdef 和不同平台的资源文件,打包脚本也各异。相比之下,Electron、Tauri 这类方案把“跨平台能力”收敛到框架层,应用本身用 HTML/CSS/JS 或 Rust + WebView 书写,源码层面几乎不需要按平台拆目录。
平台差异被抹平之后,剩下的核心差异就只剩进程了。目录结构自然就跟着进程模型走,而不是平台走。理解了这一层,后面再看到 src/main、src/renderer 就不会觉得奇怪了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主进程、预加载脚本和渲染进程:目录里三条主线的分工逻辑
2.1 src/main 的职责不是写 UI,而是管“窗”和“命”
现代桌面项目的 src/main 目录,很多人都以为它是“主入口”,只是放一个 index.js 把窗口创建出来。实际项目里远远不止这么简单。
主进程承担的是所有需要操作系统能力的逻辑,包括窗口生命周期、系统托盘、全局快捷键、菜单、文件对话框、自动更新、原生模块调用、数据库本地落盘等。它本质上像是“桌面端的后端”,只不过这个后端不是跑在远程服务器上,而是跑在用户电脑的本地宿主里。
所以你看成熟的开源项目,src/main 里边一般还会有更细的子目录:
code复制sr
