Umi Utoopack 开发服务器的 WebSocket 代理实战:配置、实现与源码验证
【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi
本文以 Umi 仓库中的示例examples/with-utoopack-websocket-proxy为主体,完整讲解如何在 Utoopack 打包器的开发服务器下为业务 WebSocket 后端(/ws路径)配置代理,使浏览器通过 Umi 开发端口透明访问独立运行的 WebSocket 服务。读完本文,你将掌握该示例的三步启动流程、.umirc.ts中proxy配置各参数(target/ws/changeOrigin)的实际含义,并能结合 Umi 源码理解开发服务器的双端口代理架构、WebSocket upgrade 请求如何被路由到正确的后端,以及对应的测试如何验证这一行为。
示例目标与整体架构
该示例要验证的核心问题是:当项目使用 Utoopack 作为开发打包器时,Umi 的proxy配置能否同时承载两类流量——
- 浏览器到业务后端的 WebSocket 长连接(本示例为
/ws→ws://127.0.0.1:3000); - 浏览器到 Utoopack 内部 dev server 的 HMR WebSocket(
/turbopack-hmr)。
两者互不干扰,且都经过 Umi 开发端口转发。要理解这一点,先看 Utoopack 开发模式的架构:从 packages/bundler-utoopack/src/index.ts 的dev()函数可以看到,Umi 对外监听port(默认 8000,见 L228-L229)的 Express 服务器,同时 Utoopack 自身的utooPackServe监听utooServePort = port + 1(即 8001):
const port = opts.port || 8000; const utooServePort = port + 1;Express 服务器承担了 CORS、compression、业务代理(opts.config.proxy)、静态资源代理和 history fallback 等职责;静态资源请求经express-http-proxy转发到 8001 端口,HMR 的 WebSocket 则通过一个专门的http-proxy-middleware实例转发到 8001。业务 WebSocket 代理与这条 HMR 通道走的是不同的中间件,这正是该示例要验证的路由正确性。
快速开始(继承自原 README)
以下操作流程完整继承自 示例 README,在仓库根目录执行:
第一步,构建一次本地 Utoopack 适配器(只需执行一次):
pnpm --filter @umijs/bundler-utoopack build第二步,启动 WebSocket 后端(示例自带的无依赖 Node 服务,监听ws://127.0.0.1:3000/ws):
pnpm --filter @example/with-utoopack-websocket-proxy backend第三步,在另一个终端启动 Umi 示例:
pnpm --filter @example/with-utoopack-websocket-proxy dev打开http://127.0.0.1:8000,页面应显示状态connected和消息connected through the Umi proxy;后端终端应恰好打印一条对/ws的连接日志。
三条脚本定义在 package.json 中:backend执行node ./scripts/ws-server.mjs,dev与build分别是umi dev和umi build。
配置解析:.umirc.ts中的 proxy 配置
示例的完整配置见 .umirc.ts:
import { defineConfig } from 'umi'; export default defineConfig({ // utoopack: {}, mako: {}, proxy: { '/ws': { target: 'ws://127.0.0.1:3000', changeOrigin: true, ws: true, }, }, });各参数含义如下:
| 参数 | 取值 | 作用 |
|---|---|---|
键'/ws' | 路径前缀 | 作为context,匹配所有以/ws开头的请求(含 upgrade 请求) |
target | ws://127.0.0.1:3000 | 转发目标,即后端 WebSocket 服务地址;使用ws://协议表明目标是 WebSocket 服务 |
ws | true | 启用 WebSocket 代理,代理层会监听 upgrade 事件并转发握手 |
changeOrigin | true | 将请求头中的origin改写为目标地址的 origin,避免跨域后端拒绝握手 |
注意当前仓库中该文件将utoopack: {}注释、启用了mako: {},说明打包器字段可按需切换,而proxy配置本身与所用打包器无关。
配置的消费路径可以直接在源码中追踪:dev()中执行if (opts.config.proxy) { createProxy(opts.config.proxy, app); }(packages/bundler-utoopack/src/index.ts#L257-L259),最终落到 packages/bundler-utils/src/proxy.ts 的createProxy。该函数支持三种 proxy 写法(对象数组、单对象、键值对形式),本示例使用的是键值对形式——键即context,随后为每个条目创建http-proxy-middleware中间件,并在onProxyReq中处理changeOrigin(把origin头替换为target的 origin)、在onProxyRes中写入x-real-url响应头(packages/bundler-utils/src/proxy.ts#L25-L45)。
后端实现:零依赖的原始 WebSocket 服务
后端 scripts/ws-server.mjs 不引入任何第三方 WebSocket 库,直接用node:http完成 RFC 6455 握手,这对理解代理“到底转发了什么”很有价值:
- 普通 HTTP 请求一律返回
426 Upgrade Required(L5-L8),强调该端口只接受 WebSocket 握手; upgrade事件里校验req.url === '/ws'且存在sec-websocket-key头,否则直接销毁 socket(L10-L14);- 按协议计算
Sec-WebSocket-Accept:将客户端的 key 与固定 GUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接后做 SHA1,再 Base64 编码,写入101 Switching Protocols响应头完成握手(L16-L30); - 握手完成后立即发送一帧文本消息(帧头
0x81表示 FIN + text opcode):connected through the Umi proxy,并打印[backend] WebSocket connected: /ws日志(L31-L32)——这正是 README 中“后端终端应报告恰好一条/ws连接”这一验收标准的来源。
前端页面:浏览器侧的验证逻辑
页面 pages/index.tsx 的要点:
- 按当前协议动态构造 WebSocket 地址:
location.protocol === 'https:'时用wss:,否则用ws:,然后拼接location.host与/ws路径(L7-L9)。注意浏览器访问的是 Umi 开发端口 8000,而不是后端的 3000——连接能否建立完全取决于代理是否生效; - 监听
open/message/error/close四个事件,分别把状态更新为connected、回填后端消息、error、closed(L11-L14); useEffect清理函数中关闭 socket,避免组件卸载后连接泄漏。
页面上Status: connected与Backend message: connected through the Umi proxy同时出现,即代表整条链路(浏览器 → Umi 8000 端口代理 → 后端 3000 端口)贯通。
源码原理:upgrade 请求如何被路由
回到 packages/bundler-utoopack/src/index.ts 的dev(),WebSocket 相关的关键代码有两处。
其一,HMR 专用代理在用户 proxy 之前注册(L249-L259):
// proxy ws to utoopack server const wsProxy = createProxyMiddleware('/turbopack-hmr', { target: `http://127.0.0.1:${utooServePort}`, ws: true, logLevel: 'silent', }); app.use('/turbopack-hmr', wsProxy); if (opts.config.proxy) { createProxy(opts.config.proxy, app); }其二,也是最容易被忽略的一步——把 upgrade 事件显式挂到 HTTP 服务器上(L350-L354):
// prevent first websocket auto disconnected // ref https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade if (wsProxy.upgrade) { server.on('upgrade', wsProxy.upgrade); }http-proxy-middleware的 WebSocket 订阅并非在模块加载时立即生效,而是在首个常规 HTTP 请求经过时才注册 upgrade 监听;显式调用wsProxy.upgrade绑定到server.on('upgrade', ...)可以保证首个 WebSocket 握手不被自动断开。用户的/ws代理则由createProxy内部创建的中间件各自处理其context匹配的 upgrade 请求。
这套“两个 WebSocket 代理各管一条路径”的设计,被 packages/bundler-utoopack/src/dev.test.ts 中的测试routes WebSocket upgrades to the matching proxy only直接验证(L156-L218):测试先起一个模拟业务后端(响应 upgrade 时记录收到的路径),再调用dev()并传入与示例同构的proxy: { '/ws': { target: 'ws://127.0.0.1:<port>', ws: true } }配置;随后分别向开发端口发起/ws和/turbopack-hmr两个 upgrade 请求,断言业务后端只收到['/ws']、Utoopack 内部服务只收到['/turbopack-hmr']。这从自动化角度确认了示例所依赖的路由隔离行为。
常见问题:dev server 启动阶段的 503
由于 Express 代理服务器与 Utoopack 内部服务是并发启动的(Promise.all([serverReady, utooPackServe(...)]),L377-L388),静态资源代理存在一个短暂的窗口期:目标端口尚未监听时请求会被拒绝。dev()中对这类ECONNREFUSED错误做了专门处理(L22-L28 的isUtoopackProxyStartupError与 L288-L294 的错误处理器):返回503和文本Utoopack dev server is starting.,而不是抛出异常。若你在页面刚打开时看到 503,稍等片刻刷新即可。业务 WebSocket 侧同理——后端服务未启动时,页面状态会显示error,此时优先确认backend脚本已在监听 3000 端口。
小结
这个看似只有三个文件的示例,实际上覆盖了 Utoopack 开发模式下的完整 WebSocket 代理链路:
- 配置层:
.umirc.ts的proxy以路径前缀为键,ws: true+target: 'ws://...'声明 WebSocket 转发,changeOrigin: true处理 origin 头改写; - 架构层:Umi 8000 端口的 Express 代理服务器与 8001 端口的 Utoopack 内部服务并存,
/turbopack-hmr(HMR)与用户代理(/ws)各走各的中间件; - 验证层:前端页面上的状态/消息展示、后端终端的连接日志,以及 dev.test.ts 中对 upgrade 路由隔离的单测,三层证据互相印证。
按上述三步启动流程即可在本地完整复现:先构建@umijs/bundler-utoopack,再起backend与dev,在http://127.0.0.1:8000上观察connected状态与connected through the Umi proxy消息即验证成功。
【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考