PAYATHON 2026

为什么 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 静态导出文档

需要分别决定:

  1. 界面是否需要 Next.js 的运行时服务端能力;
  2. 桌面端是否需要原生 TCP、文件或数据库访问;
  3. 是否存在必须由自己控制的远程服务完成的功能。

这三个决定可以独立作出。在用户电脑上启动 Next.js 服务,也不能让其中的代码和密钥获得远程服务器那样的保密性。

实际落地时,先做哪些决定

先确认目标 XMPP 服务支持哪些传输方式。如果 WebSocket 已能满足需求,浏览器版和 Electron 版可以共享更多连接逻辑;如果必须使用 TCP,就为桌面端提供本地连接适配器。

接着确定界面与消息服务之间的接口,例如 connect、sendMessage、disconnect 和消息事件。把协议处理集中在服务模块中,让界面通过接口调用,后续更换传输方式或将模块移到其他进程时,就不必大幅改动界面。

是否新增远程 backend,要看具体需求:是否需要保管第三方服务密钥、提供自有共享数据,或者在客户端退出后继续运行任务。如果都不需要,现有 XMPP 服务器加上本地客户端模块就可能已经足够。

进程间调用有序列化和调度开销,新增远程代理还会带来网络延迟、部署和故障处理成本。应根据权限、数据和运行需求划分职责,把功能放在同一个安装包里仍然是可行的设计。

备注:内容仅供参考。