news 2026/9/14 18:14:27

Umi Utoopack 开发服务器的 WebSocket 代理实战:配置、实现与源码验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Umi Utoopack 开发服务器的 WebSocket 代理实战:配置、实现与源码验证

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.tsproxy配置各参数(target/ws/changeOrigin)的实际含义,并能结合 Umi 源码理解开发服务器的双端口代理架构、WebSocket upgrade 请求如何被路由到正确的后端,以及对应的测试如何验证这一行为。

示例目标与整体架构

该示例要验证的核心问题是:当项目使用 Utoopack 作为开发打包器时,Umi 的proxy配置能否同时承载两类流量——

  1. 浏览器到业务后端的 WebSocket 长连接(本示例为/wsws://127.0.0.1:3000);
  2. 浏览器到 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.mjsdevbuild分别是umi devumi 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 请求)
targetws://127.0.0.1:3000转发目标,即后端 WebSocket 服务地址;使用ws://协议表明目标是 WebSocket 服务
wstrue启用 WebSocket 代理,代理层会监听 upgrade 事件并转发握手
changeOrigintrue将请求头中的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、回填后端消息、errorclosed(L11-L14);
  • useEffect清理函数中关闭 socket,避免组件卸载后连接泄漏。

页面上Status: connectedBackend 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.tsproxy以路径前缀为键,ws: true+target: 'ws://...'声明 WebSocket 转发,changeOrigin: true处理 origin 头改写;
  • 架构层:Umi 8000 端口的 Express 代理服务器与 8001 端口的 Utoopack 内部服务并存,/turbopack-hmr(HMR)与用户代理(/ws)各走各的中间件;
  • 验证层:前端页面上的状态/消息展示、后端终端的连接日志,以及 dev.test.ts 中对 upgrade 路由隔离的单测,三层证据互相印证。

按上述三步启动流程即可在本地完整复现:先构建@umijs/bundler-utoopack,再起backenddev,在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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 18:14:07

花15亿美元买下35人团队,巨头却坚决不办收购

花15亿美元买下35人团队&#xff0c;巨头却坚决不办收购 一家成立才一年多、全公司一共只有大约 35 人的初创团队&#xff0c;竟然让科技巨头心甘情愿端出了超过 15 亿美元的谈判对价。但更离奇的地方在于&#xff0c;当这笔让人咋舌的巨资落地之后&#xff0c;买方既没有把它买…

作者头像 李华
网站建设 2026/9/14 18:13:00

Trae中Git实战:从配置、分支管理到冲突解决全流程

经常有人问我&#xff1a;Trae 写代码用得很顺手&#xff0c;但一遇到 Git 就犯怵&#xff0c;图形界面按钮不敢点&#xff0c;命令行又记不住那么多参数。坦白讲&#xff0c;我刚开始转到 Trae 的时候也有同样的顾虑&#xff0c;但实际用了一个多月、把分支管理、推送、冲突处…

作者头像 李华
网站建设 2026/9/14 18:11:25

LIMS系统如何解决实验室数据管理难题

1. 实验室数据管理的现状与挑战 实验室数据管理长期以来面临着从传统手工记录向数字化系统转型的迫切需求。记得三年前我参与某化工企业实验室调研时&#xff0c;看到实验员们还在使用纸质记录本&#xff0c;各种颜色的便利贴贴满操作台&#xff0c;重要数据分散在Excel表格、纸…

作者头像 李华
网站建设 2026/9/14 18:10:39

内景 楼梯艺术画廊场景模型

本项目为前几天收费帮学妹做的一个项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 楼梯艺术画廊场景模型 地址&#xff1a;本地PC端运行&#xff08;或WebGL端部署链接…

作者头像 李华
网站建设 2026/9/14 18:10:37

提示词工程实战:10个降低大语言模型猜测成本的技巧与模板

不用把它想得太玄。我在日常项目里折腾提示词也有几年了&#xff0c;刚入门的时候一度以为这是门“语言艺术”&#xff0c;靠文采和花哨的措辞取胜。后来被真实业务连续教育了几次才明白&#xff0c;提示词工程本质是需求分析&#xff0c;是把模糊的意图翻译成模型能稳定执行的…

作者头像 李华