Leptos 多服务器多客户端架构实战:基于 Nginx 反向代理实现单域名下多 SSR 应用与共享服务通信
2026/9/13 17:38:45 网站建设 项目流程

Leptos 多服务器多客户端架构实战:基于 Nginx 反向代理实现单域名下多 SSR 应用与共享服务通信

【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos

本指南以仓库中的 nginx-mpmc 项目为蓝本,完整讲解如何用 Nginx 作为反向代理,让多个 Leptos SSR 应用(App-1、App-2)与多个共享服务器(Shared-Server-1、Shared-Server-2)在同一个域名localhost:80下协同工作。读完本文,你将掌握 Nginx 多upstream路由拆分、cargo leptos serve多实例启动、服务器函数(server functions)跨服务器调用,以及create_resourcecreate_local_resource在 SSR 多服务器场景下的关键差异。

一、场景概述:为什么要做多服务器多客户端

在实际部署中,一个业务系统往往由多个独立服务组成:前端 SSR 渲染层、多个后端 API 服务、共享的业务逻辑服务器等。如果每个服务都暴露独立端口和域名,会导致 CORS 配置复杂、域名管理混乱、客户端跨域请求频繁出错。

nginx-mpmc 示例解决的核心问题就是:在只有一个域名(localhost:80)的前提下,让多个客户端与多个服务器互相通信,由 Nginx 统一入口做路径转发。整体拓扑如下:

组件角色监听端口访问路径
Nginx(Docker 容器)反向代理统一入口80localhost(默认)
App-1Leptos SSR 应用3000localhost(默认,/
App-2Leptos SSR 应用3001localhost/app2
Shared-Server-1服务器函数服务3002localhost/api_shared
Shared-Server-2服务器函数服务3003localhost/api_shared2

两个共享服务器不承担页面渲染,仅注册并响应 Leptos 服务器函数,供两个 App 通过 action 或本地资源(local resource)调用。

二、项目结构与四个服务

nginx-mpmc 示例位于 projects/nginx-mpmc,目录结构如下:

projects/nginx-mpmc/ ├── app-1/ # Leptos SSR 应用 1(cargo leptos serve) ├── app-2/ # Leptos SSR 应用 2(cargo leptos serve) ├── shared-server-1/ # 纯服务器函数服务 1(cargo run) ├── shared-server-2/ # 纯服务器函数服务 2(cargo run) ├── nginx.conf # macOS/Docker Desktop 使用的 Nginx 配置 ├── nginx_linux.conf # Linux 下使用 host 网络模式时使用的配置 ├── run.sh # 一键启动脚本(macOS/Docker Desktop) ├── run_linux.sh # 一键启动脚本(Linux) └── kill.sh # 一键清理脚本

2.1 两个 SSR 客户端应用(App-1 与 App-2)

App-1 与 App-2 都是标准 Leptos SSR 应用,由cargo leptos serve启动。它们的差异体现在:

  • 端口不同:App-1 监听127.0.0.1:3000,App-2 监听127.0.0.1:3001(见 app-1/Cargo.toml 与 app-2/Cargo.toml 中的site-addr配置)。
  • 静态资源目录不同:这是本示例的关键改动。App-2 在 Cargo.toml 中将site-pkg-dir从默认的pkg改为pkg2,以避免与 App-1 的 WASM/JS/CSS 产物在同一域名下冲突;对应的,App-2 页面中的样式引用路径为/pkg2/app-2.css(见 app-2/src/app.rs),而 App-1 使用/pkg/app-1.css
  • 路由挂载路径不同:App-2 的<Route path="app2">使其页面挂在/app2路径下(见 app-2/src/app.rs)。

两个 App 都通过 Cargo 依赖以本地路径引入两个共享服务器 crate:

shared-server = { path = "../shared-server", default-features = false } shared-server-2 = { path = "../shared-server-2", default-features = false }

并且在ssrhydrate两个 feature 中分别开启共享服务器的对应 feature,确保服务器函数在服务端注册、在客户端以可调用形式编译。

2.2 两个纯服务器函数服务(Shared-Server-1 与 Shared-Server-2)

两个共享服务器是「只有服务器、没有客户端配对」的 Leptos 服务:它们不输出 WASM,也不做 hydration,只通过leptos_axum::handle_server_fns暴露服务器函数端点。

以 Shared-Server-1 为例,服务器函数定义在 shared-server-1/src/lib.rs:

use leptos::*; #[cfg(feature = "ssr")] #[derive(Clone)] pub struct SharedServerState; #[tracing::instrument] #[server(prefix = "/api_shared", endpoint = "/a")] pub async fn shared_server_function() -> Result<String, ServerFnError> { tracing::debug!("SHARED SERVER 1"); let _: axum::Extension<SharedServerState> = leptos_axum::extract().await?; Ok("This message is from the shared server.".to_string()) }

这里有两个要点:

  1. #[server(prefix = "/api_shared", endpoint = "/a")]显式指定了服务器函数的路由前缀为/api_shared,端点路径为/a。这正好与 Nginx 中location /api_shared的转发规则对应。Shared-Server-2 则使用prefix = "/api_shared2",对应 Nginx 的location /api_shared2
  2. 服务器函数通过leptos_axum::extract()取出axum::Extension<SharedServerState>状态,说明它可以依赖由共享服务器主程序注入的 axum 扩展状态(实际业务中可替换为数据库连接池、会话状态等)。

主程序在 shared-server-1/src/main.rs 中注册路由:

let app = Router::new() .route("/api_shared/*fn_name", post(leptos_axum::handle_server_fns)) .layer(tower_http::trace::TraceLayer::new_for_http()) .layer(axum::Extension(shared_server::SharedServerState));

注意route("/api_shared/*fn_name", ...)使用通配符*fn_name捕获任意服务器函数名,统一交给leptos_axum::handle_server_fns处理。Shared-Server-2 与之对称,监听127.0.0.1:3003,注册/api_shared2/*fn_name

由于这两个服务没有客户端配对,main函数在非ssrfeature 下为空实现(注释明确说明:No hydrate function on a server function only server),因此只通过cargo run --features ssr(对应cargo run前的 feature 配置)运行。

三、Nginx 反向代理配置详解

Nginx 是整套架构的统一入口。仓库提供了两份几乎一致的配置,区别仅在于 upstream 地址的写法:

  • nginx.conf:适用于 macOS / Docker Desktop,upstream 指向host.docker.internal:端口(从容器内访问宿主机端口)。
  • nginx_linux.conf:适用于 Linux,配合--network="host"网络模式,upstream 直接指向127.0.0.1:端口

3.1 upstream 定义(端口别名)

upstream app_server { server host.docker.internal:3000; } upstream app_2_server { server host.docker.internal:3001; } upstream shared_server { server host.docker.internal:3002; } upstream shared_server_2 { server host.docker.internal:3003; }

四个 upstream 分别代表四个后端服务,Nginx 通过路径匹配将请求转发到对应 upstream。Linux 版本将host.docker.internal全部替换为127.0.0.1

3.2 location 路由规则(核心)

server { listen 80; # /app2 提供 app2 的客户端页面,任何客户端都可以通过 /app2/api 调用其 API location /app2 { proxy_pass http://app_2_server; } # 需要为 app2 配置独立的 pkg 目录,并在此转发 location /pkg2 { proxy_pass http://app_2_server; } # /api_shared 调用 shared_server 上注册的服务器函数 location /api_shared { proxy_pass http://shared_server; } # /api_shared_2 调用 shared_server_2 上注册的服务器函数 location /api_shared2 { proxy_pass http://shared_server_2; } # 默认提供 app-1 的客户端 location / { proxy_pass http://app_server; } }

各 location 的作用总结:

location转发目标用途
/app2app_2_server(3001)App-2 的页面与路由请求
/pkg2app_2_server(3001)App-2 的 WASM/JS/CSS 静态资源(对应其site-pkg-dir = "pkg2"
/api_sharedshared_server(3002)Shared-Server-1 的服务器函数调用
/api_shared2shared_server_2(3003)Shared-Server-2 的服务器函数调用
/app_server(3000)默认入口,App-1 的页面与资源

配置文件中的注释也直接点明了设计意图:/app2 will serve the client for app2, and any client can call the api by calling /app2/api,以及We need to set app2 to have a different pkg directory, and to forward on that——即 App-2 必须使用独立 pkg 目录,Nginx 再按该目录路径单独转发。

四、一键启动与清理脚本

4.1 启动脚本 run.sh(macOS / Docker Desktop)

原文档给出的启动方式:

./run.sh

run.sh 的内容解析:

# 保存 pwd 变量 # 将 pwd 追加到 nginx.conf 前缀 # 使用新的 nginx.conf 路径运行该命令 (cd app-1 && cargo leptos serve) & \ (cd app-2 && cargo leptos serve) & \ (cd shared-server-1 && cargo run) & \ (cd shared-server-2 && cargo run) & \ ( current_dir=$(pwd) && \ docker run --rm -v "$current_dir"/nginx.conf:/etc/nginx/nginx.conf:ro -p 80:80 nginx)

脚本依次在后台启动四个服务,并通过 Docker 运行 Nginx:

  • (cd app-1 && cargo leptos serve):以开发模式启动 App-1(端口 3000)。
  • (cd app-2 && cargo leptos serve):以开发模式启动 App-2(端口 3001)。
  • (cd shared-server-1 && cargo run):启动共享服务器 1(端口 3002)。
  • (cd shared-server-2 && cargo run):启动共享服务器 2(端口 3003)。
  • docker run --rm -v "$current_dir"/nginx.conf:/etc/nginx/nginx.conf:ro -p 80:80 nginx:以只读方式挂载项目内的nginx.conf到容器内,映射宿主机 80 端口。注意这里通过current_dir=$(pwd)拼接出 nginx.conf 的绝对路径,因为 Docker 挂载卷不接受相对路径。

4.2 Linux 启动脚本 run_linux.sh

./run_linux.sh

run_linux.sh 与 run.sh 的唯一区别在 Nginx 启动命令:

docker run --rm -v "$current_dir"/nginx_linux.conf:/etc/nginx/nginx.conf:ro -p 80:80 --network="host" nginx

它挂载nginx_linux.conf,并追加--network="host"参数,让容器直接使用宿主机网络栈,因此配置中可以使用127.0.0.1直连四个后端端口。

4.3 清理脚本 kill.sh

原文档强调:结束示例时不要只按 Ctrl-C,因为「多次按 Ctrl-C 无法关闭所有已打开的程序」,需要运行:

./kill.sh

kill.sh 通过lsof找到占用各端口的进程并结束:

lsof -ti :3000 | xargs kill && \ lsof -ti :3001 | xargs kill && \ lsof -ti :3002 | xargs kill && \ lsof -ti :3003 | xargs kill && \ lsof -ti :80 | xargs kill

即按顺序清理 3000、3001、3002、3003 四个服务端口以及 80 端口的 Nginx 进程。

4.4 启动后的访问验证

启动完成后:

  • 浏览器访问http://localhost→ 得到 App-1 的页面;
  • 浏览器访问http://localhost/app2→ 得到 App-2 的页面;
  • 在两个页面上点击「request from shared server 1 / 2」按钮,即可通过 action 向两个共享服务器发起请求并展示返回消息。

五、客户端如何跨服务器调用:Action 与 Local Resource

5.1 通过 Server Action 调用共享服务器

App-1 的 app.rs 展示了标准做法:

use shared_server::SharedServerFunction; use shared_server_2::SharedServerFunction2; let hello_1_action = Action::<SharedServerFunction, _>::server(); let hello_2_action = Action::<SharedServerFunction2, _>::server(); let value_1 = create_rw_signal(String::from("waiting for update from shared server.")); let value_2 = create_rw_signal(String::from("waiting for update from shared server 2.")); create_effect(move |_| { if let Some(Ok(msg)) = hello_1_action.value().get() { value_1.set(msg) } }); create_effect(move |_| { if let Some(Ok(msg)) = hello_2_action.value().get() { value_2.set(msg) } });

按钮点击时通过dispatch触发服务器调用:

<button on:click=move |_| hello_1_action.dispatch(SharedServerFunction{})> request from shared server 1 </button> {move || value_1.get()}

由于服务器函数上通过#[server(prefix = "/api_shared", ...)]声明了前缀,客户端编译时会自动把请求发往 Nginx 的/api_shared路径,再被 Nginx 转发到 Shared-Server-1。App-2 的 app.rs 采用了完全相同的 action 模式,证明「任何客户端都可以调用任一共享服务器」。

5.2 关键陷阱:create_resource在 SSR 下不适用于跨服务器调用

这是原文档特别强调、也是本示例最有价值的一条经验:

create_resource在尝试与不同服务器通信时无法按预期工作。它会尝试在你正在提供 SSR 内容的服务器上运行服务器函数。如果你的服务器函数依赖该服务器上不存在的状态,就会导致错误。

原因在于:使用 SSR 时,create_resource会在渲染服务器端初始化并执行资源(见 app-1/src/app.rs 中的注释:A resource is initialized on the rendering server when using SSR)。也就是说,如果你在 App-1 页面里写create_resource(..., shared_server::shared_server_function),Leptos 会在 App-1(3000 端口)服务端尝试执行这个函数——而该函数注册在 Shared-Server-1(3002 端口),App-1 内部并无对应路由与状态,因此会失败。

正确的替代方案有两个:

  1. 使用create_local_resourcecreate_local_resource会等待客户端加载完成后,才在客户端初始化执行(见 app-1/src/app.rs 的注释:A local resource will wait for the client to load before attempting to initialize),从而让请求真正从浏览器发出、经由 Nginx 路由到正确的共享服务器。示例代码:
// A local resource will wait for the client to load before attempting to initialize. let hello_1 = create_local_resource(move || (), |_| shared_server::shared_server_function()); // 这样不行:let hello_1 = create_resource(move || (), |_| shared_server::shared_server_function());
  1. 使用 Server Action(推荐):如上文所示,action 天然在客户端触发,与渲染服务器无关,最适合跨服务器调用。

5.3 Suspense 展示本地资源结果

App-1 用<Suspense>包裹本地资源以呈现加载与成功状态:

<Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> {move || { hello_1.get().map(|data| match data { Err(_) => view! { <pre>"Error"</pre> }.into_view(), Ok(hello) => hello.into_view(), }) }} </Suspense>

create_local_resource返回响应式资源,配合Suspense可以在 SSR 页面中优雅地处理异步数据的加载态与错误态。而两个 action 的响应则通过create_effect监听action.value()写入信号,驱动界面更新——这形成了「本地资源 + Action + 响应式信号」三种机制协作的完整跨服务器通信方案。

六、源码验证要点与常见问题排查

6.1 关键实现位置速查

  • Nginx 路由拆分:nginx.conf(macOS/Docker Desktop)与 nginx_linux.conf(Linux)
  • 共享服务器函数声明与路由前缀:shared-server-1/src/lib.rs、shared-server-2/src/lib.rs
  • 共享服务器 axum 路由注册与状态注入:shared-server-1/src/main.rs、shared-server-2/src/main.rs
  • App-1 的跨服务器调用示例(local resource + action):app-1/src/app.rs
  • App-2 的跨服务器调用示例(纯 action):app-2/src/app.rs
  • App-2 独立 pkg 目录配置:site-pkg-dir = "pkg2"(见 app-2/Cargo.toml)

6.2 常见问题排查

  1. 80 端口被占用:先确认本机 80 端口未被其他进程占用,否则 Docker 映射失败。可用lsof -i :80检查。
  2. macOS 上访问共享服务器 404:确认使用的是 nginx.conf(host.docker.internal)。若误用了 Linux 版配置(127.0.0.1),Docker 容器内访问不到宿主机服务。
  3. Linux 上容器内无法连接后端:确认使用run_linux.sh并以--network="host"启动,否则127.0.0.1在容器内指向容器自身。
  4. App-2 样式或 WASM 加载失败:检查是否同时满足两点——App-2 的 Cargo.toml 中site-pkg-dir = "pkg2",且 Nginx 中存在location /pkg2转发规则,二者缺一不可。
  5. create_resource调用共享服务器报错:按本文 5.2 节改用create_local_resource或 Server Action。
  6. 多次 Ctrl-C 后端口仍被占用:按原文档建议运行./kill.sh清理 3000/3001/3002/3003/80 端口。

七、结语:本示例的可复用架构价值

nginx-mpmc 展示了一套可直接迁移到生产环境的 Leptos 多服务部署范式:

  • 入口统一:单一域名 + Nginx 路径路由,天然规避 CORS 与多域名证书问题;
  • 前后端解耦:SSR 渲染层与纯服务器函数服务分离,共享服务器可以独立扩展、独立部署、复用状态注入(axum::Extension);
  • 资源隔离:通过site-pkg-dir为每个 SSR 应用分配独立静态资源路径,避免 WASM/JS 产物互相覆盖;
  • 通信纪律:跨服务器调用必须走客户端侧触发机制(Action /create_local_resource),这是 SSR 多服务器场景下避免「在错误服务器上执行函数」的关键约束。

无论你是要在单机 Docker 环境中模拟微服务架构,还是规划 Leptos 应用的模块化部署,都可以以此为起点:按需增加upstreamlocation规则、为每个新服务分配独立端口与 pkg 目录,即可将任意数量的客户端与服务器纳入同一域名体系。

【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询