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

相关推荐
晴殇i14 小时前
CSS Grid 与 Flexbox:现代前端布局的双子星
前端·css
曹卫平dudu14 小时前
一起学习TailWind Css
前端·css
sosojie14 小时前
and+design的table前端本地分页处理
前端·vue.js
炫饭第一名14 小时前
前端玩转 AI 应用开发|SSE 协议与JS中的流式处理🌊
前端·人工智能·程序员
前端老宋Running14 小时前
一种名为“Webpack 配置工程师”的已故职业—— Vite 与“零配置”的快乐
前端·vite·前端工程化
用户66006766853914 小时前
从“养猫”看懂JS面向对象:原型链与Class本质拆解
前端·javascript·面试
parade岁月14 小时前
我的第一个 TDesign PR:修复 Empty 组件的 v-if 警告
前端
云鹤_14 小时前
【Amis源码阅读】低代码如何实现交互(下)
前端·低代码·架构
StarkCoder14 小时前
一次搞懂 iOS 组合布局:用 CompositionalLayout 打造马赛克 + 网格瀑布流
前端
之恒君14 小时前
JavaScript 对象相等性判断详解
前端·javascript