为什么 Web 应用通常采用 client/backend 分离架构,而传统软件可以集成两者?
传统桌面软件把 GUI、网络通信和本地数据处理放在同一个安装包中,是常见且仍然适用的设计。Web 应用常把部分功能放到服务器,主要考虑浏览器权限、数据归属和信任边界。并没有“网络连接和业务逻辑必须全部属于 backend”的通用规则。
对于 Next.js + Electron 的 XMPP 客户端,可以先让 renderer 负责界面,主进程中的服务模块负责原生网络连接和本地存储,XMPP 服务器负责账号验证与消息路由。有额外的服务端需求时,再部署自己的 backend。
先区分逻辑分层、进程分离和远程服务
| 概念 | 解决的问题 | 示例 |
|---|---|---|
| 逻辑分层 | 代码如何组织、职责如何划分 | GUI 调用独立的消息服务模块 |
| 进程分离 | 如何隔离权限、故障和运行负载 | Electron renderer 通过 IPC 调用主进程 |
| 客户端与远程服务分离 | 哪一方拥有数据和最终决定权 | 聊天客户端连接 XMPP 服务器 |
一个 Java 程序可以在同一进程里运行 Swing 界面和网络通信模块,也可以把耗时任务交给工作线程。这些安排都允许代码保持清楚的逻辑分层。
一个 Electron 安装包通常包含多个进程。主进程运行在 Node.js 环境中,可以使用原生网络和文件系统能力;renderer 主要负责网页界面。这种进程划分不要求另外部署服务器。Electron 进程模型
聊天客户端连接 XMPP 服务器,本身就是 client/server 架构。客户端负责协议解析、重连和消息发送,这些都属于客户端的职责,并不意味着它也承担了服务器的职责。
为什么浏览器里的 Web 应用常常需要 backend
浏览器限制网页访问本机和网络的能力
普通网页可以在用户打开链接后立即执行,因此浏览器会限制它访问文件、操作系统和底层网络的能力。Java、C# 等桌面程序则在安装、启动后,按照操作系统授予的权限运行。
普通网页没有通用的原始 TCP socket API。它可以使用 fetch、WebSocket 等接口,但各有协议约束。例如,WebSocket 要求对端支持 WebSocket 握手和消息格式,不能直接连接只接受原生 XMPP TCP 流的端口。RFC 7395
这些限制决定了网页能做什么,不能据此推导出“所有业务处理都应放到 backend”。
服务器需要独立验证客户端的操作
客户端运行在用户控制的设备上。用户可以修改 JavaScript、替换桌面程序,或者绕过界面直接构造请求,因此服务器必须自行检查用户身份和操作权限。
例如:
- 客户端可以检查消息是否为空,及时提示用户。
- 服务器仍须判断该账号能否向目标发送消息。
- 客户端可以显示群管理员按钮。
- 群服务仍须验证调用者是否具有管理员权限。
把代码放在服务器上,可以避免将这部分实现分发给用户,但系统安全不能依赖实现细节保密。Java、C# 或 Electron 程序中的内置凭据,即使经过编译、打包或混淆,也不能视为秘密。
共享数据和持续运行的任务需要统一管理
如果数据属于所有用户,或者任务需要在客户端退出后继续执行,通常交给服务器更合适,例如账号管理、跨用户消息路由和需要持续运行的自动化任务。
数据库连接也要区分用途:
- 本地 SQLite 数据库可以用于缓存聊天记录,由桌面客户端管理。
- 共享业务数据库通常应由服务端访问,再通过受控接口提供功能。
- 为用户提供的数据库管理工具可以直接连接数据库,前提是用户使用自己的凭据,并受数据库权限约束。
客户端能否连接数据库,与是否应该持有整套业务数据库的访问权限,需要分别考虑。
XMPP 客户端可以怎样划分职责
方案一:通过 WebSocket 直接连接 XMPP 服务
如果目标 XMPP 服务提供可用的 WebSocket 端点,浏览器版客户端就可以用支持该传输方式的 XMPP 库连接它,并使用 wss:// 保护传输。RFC 7395 定义了 XMPP over WebSocket;浏览器还可以使用基于 HTTP 的 BOSH 传输方式。RFC 7395、XEP-0206
这种情况下,客户端可以负责:
- 联系人、会话和消息界面;
- XMPP 客户端侧协议处理;
- 登录流程中客户端需要完成的协商;
- 输入检查、消息格式化和界面状态;
- 本地缓存。
XMPP 服务器负责验证凭据、执行权限检查和路由消息。客户端参与认证流程是正常行为,因此“身份验证必须全部放在自己的 backend”并不准确。
采用这种方案前,需要确认目标服务是否启用相应端点,以及来源检查、TLS 配置和客户端库是否兼容。服务器支持 XMPP TCP,并不足以说明浏览器可以直连。
方案二:由 Electron 本地模块管理连接
如果需要连接只提供 TCP 的 XMPP 服务,或者希望统一管理本地数据库、系统凭据和长连接,可以把这些能力放在 Electron 主进程管理的模块中,再通过 IPC 提供给界面。
| 职责 | 建议位置 |
|---|---|
| React 界面、输入反馈、列表渲染 | renderer |
| 文本格式化、与平台无关的纯计算 | 普通共享模块,按调用需要使用 |
| TCP/TLS 连接、XMPP 会话和重连 | 主进程中的服务模块,必要时迁往独立进程 |
| 本地数据库、文件和系统凭据访问 | 受控的本地服务模块 |
| 账号验证、共享状态和访问控制 | XMPP 服务器或相应远程服务 |
| 必须保密的服务端密钥 | 自己控制的远程服务 |
Electron 支持通过 preload、contextBridge 和 IPC 向 renderer 暴露有限的本地能力,无须为本机进程间调用再搭建 REST 服务。Electron IPC 文档
主进程同时负责窗口和应用生命周期管理。异步网络 I/O 可以先放在这里;如果协议处理、数据库操作中有明显的同步阻塞或大量计算,再考虑工作线程或 utility process。Electron 进程模型
一个受控 IPC 接口的代码示例
假设主进程已建立 XMPP 会话,xmppSession.sendChatMessage(to, body) 是应用自行封装的方法,负责验证目标 JID、使用协议库构造消息并提交发送。它不是 Electron 自带的 API。
创建窗口时,限制 renderer 的权限:
// 主进程片段
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
},
});
在 preload 中只暴露具体操作:
// preload.cjs
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('chat', {
sendMessage: (to, body) =>
ipcRenderer.invoke('chat:send-message', { to, body }),
});
主进程收到请求后,检查调用来源和参数:
// 主进程片段
// 假设应用已注册 app:// 协议,并从 app://chat 加载受信任的界面。
ipcMain.handle('chat:send-message', async (event, payload) => {
const frame = event.senderFrame;
if (
event.sender !== win.webContents ||
!frame ||
frame !== win.webContents.mainFrame
) {
throw new Error('Untrusted sender');
}
const url = new URL(frame.url);
if (url.protocol !== 'app:' || url.host !== 'chat') {
throw new Error('Untrusted page');
}
if (
!payload ||
typeof payload.to !== 'string' ||
payload.to.trim() === '' ||
typeof payload.body !== 'string' ||
payload.body.trim() === ''
) {
throw new Error('Invalid message');
}
await xmppSession.sendChatMessage(payload.to, payload.body);
return { submitted: true };
});
界面代码只需调用:
// renderer 中的事件处理函数
await window.chat.sendMessage('friend@example.com', '你好');
submitted 只表示应用已提交发送,不代表对方收到或已读。完整实现还需要处理连接状态、错误、消息大小限制,以及协议支持的确认机制。
实际应用还应限制页面导航和新窗口,避免不受信任的页面进入具有 IPC 能力的窗口。不要向 renderer 暴露整个 ipcRenderer,也不要提供任意执行 SQL、任意读文件或任意打开 socket 的通用接口。Electron 安全建议
Next.js 是否要求保留一个 backend
Next.js 支持服务端能力,但用它构建界面,不代表桌面应用运行时必须启动 Next.js 服务。
如果界面符合静态导出的限制,可以使用:
// next.config.js
module.exports = {
output: 'export',
};
构建得到的静态资源可以随 Electron 一起打包。静态导出中的 Server Components 可以在构建时执行;需要在请求时动态处理的服务端功能,则无法仅靠这些静态文件运行。Next.js 静态导出文档
需要分别决定:
- 界面是否需要 Next.js 的运行时服务端能力;
- 桌面端是否需要原生 TCP、文件或数据库访问;
- 是否存在必须由自己控制的远程服务完成的功能。
这三个决定可以独立作出。在用户电脑上启动 Next.js 服务,也不能让其中的代码和密钥获得远程服务器那样的保密性。
实际落地时,先做哪些决定
先确认目标 XMPP 服务支持哪些传输方式。如果 WebSocket 已能满足需求,浏览器版和 Electron 版可以共享更多连接逻辑;如果必须使用 TCP,就为桌面端提供本地连接适配器。
接着确定界面与消息服务之间的接口,例如 connect、sendMessage、disconnect 和消息事件。把协议处理集中在服务模块中,让界面通过接口调用,后续更换传输方式或将模块移到其他进程时,就不必大幅改动界面。
是否新增远程 backend,要看具体需求:是否需要保管第三方服务密钥、提供自有共享数据,或者在客户端退出后继续运行任务。如果都不需要,现有 XMPP 服务器加上本地客户端模块就可能已经足够。
进程间调用有序列化和调度开销,新增远程代理还会带来网络延迟、部署和故障处理成本。应根据权限、数据和运行需求划分职责,把功能放在同一个安装包里仍然是可行的设计。
备注:内容仅供参考。
