普通视图
PinMe:零成本三秒发布你的网站
ArcGIS Pro 添加底图的方式
GPT-5.2 七天“手搓”Chrome,社区评价:依托大的
为什么每次打开 ArcGIS Pro 页面加载都如此缓慢?
🌰在 OpenLayers 中实现图层裁切
taro项目踩坑指南——(一)
小程序 Markdown 渲染血泪史:mp-html 组件从入门到放弃再到重生
从零构建本地AI应用:React与Ollama全栈开发实战
Cursor 最新发现:超大型项目 AI 也能做了,上百个 Agent 一起上
async/await : 一场生成器和 Promise的里应外合
Message组件和Vue3 进阶:手动挂载组件与 Diff 算法深度解析
Vue转React学习笔记(1): 关于useEffect的困惑和思考
前端路由不再难:React Router 从入门到工程化
5大核心分析维度+3种可视化方案:脑肿瘤大数据分析系统全解析 毕业设计 选题推荐 毕设选题 数据分析 机器学习
ArcGIS Pro 实现影像波段合成
2026 年 Node.js + TS 开发:别再纠结 nodemon 了,聊聊热编译的最优解
在开发 Node.js 服务端时,“修改代码 -> 自动生效”的开发体验(即热编译/热更新)是影响效率的关键。随着 Node.js 23+ 原生支持 TS 以及 Vite 5 的普及,我们的工具链已经发生了巨大的更迭。
今天我们深度拆解三种主流的 Node.js TS 开发实现方式,帮你选出最适合 2026 年架构的方案。
一、 方案对比大盘点
| 方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| tsx (Watch Mode) | 基于 esbuild 的极速重启 | 零配置、性能强、生态位替代 nodemon | 每次修改重启整个进程,状态丢失 | 小型服务、工具脚本 |
| vite-node | 基于 Vite 的模块加载器 | 完美继承 Vite 配置、支持模块级 HMR | 配置相对复杂,需手动处理 HMR 逻辑 | 中大型 Vite 全栈项目 |
| Node.js 原生 | Node 23+ Type Stripping | 无需第三方依赖,官方标准 | 需高版本 Node,功能相对单一 | 追求极简、前瞻性实验 |
二、 方案详解
- 现代替代者:
tsx—— 告别 nodemon + ts-node
过去我们常用 nodemon --exec ts-node,但在 ESM 时代,这套组合经常报 ERR_UNKNOWN_FILE_EXTENSION 错误。
tsx 内部集成了 esbuild,它是目前 Node 18+ 环境下最稳健的方案。
-
实现热编译:
bash
npx tsx --watch src/index.ts请谨慎使用此类代码。
-
为什么选它: 它不需要额外的加载器配置(--loader),且
watch模式非常智能,重启速度在毫秒级。
- 开发者体验天花板:
vite-node—— 真正的 HMR
如果你已经在项目中使用 Vite 5,那么 vite-node 是不二之选。它不仅是“重启”,而是“热替换”。
-
核心优势:
-
共享配置:直接复用
vite.config.ts中的alias和插件。 - 按需编译:只编译当前运行到的模块,项目越大优势越明显。
-
共享配置:直接复用
-
实现热更新(不重启进程):
typescript
// src/index.ts import { app } from './app'; let server = app.listen(3000); if (import.meta.hot) { import.meta.hot.accept('./app', (newModule) => { server.close(); // 优雅关闭旧服务 server = newModule.app.listen(3000); // 启动新逻辑,DB连接可复用 }); }请谨慎使用此类代码。
- 官方正统:Node.js 原生支持
如果你能使用 Node.js 23.6+ ,那么可以摆脱所有构建工具。
-
运行:
node --watch src/index.ts - 点评: 这是未来的趋势,但在 2026 年,由于生产环境往往还停留在 Node 18/20 LTS,该方案目前更多用于本地轻量级开发。
三、 避坑指南:Vite 5 打包 Node 服务的报错
在实现热编译的过程中,如果你尝试用 Vite 打包 Node 服务,可能会遇到:
Invalid value for option "preserveEntrySignatures" - setting this option to false is not supported for "output.preserveModules"
原因: 当你开启 preserveModules: true 想保持源码目录结构输出时,Rollup 无法在“强制保留模块”的同时又“摇树优化(Tree Shaking)”掉入口导出。
修复方案:
在 vite.config.ts 中明确设置:
typescript
build: {
rollupOptions: {
preserveEntrySignatures: 'exports-only', // 显式声明保留导出
output: {
preserveModules: true
}
}
}
请谨慎使用此类代码。
四、 总结:我该选哪个?
-
如果你只想快速写个接口,不想折腾配置:请直接使用
tsx。它是 2026 年 nodemon 的完美继承者。 -
如果你在做复杂全栈项目,或者有大量的路径别名:请使用
vite-node。它能让你在 Node 端获得跟前端 React/Vue 编写时一样丝滑的 HMR 体验。 -
如果是为了部署生产环境:无论开发环境用什么,生产环境请务必通过
vite build产出纯净的 JS,并使用node dist/index.js运行。
使用 LangChain.js 在node端 连接glm大模型示例
使用 LangChain 在后端连接大模型:实践指南 🚀
本文以实战项目代码为例,包含完整后端接入、流式(SSE)实现、前端接收示例与调试方法,读者可直接复制运行。
介绍 ✨
随着大模型在各类应用中的普及,后端如何稳健地接入并把模型能力以 API/流式方式对外提供,成为常见需求。本文基于 LangChain(JS)演示如何在 Node.js 后端(Koa)中:
- 初始化并调用大模型(示例使用智谱 GLM 的接入方式)
- 支持普通请求与流式(Server-Sent Events,SSE)响应
- 在前端用 fetch 读取流并实现打字机效果
适用人群:熟悉 JS/TS、Node.js、前端基本知识,想把模型能力放到后端并对外提供 API 的工程师。
一、准备与依赖 🧩
环境:Node.js 16+。
安装依赖(Koa 示例):
npm install koa koa-router koa-bodyparser @koa/cors dotenv
npm install @langchain/openai @langchain/core
在项目根创建 .env:
ZHIPU_API_KEY=你的_api_key_here
PORT=3001
提示:不同模型提供方的 baseURL 与认证字段会不同,请根据提供方文档调整。
二、后端:服务封装(chatService)🔧
把模型调用封装到服务层,提供普通调用与流式调用接口:
// backend/src/services/chatService.js
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
const chatService = {
llm: null,
init() {
if (!this.llm) {
this.llm = new ChatOpenAI({
openAIApiKey: process.env.ZHIPU_API_KEY,
modelName: "glm-4.5-flash",
temperature: 0.7,
configuration: { baseURL: "https://open.bigmodel.cn/api/paas/v4/" },
});
}
},
async sendMessage(message, conversationHistory = []) {
this.init();
const messages = [
new SystemMessage("你是一个有帮助的 AI 助手,使用中文回答问题。"),
...conversationHistory.map((msg) =>
msg.role === "user"
? new HumanMessage(msg.content)
: new SystemMessage(msg.content),
),
new HumanMessage(message),
];
try {
const response = await this.llm.invoke(messages);
return {
success: true,
content: response.content,
usage: response.usage_metadata,
};
} catch (error) {
console.error("聊天服务错误:", error);
return { success: false, error: error.message };
}
},
async sendMessageStream(message, conversationHistory = []) {
this.init();
const messages = [
new SystemMessage("你是一个有帮助的 AI 助手,使用中文回答问题。"),
...conversationHistory.map((msg) =>
msg.role === "user"
? new HumanMessage(msg.content)
: new SystemMessage(msg.content),
),
new HumanMessage(message),
];
try {
// 假设 llm.stream 返回异步迭代器,逐 chunk 返回 { content }
const stream = await this.llm.stream(messages);
return { success: true, stream };
} catch (error) {
console.error("流式聊天服务错误:", error);
return { success: false, error: error.message };
}
},
};
export default chatService;
说明:实际 SDK 接口名(如 invoke、stream)请依据你所用的 LangChain / provider 版本调整。
三、控制器:普通与 SSE 流式(Koa)🌊
SSE 要点:需要直接写原生 res,并设置 ctx.respond = false,防止 Koa 在中间件链结束时覆盖响应或返回 404。
// backend/src/controllers/chatController.js
import chatService from "../services/chatService.js";
const chatController = {
async sendMessage(ctx) {
try {
const { message, conversationHistory = [] } = ctx.request.body;
if (!message) {
ctx.status = 400;
ctx.body = { success: false, error: "消息内容不能为空" };
return;
}
const result = await chatService.sendMessage(
message,
conversationHistory,
);
if (result.success) ctx.body = result;
else {
ctx.status = 500;
ctx.body = result;
}
} catch (error) {
ctx.status = 500;
ctx.body = { success: false, error: "服务器内部错误" };
}
},
async sendMessageStream(ctx) {
try {
const { message, conversationHistory = [] } = ctx.request.body;
if (!message) {
ctx.status = 400;
ctx.body = { success: false, error: "消息内容不能为空" };
return;
}
const result = await chatService.sendMessageStream(
message,
conversationHistory,
);
if (!result.success) {
ctx.status = 500;
ctx.body = result;
return;
}
ctx.set("Content-Type", "text/event-stream");
ctx.set("Cache-Control", "no-cache");
ctx.set("Connection", "keep-alive");
ctx.status = 200;
// 关键:让我们直接操作 Node 原生 res
ctx.respond = false;
for await (const chunk of result.stream) {
const content = chunk.content;
if (content) ctx.res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
ctx.res.write("data: [DONE]\n\n");
ctx.res.end();
} catch (error) {
console.error("流式控制器错误:", error);
ctx.status = 500;
ctx.body = { success: false, error: "服务器内部错误" };
}
},
};
export default chatController;
四、路由与启动 🌐
// backend/src/routes/index.js
import Router from "koa-router";
import chatController from "../controllers/chatController.js";
const router = new Router({ prefix: "/api" });
router.post("/chat", chatController.sendMessage);
router.post("/chat/stream", chatController.sendMessageStream);
export default router;
// backend/src/app.js
import dotenv from "dotenv";
import Koa from "koa";
import bodyParser from "koa-bodyparser";
import cors from "@koa/cors";
import router from "./routes/index.js";
dotenv.config();
const app = new Koa();
app.use(cors({ origin: "*", credentials: true }));
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(process.env.PORT || 3001, () => console.log("Server running"));
五、前端:接收 SSE 流并实现“打字机”效果 ⌨️
前端用 fetch + ReadableStream 读取 SSE 后端发送的 chunk(格式为 data: {...}\n\n)。下面给出简洁示例:
// frontend/src/services/chatService.ts (核心片段)
export const sendMessageStream = async (
message,
conversationHistory,
onChunk,
onComplete,
onError,
) => {
try {
const response = await fetch("/api/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, conversationHistory }),
});
if (!response.ok) throw new Error("网络请求失败");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// 简单按行解析 SSE data: 行
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
onComplete();
return;
}
try {
const parsed = JSON.parse(data);
if (parsed.content) onChunk(parsed.content);
} catch (e) {}
}
}
}
} catch (err) {
onError(err.message || "流式请求失败");
}
};
打字机效果思路(前端)📌:
- 后端 chunk 通常是按小段返回,前端把每个 chunk 追加到
buffer。 - 用一个定时器以固定速度(如 20–40ms/字符)把
buffer的字符逐个移动到展示内容,使文本逐字出现。 - onComplete 时快速显示剩余字符并停止定时器。
你可以参考项目中 App.tsx 的实现(已实现逐 chunk 追加与打字机渲染逻辑)。
App.tsx
import React, { useState } from "react";
import MessageList from "./components/MessageList";
import ChatInput from "./components/ChatInput";
import { chatService, Message } from "./services/chatService";
import "./App.css";
const App: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [isStreaming, setIsStreaming] = useState(false);
const handleSendMessage = async (message: string) => {
const userMessage: Message = { role: "user", content: message };
setMessages((prev) => [...prev, userMessage]);
setIsStreaming(true);
let assistantContent = "";
const conversationHistory = messages;
await chatService.sendMessageStream(
message,
conversationHistory,
(chunk: string) => {
assistantContent += chunk;
setMessages((prev) => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && lastMessage.role === "assistant") {
lastMessage.content = assistantContent;
} else {
newMessages.push({ role: "assistant", content: assistantContent });
}
return newMessages;
});
},
() => {
setIsStreaming(false);
},
(error: string) => {
console.error("流式响应错误:", error);
setMessages((prev) => [
...prev,
{ role: "assistant", content: "抱歉,发生了错误,请稍后重试。" },
]);
setIsStreaming(false);
},
);
};
return (
<div className="app">
<header className="app-header">
<h1>🤖 LangChain + 智谱 GLM</h1>
<p>AI 聊天助手</p>
</header>
<main className="app-main">
<MessageList messages={messages} isStreaming={isStreaming} />
<ChatInput onSendMessage={handleSendMessage} disabled={isStreaming} />
</main>
</div>
);
};
export default App;
MessageList
import React, { useRef, useEffect } from "react";
import { Message } from "../services/chatService";
interface MessageListProps {
messages: Message[];
isStreaming: boolean;
}
const MessageList: React.FC<MessageListProps> = ({ messages, isStreaming }) => {
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isStreaming]);
return (
<div className="message-list">
{messages.length === 0 ? (
<div className="empty-state">
<p>👋 欢迎使用 LangChain + 智谱 GLM 聊天助手!</p>
<p>开始提问吧,我会尽力帮助你 💬</p>
</div>
) : (
messages.map((msg, index) => (
<div key={index} className={`message ${msg.role}`}>
<div className="message-avatar">
{msg.role === "user" ? "👤" : "🤖"}
</div>
<div className="message-content">
<div className="message-role">
{msg.role === "user" ? "用户" : "AI 助手"}
</div>
<div className="message-text">{msg.content}</div>
</div>
</div>
))
)}
{isStreaming && (
<div className="message assistant">
<div className="message-avatar">🤖</div>
<div className="message-content">
<div className="message-role">AI 助手</div>
<div className="message-text streaming">
<span className="typing-indicator">...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
);
};
export default MessageList;
六、调试建议与常见坑 ⚠️
- 404:确认前端请求路径
/api/chat/stream与路由前缀一致;开发时若使用 Vite,请在vite.config.ts配置 proxy 到后端端口。 - SSE 返回 404/空响应:确认控制器里
ctx.respond = false已设置,并且在设置 header 后立即开始ctx.res.write。 - headers already sent:不要在写入原生
res后再次设置ctx.body或ctx.status。 - CORS:若跨域,确保后端 CORS 配置允许
Content-Type、Authorization等必要 header。
七、快速上手测试命令 🧪
启动后端:
# 在 backend 目录下
node src/app.js
# 或使用 nodemon
npx nodemon src/app.js
用 curl 测试流式(查看快速返回流):
curl -N -H "Content-Type: application/json" -X POST http://localhost:3001/api/chat/stream -d '{"message":"你好","conversationHistory":[]} '
你应能看到类似:
data: {"content":"你"}
data: {"content":"好"}
data: [DONE]