Umi 开发环境 SSE 流式推送失效的根因与解决方案:UMI_DEV_SERVER_COMPRESS=none 实战
2026/9/14 9:45:57 网站建设 项目流程

Umi 开发环境 SSE 流式推送失效的根因与解决方案:UMI_DEV_SERVER_COMPRESS=none 实战

【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi

本文以 umi 官方示例 with-no-compress-for-sse 为主体,讲解一个典型的开发环境排障问题:umi 开发服务器内置的compress(compression)压缩中间件会把 SSE(Server-Sent Events)流缓冲,导致EventSource无法逐条实时收到数据;并完整演示如何通过UMI_DEV_SERVER_COMPRESS=none关闭压缩中间件,配合api.onBeforeMiddleware插件事件挂载自定义 SSE 接口,让流式推送在本地开发时恢复正常。读完你可以独立复现该示例、定位压缩中间件在开发服务器中的注册位置,并掌握用 umi 插件机制扩展开发服务器中间件的通用做法。

背景:compress 中间件为什么会“吞掉” SSE 流

问题最初来自 umi 社区的 issue #12144:在开发环境下,由于umi dev启动的 dev server 内置了compress压缩中间件,SSE 流在本地开发时传递不符合预期——前端EventSource无法流式逐条获取数据。

原理上,compression 这类压缩中间件会接管响应的write/end行为:数据先被写入压缩缓冲区,按内部阈值决定何时向客户端刷出。对于一次性返回的完整响应这没有问题,但对 SSE 这种需要“每写一条就立刻推给客户端”的长连接流式响应,缓冲机制就破坏了实时性——数据被攒在压缩缓冲里,直到流结束或缓冲达到阈值才一次性发出,客户端感知到的就是“收不到流式更新”,甚至直接判定连接失败。

umi 的 webpack 开发服务器中,这个中间件是在服务初始化阶段无条件挂上的(除非显式关闭)。源码位于 createServer:

// See https://github.com/umijs/umi/issues/12144 if (process.env.UMI_DEV_SERVER_COMPRESS !== 'none') { app.use(require('@umijs/bundler-webpack/compiled/compression')()); }

可以看到关键事实:

  • 压缩中间件使用 umi 编译内置的compression包(@umijs/bundler-webpack/compiled/compression),无需项目自己安装;
  • 判断逻辑是process.env.UMI_DEV_SERVER_COMPRESS !== 'none',即默认开启,只有环境变量取值恰好为none时才跳过注册;
  • 从源码结构看,该判断目前只存在于 webpack 打包器的开发服务器中(packages/bundler-webpack/src/server/server.ts),是默认umi dev的行为。

官方示例全解:一个最小可运行的 SSE 演示工程

示例工程位于examples/with-no-compress-for-sse/,结构非常精简:

文件作用
plugin.tsumi 插件入口,注册 SSE 中间件
sse-middleware.tsExpress SSE 接口实现,每秒推送一条事件
pages/index.tsx前端页面,用EventSource接收并展示事件
package.json定义devdev:nocompress两套启动脚本

后端:用 onBeforeMiddleware 挂载 SSE 接口

示例先通过 umi 插件拿到开发服务器的 Expressapp实例。plugin.ts 全文如下:

import { IApi } from 'umi'; import { sseMiddleware } from './sse-middleware'; export default (api: IApi) => { api.onBeforeMiddleware(({ app }) => { sseMiddleware(app); }); };

onBeforeMiddleware是 umi 的插件生命周期事件之一,类型为IEvent<{ app: Express }>(见 types.ts)。dev 命令在启动打包器时把它转接为插件调用(见 dev.ts):

onBeforeMiddleware(app: any) { api.applyPlugins({ key: 'onBeforeMiddleware', args: { app, }, }); },

事件在 registerMethods.ts 中注册进 umi 的事件体系,最终由 bundler 的createServer在合适的时机执行回调,把 Express 实例交给所有订阅的插件。

需要注意中间件的挂载顺序:在 createServer 中,corscompress中间件最先注册,之后才执行opts.onBeforeMiddleware(app)。也就是说,示例中注册的 SSE 路由虽然运行在压缩中间件“之后”,但 compression 接管的是响应写出管道——这正是压缩会缓冲 SSE 输出的位置,也解释了为什么仅靠用户路由代码无法绕过该问题,必须从环境变量层面关闭压缩。

SSE 路由本身的实现在 sse-middleware.ts,是一个标准的 SSE 服务端写法:

import type { Express } from '@umijs/bundler-utils/compiled/express'; export const sseMiddleware = (app: Express) => { app.get('/events/number', (req, res) => { console.log('new connection'); res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); let counter = 1; const intervalId = setInterval(() => { if (counter === 5) { clearInterval(intervalId); res.end(`data: 事件${counter}\n\n`); return; } res.write(`data: 事件${counter}\n\n`); counter++; }, 1000); req.on('close', () => { clearInterval(intervalId); res.end(); }); }); };

实现要点:

  • 响应头必须包含Content-Type: text/event-stream,SSE 协议的三个响应头在示例中一并设置(Cache-Control: no-cacheConnection: keep-alive);
  • setInterval每秒res.write一条data: 事件N\n\n格式的事件,共 5 条,第 5 条用res.end收尾并清除定时器;
  • 监听req.on('close'),在客户端断开时清理定时器并结束响应,避免服务端定时器泄漏。

前端:用 EventSource 验证流式效果

pages/index.tsx 在页面加载时创建EventSource('/events/number'),把每条消息连同本地接收时间存入 state 并渲染成表格:

useEffect(() => { console.log('开始请求'); const eventSource = new EventSource('/events/number'); let startEvent = new Event('开始请求'); setEvents((prev) => [...prev, startEvent]); eventSource.onmessage = function (e: any) { let item = new Event(e.data); setEvents((prev) => [...prev, item]); }; eventSource.onerror = (e) => { console.log('EventSource failed:', e); eventSource.close(); }; }, []);

页面上明确标注了验证结论:演示:当默认存在 compress 时,数据无法流式获取。结合表格中的“接收时间”列,可以直观对比两种启动方式:

  • 带压缩(默认umi dev):事件无法逐条实时到达,流式行为被破坏(对应 issue #12144 描述的现象);
  • 关闭压缩后:5 条事件以约 1 秒的间隔依次出现在表格中,时间戳逐条递增,证明流式推送恢复正常。

启动方式:两套脚本对照运行

package.json 定义了两个关键脚本:

{ "scripts": { "build": "umi build", "dev": "umi dev", "dev:nocompress": "cross-env UMI_DEV_SERVER_COMPRESS=none npm run dev", "start": "npm run dev" }, "dependencies": { "umi": "workspace:*" }, "devDependencies": { "cross-env": "^7.0.3" } }
  • npm run dev:复现问题。默认启动,压缩中间件生效;
  • npm run dev:nocompress:解决问题。通过cross-env设置UMI_DEV_SERVER_COMPRESS=none后再执行umi dev,保证 Windows/Linux/macOS 各平台都能正确注入环境变量,从而跳过 compression 注册。

不经过 umi 脚本、直接手动运行时,等价命令为:

UMI_DEV_SERVER_COMPRESS=none umi dev

这也是 umi 官方环境变量文档中对UMI_DEV_SERVER_COMPRESS的记录与用法(见 环境变量文档 中 “UMI_DEV_SERVER_COMPRESS” 一节):默认 Umi 开发服务器自带 compress 压缩中间件,这会使开发时 SSE 数据的传输无法流式获取,通过指定UMI_DEV_SERVER_COMPRESS=none来关闭 compress 压缩功能。

UMI_DEV_SERVER_COMPRESS 的适用边界

结合源码与示例,使用该环境变量时需要注意以下前提与限制:

  1. 只影响开发服务器,不影响构建产物。该判断仅出现在umi dev走的createServer流程中(server.ts),umi build生成的生产产物不涉及此中间件;生产环境的流式行为由你部署的网关/服务器决定。
  2. 取值是精确匹配。源码判断为!== 'none',其他任何取值(如offfalse、空字符串)都不会关闭压缩,只有none有效。
  3. 作用对象是 webpack 开发服务器。从当前仓库代码结构看,UMI_DEV_SERVER_COMPRESS的检查目前只存在于packages/bundler-webpack的开发服务器实现中;它对应的是 umi 默认打包器场景,可视为当前仓库已确认的事实边界。
  4. 代价是放弃开发环境的响应压缩。关闭 compression 后,开发服务器对所有响应都不再 gzip,本地开发带宽开销略增,但换来 SSE 之外的流式接口(如长轮询大响应、分块下载)行为也更可预期,通常可接受。

小结

  • 现象:开发环境 SSE 无法流式到达,根因是 dev server 内置的 compress 中间件缓冲了响应流;
  • 解决:启动时附加UMI_DEV_SERVER_COMPRESS=none(跨平台可借助cross-env),源码层面它对应packages/bundler-webpack/src/server/server.ts中对 compression 注册的跳过逻辑;
  • 配套能力:用api.onBeforeMiddleware事件拿到 Express 实例,即可在开发服务器上注册任意自定义中间件(本示例中的/events/numberSSE 路由即此模式);
  • 验证:跑官方示例的两套脚本对照,观察前端表格中事件接收时间是否逐条间隔出现。

完整示例代码与说明参见 示例 README 及同目录下的 plugin.ts、sse-middleware.ts、pages/index.tsx。

【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi

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

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

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

立即咨询