纯前端用 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 :)

相关推荐
FFF-X4 分钟前
前端无感刷新 Token 的 Axios 封装方案
前端
qq_589568104 分钟前
javaweb开发笔记—— 前端工程化
java·前端
gnip23 分钟前
包管理工具的发展
前端
前端工作日常1 小时前
H5 实时摄像头 + 麦克风:完整可运行 Demo 与深度拆解
前端·javascript
韩沛晓2 小时前
uniapp跨域怎么解决
前端·javascript·uni-app
前端工作日常2 小时前
以 Vue 项目为例串联eslint整个流程
前端·eslint
程序员鱼皮2 小时前
太香了!我连夜给项目加上了这套 Java 监控系统
java·前端·程序员
该用户已不存在2 小时前
这几款Rust工具,开发体验直线上升
前端·后端·rust
前端雾辰2 小时前
Uniapp APP 端实现 TCP Socket 通信(ZPL 打印实战)
前端
无羡仙2 小时前
虚拟列表:怎么显示大量数据不卡
前端·react.js