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

相关推荐
哆啦A梦15881 小时前
[前台小程序] 01 项目初始化
前端·vue.js·uni-app
小周同学@3 小时前
谈谈对this的理解
开发语言·前端·javascript
Wiktok3 小时前
Pyside6加载本地html文件并实现与Javascript进行通信
前端·javascript·html·pyside6
一只小风华~3 小时前
Vue:条件渲染 (Conditional Rendering)
前端·javascript·vue.js·typescript·前端框架
柯南二号3 小时前
【大前端】前端生成二维码
前端·二维码
程序员码歌4 小时前
明年35岁了,如何破局?说说心里话
android·前端·后端
博客zhu虎康5 小时前
React Hooks 报错?一招解决useState问题
前端·javascript·react.js
灰海5 小时前
vue中通过heatmap.js实现热力图(多个热力点)热区展示(带鼠标移入弹窗)
前端·javascript·vue.js·heatmap·heatmapjs
王源骏5 小时前
LayaAir鼠标(手指)控制相机旋转,限制角度
前端
大虾写代码6 小时前
vue3+TS项目配置Eslint+prettier+husky语法校验
前端·vue·eslint