You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Glitch的Node.js+Fastify+Handlebars中如何先渲染初始数据再更新最终数据?

分步解决Fastify+Handlebars异步内容更新问题

核心思路

HTTP是无状态的同步协议,要实现"先返回初始内容,再更新最终内容",需要拆分流程:

  1. 服务器收到请求后,立即返回带初始内容的页面,并启动异步任务处理耗时操作(API调用等)
  2. 前端通过轮询或Server-Sent Events(SSE)主动获取任务状态,完成后替换页面内容

方案一:轮询实现(简单易上手)

1. 服务器端调整

首先添加内存存储跟踪任务状态(Glitch单实例环境可用,多实例需替换为Redis等共享存储),修改路由逻辑:

const crypto = require('crypto');
// 内存存储任务状态,生产环境建议用Redis
const taskStore = new Map();

fastify.get("/", async function (request, reply) {
  let params = { content: "<h2>正在生成内容,请稍候...</h2>" };
  
  if (request.query.generate === "now") {
    const taskId = crypto.randomUUID();
    // 初始化任务状态
    taskStore.set(taskId, { status: "pending", result: null });
    
    // 启动异步任务,不阻塞响应返回
    (async () => {
      try {
        // 替换为实际的耗时API调用逻辑
        await new Promise(resolve => setTimeout(resolve, 3000));
        const finalContent = "<h2>这是最终生成的内容:XXX</h2>";
        taskStore.set(taskId, { status: "done", result: finalContent });
      } catch (err) {
        taskStore.set(taskId, { status: "error", result: "<h2>生成失败,请重试</h2>" });
      }
    })();
    
    // 将任务ID传给模板,供前端轮询使用
    params.taskId = taskId;
  }
  
  return reply.view("/src/pages/index.hbs", params);
});

// 新增轮询用的状态查询路由
fastify.get("/task-status", async function (request, reply) {
  const { taskId } = request.query;
  if (!taskId || !taskStore.has(taskId)) {
    return reply.send({ status: "invalid" });
  }
  
  const task = taskStore.get(taskId);
  // 任务完成后清理存储,避免内存泄漏
  if (task.status === "done" || task.status === "error") {
    taskStore.delete(taskId);
  }
  return reply.send(task);
});

2. 前端Handlebars模板调整

在index.hbs中添加轮询逻辑,当存在taskId时自动查询状态并更新内容:

<!DOCTYPE html>
<html>
<head>
  <title>内容生成器</title>
</head>
<body>
  {{{content}}}
  <a href="?generate=now">生成内容</a>
  
  {{#if taskId}}
  <script>
    const taskId = "{{taskId}}";
    const pollInterval = setInterval(async () => {
      try {
        const response = await fetch(`/task-status?taskId=${taskId}`);
        const data = await response.json();
        
        if (data.status === "done") {
          document.body.innerHTML = data.result + '<a href="?generate=now">生成内容</a>';
          clearInterval(pollInterval);
        } else if (data.status === "error") {
          document.body.innerHTML = data.result + '<a href="?generate=now">生成内容</a>';
          clearInterval(pollInterval);
        }
      } catch (err) {
        clearInterval(pollInterval);
        document.body.innerHTML = '<h2>连接出错,请重试</h2>' + '<a href="?generate=now">生成内容</a>';
      }
    }, 500);
  </script>
  {{/if}}
</body>
</html>

方案二:Server-Sent Events(SSE)实现(更高效)

SSE允许服务器主动向前端推送更新,无需前端频繁轮询,适合长时间任务场景。

1. 服务器端新增SSE路由

fastify.get("/task-stream", async function (request, reply) {
  // 设置SSE响应头
  reply.header("Content-Type", "text/event-stream");
  reply.header("Cache-Control", "no-cache");
  reply.header("Connection", "keep-alive");
  
  const { taskId } = request.query;
  if (!taskId || !taskStore.has(taskId)) {
    reply.write("event: error\ndata: 无效的任务ID\n\n");
    reply.end();
    return;
  }
  
  // 定时检查任务状态
  const checkInterval = setInterval(async () => {
    const task = taskStore.get(taskId);
    if (task.status === "done") {
      reply.write(`event: done\ndata: ${JSON.stringify(task.result)}\n\n`);
      clearInterval(checkInterval);
      taskStore.delete(taskId);
      reply.end();
    } else if (task.status === "error") {
      reply.write(`event: error\ndata: ${JSON.stringify(task.result)}\n\n`);
      clearInterval(checkInterval);
      taskStore.delete(taskId);
      reply.end();
    }
  }, 500);
  
  // 客户端断开连接时清理资源
  request.raw.on("close", () => {
    clearInterval(checkInterval);
    taskStore.delete(taskId);
  });
});

2. 前端替换为SSE逻辑

{{#if taskId}}
<script>
  const taskId = "{{taskId}}";
  const eventSource = new EventSource(`/task-stream?taskId=${taskId}`);
  
  eventSource.addEventListener("done", (event) => {
    const result = JSON.parse(event.data);
    document.body.innerHTML = result + '<a href="?generate=now">生成内容</a>';
    eventSource.close();
  });
  
  eventSource.addEventListener("error", (event) => {
    const errorMsg = JSON.parse(event.data);
    document.body.innerHTML = errorMsg + '<a href="?generate=now">生成内容</a>';
    eventSource.close();
  });
  
  // 页面关闭时断开连接
  window.addEventListener("beforeunload", () => {
    eventSource.close();
  });
</script>
{{/if}}

关键注意事项

  • 内存存储仅适用于单实例部署,多实例需使用Redis等共享存储同步任务状态
  • 任务完成后及时清理存储,避免内存泄漏
  • 可根据实际需求调整轮询间隔或SSE检查频率

内容的提问来源于stack exchange,提问作者Samuli Viitasaari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 13:58:23