在Glitch的Node.js+Fastify+Handlebars中如何先渲染初始数据再更新最终数据?
分步解决Fastify+Handlebars异步内容更新问题
核心思路
HTTP是无状态的同步协议,要实现"先返回初始内容,再更新最终内容",需要拆分流程:
- 服务器收到请求后,立即返回带初始内容的页面,并启动异步任务处理耗时操作(API调用等)
- 前端通过轮询或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
相关产品推荐
相关产品推荐

