SSR入门分享

Meow 发布于 2025-05-14 393 次阅读 预计阅读时间: 6 分钟


🚀 服务端渲染(SSR)入门分享:聊聊 eventSource 和 Next.js / Nuxt.js 的实践

最近有朋友问我:“现在做前端还搞 SSR 吗?不是都客户端渲染(CSR)就行了吗?”

我笑着回答:“你以为我愿意吗?是 SEO 要求的。”

今天简单分享一下我最近踩 SSR 的一些坑,顺便聊聊 eventSource 这个冷门但有趣的钩子,还有 Next.js / Nuxt.js 这些现代框架如何帮我们优雅地搞定 SSR

🤔 SSR 是什么?为啥还要用?

SSR(Server Side Rendering),顾名思义,就是在服务器把 HTML 页面“渲染好”,再返回给浏览器,而不是像 CSR 那样等 JS 加载完再让浏览器自己渲染。

如下所示

模式CSR(客户端渲染)SSR(服务端渲染)
页面内容初始空壳,JS 拉数据后渲染初始就有完整内容
SEO 友好❌(搜索引擎抓不到内容)✅(服务端已生成)
首屏速度相对慢相对快(但服务器压力大)

📡 eventSource 是啥?它和 SSR 有啥关系?

EventSource 是 HTML5 提供的一种 服务端推送(Server-Sent Events, SSE) 技术。光看字面意思就是事件来源

你可以简单理解成:服务端主动“往前端发消息”,而不是前端不停轮询。

const es = new EventSource('/api/events');

es.onmessage = (event) => {
  console.log('推送消息:', event.data);
};

它的作用和 WebSocket 类似,但更轻量、基于 HTTP 协议。
在 SSR 场景中,SSE(Server-Sent Events)可以作为 SSR 后的补充手段,用于实时推送数据,实现日志回显、构建进度或 AI 对话等流式输出。

在我之前做的MeowTalk一个LLM对话平台中就是用了该API做AI对话的实时推送,以下是代码片段

//前端,使用new创建实例
try {
            const eventSource = new EventSource(`http://localhost:5927/askai?message=${encodeURIComponent(value)}&sessionId=${sessionId}`, {withCredentials: true});

            eventSource.onmessage = (event) => {
                onAiMessage?.(event.data);
            };

            eventSource.onerror = () => {
                eventSource.close();
                setLoading(false);
                message.error("连接异常,已断开");
            };

            eventSource.addEventListener("end", () => {
                eventSource.close();
                setLoading(false);
            });

        } catch (error) {
            console.error("请求出错:", error);
        }

//后端
const fetchMeowTalk = async (message: string, res?: Response): Promise<string> => {
    return new Promise((resolve, reject) => {
        try {
            const response = axios.post("https://spark-api-open.xf-yun.com/v1/chat/completions/",
                {
                    "max_tokens": 4096,
                    "top_k": 4,
                    "temperature": 0.5,
                    "messages": [
                        {
                            "role": "system",
                            "content": "你是一个无所不知的人"
                        },
                        {
                            "role": "user",
                            "content": message,
                        }
                    ],
                    "model": "4.0Ultra",
                    "stream": true
                },
                {
                    headers: {
                        "Content-Type": "application/json",
                        authorization: `Bearer ${process.env.SERVER_CLIENT_ID}`,
                    },
                    responseType: "stream",
                }
            );

            res?.setHeader("Content-Type", "text/event-stream");
            res?.setHeader("Cache-Control", "no-cache");
            res?.setHeader("Connection", "keep-alive");

            let fullResponse = "";
            let isFirstChunk = true; // 标记是否是第一个文本片段

            response.then(apiResponse => {
                apiResponse.data.on("data", (chunk: Buffer) => {
                    const dataStr = chunk.toString().trim();

                    if (!dataStr.startsWith('data: [DONE]')) {
                        try {
                            const jsonData = JSON.parse(dataStr.replace(/^data:/, "").trim());
                            const textChunk = jsonData?.choices?.[0]?.delta?.content;

                            fullResponse += textChunk;

                            // 只渲染新获取的内容
                            const parsedText = isFirstChunk ? textChunk : fullResponse;
                            isFirstChunk = false;

                            // 发送 SSE 数据流
                            res?.write(`data: ${parsedText}\n\n`); //SSE规范,一定要data: XXX\n\n
                        } catch (err) {
                            console.error("JSON 解析失败:", err);
                        }
                    }
                });

                apiResponse.data.on("end", () => {
                    res?.end();
                    console.log("完整响应内容:", fullResponse);
                    resolve(fullResponse);
                })

                apiResponse.data.on("error", (err: any) => {
                    console.error("流式请求出错:", err);
                    res?.status(500).end();
                });
            }).catch(error => {
                console.error("Error fetching MeowTalk API:", error);
                res?.status(500).json({ error: "Failed to fetch AI response" });
                reject("Ai 请求出错")
            });

        } catch (error) {
            console.error("Error in fetchMeowTalk:", error);
            res?.status(500).json({ error: "Failed to fetch AI response" });
            reject("Ai 请求出错")
        }
    });
};

🛠️ Next.js / Nuxt.js 是如何做 SSR 的?

✅ Next.js(React 生态)

Next.js 是 Vercel 出品的 React SSR 框架,它主打“一站式全栈 React 应用”。

它的 SSR 主要通过两种方式:

  1. getServerSideProps
    每次请求时都会在服务端执行,适合动态内容:

    export async function getServerSideProps(context) {
     const res = await fetch('https://api.example.com/data');
     const data = await res.json();
     return { props: { data } };
    }
  2. getStaticProps + getStaticPaths
    适合生成静态页面(SSG),打包时一次性生成,提高性能。

你甚至可以把 EventSource 接入 API 路由(/api/events),实现“服务端推送构建进度”。

✅ Nuxt.js(Vue 生态)

Nuxt.js 就是 Vue 的 SSR 框架,和 Next.js 的理念非常像。

它的 SSR 路由系统基于文件结构,你只需要放好页面和组件,Nuxt 会帮你自动配置服务端渲染。

它也有几种“数据预取”方式:

  • asyncData:页面级钩子,在 SSR 时运行,用来提前加载数据。
  • fetch:在组件中运行,也支持 SSR。
export default {
  async asyncData({ params }) {
    const data = await fetchData(params.id)
    return { data }
  }
}

此外,你可以用 Nuxt 插件方式监听服务端事件或使用中间件,实现和 EventSource 类似的功能。


小结

虽然现在前端框架普遍以客户端渲染(CSR)为主,但服务端渲染(SSR)在某些场景下依然不可替代,甚至越来越重要,在以下场景下它依然有用:

  • 📈 需要 SEO 的场景(比如博客、电商)
  • ⚡️ 首屏加载性能敏感的项目
  • 🌐 面向爬虫、社交分享卡片的内容

而在 SSR 渲染之后,有些数据仍然是动态变化的、实时性的,比如日志回显、AI 对话响应、构建进度、股票价格、数据大屏更新等。此时,EventSource(SSE)就是一种非常合适的解决方案

  • 🔄 不需要复杂的 WebSocket 握手和状态管理;
  • 🚀 建立简单(只用一个 HTTP 请求);
  • 📡 推送稳定可靠,兼容主流浏览器;
  • ✅ 非常适合“从服务端单向推送数据到客户端”的需求。
一只菜菜的半栈工程师
最后更新于 2025-05-21