纯前端用 Next.js 本地实现 SSE 流式传输

很多同学已经熟练使用 AI 辅助编程了,但直觉在本地搭建一个轻便的 SSE "打字"输出的效果并不好弄。有很多教程需要依赖 ExpressNode 来监听端口,或者使用 Mock Service Worker (MSW),总之都要依赖点什么,一旦中间某处卡住了,直接"未开始便放弃了"。其实,只需要 Next.js 就够了。

框架与依赖

框架

Next.js (支持 App router 的版本)

依赖(非必须)

@microsoft/fetch-event-source(直接使用原生的 EventSource 也可以)

思路

用 Next.js 中的 API route 模拟一个 text/event-stream 头的返回

代码

API route 部分

js 复制代码
// 存到这里 src/app/api/sse/route.ts

const EVENTS = {
  MESSAGE: "message",
  ERROR: "error",
  DONE: "done",
}
const WHEN_ERROR = 5;
const WHEN_DONE = 10;

export async function GET() {
  const encoder = new TextEncoder();
  let interval: NodeJS.Timeout;

  const stream = new ReadableStream({
    start(controller) {
      let count = 0;
      let payload;
      interval = setInterval(() => {
        count++;
        payload = {
          time: new Date().toISOString(),
          count,
          event: count === WHEN_DONE ? EVENTS.DONE : EVENTS.MESSAGE
        };
        
        // uncomment below lines to test error
        // if (count === WHEN_ERROR) {
        //   payload.event = EVENTS.ERROR;
        // }

        // replace data in chunk with your split contents
        const chunk = `id: ${payload.count}\nevent: ${payload.event}\ndata: ${JSON.stringify(payload)}\n\n`;
        controller.enqueue(encoder.encode(chunk));
        if ([EVENTS.ERROR,EVENTS.DONE].includes(payload.event)) {
          clearInterval(interval);
          controller.close();
        }
      }, 500);
    },
    cancel() {
      // cleanup when the stream is canceled
      clearInterval(interval);
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache, no-transform',
      Connection: 'keep-alive',
      // 'X-Accel-Buffering': 'no', // helpful for some proxies
    },
  });
}

页面部分

js 复制代码
// 存到这里 src/app/page.tsx

import { useEffect, useState } from "react";
import { fetchEventSource } from '@microsoft/fetch-event-source';

// 此处代码省略

useEffect(()=>{
    const controller = new AbortController();
    const { signal } = controller;
    const getStream = async()=>{
      console.log("SSE starts")
      await fetchEventSource('/api/sse', {
        signal,
        async onopen(response) {
            if (response.ok) {
                console.log("SSE connected")
                return;
            } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
                // client-side errors are usually non-retriable
                throw new Error("SSE FatalError");
            } else {
                throw new Error("SSE RetriableError");
            }
        },
        onmessage(ev) {
            const data = JSON.parse(ev.data);
            console.log("SSE data: ", data);
            if (data.event === "error" ) {
              controller.abort();
              console.log("SSE error");
            }
            if (data.event === "done" ) {
              console.log("SSE done");
            }
            if (["done", "error"].includes(data.event)) {
              console.log("SSE closed");
              return;
            }
            // setMessages(prev => [...prev,data])
        },
        onerror(err){
          console.log("SSE error: ", err)
        }
      });
    };
    getStream();
  }, []);
  
  // 此处代码省略

结果

跑起来后,如果看到以下 console.log 就成功了。 Have fun :)

相关推荐
β添砖java15 分钟前
CSS网格布局
前端·css·html
木易 士心2 小时前
Ref 和 Reactive 响应式原理剖析与代码实现
前端·javascript·vue.js
程序员博博2 小时前
概率与决策 - 模拟程序让你在选择中取胜
前端
被巨款砸中2 小时前
一篇文章讲清Prompt、Agent、MCP、Function Calling
前端·vue.js·人工智能·web
sophie旭2 小时前
一道面试题,开始性能优化之旅(1)-- beforeFetch
前端·性能优化
Cache技术分享2 小时前
204. Java 异常 - Error 类:表示 Java 虚拟机中的严重错误
前端·后端
uhakadotcom2 小时前
execjs有哪些常用的api,如何逆向分析网站的加签机制
前端·javascript·面试
ObjectX前端实验室2 小时前
【图形编辑器架构】:无限画布标尺与网格系统实现解析
前端·canvas·图形学
你的电影很有趣3 小时前
lesson71:Node.js与npm基础全攻略:2025年最新特性与实战指南
前端·npm·node.js
闲蛋小超人笑嘻嘻3 小时前
find数组方法详解||Vue3 + uni-app + Wot Design(wd-picker)使用自定义插槽内容写一个下拉选择器
前端·javascript·uni-app