阅读视图

发现新文章,点击刷新页面。

PinMe:零成本三秒发布你的网站

PinMe 是一款零配置的前端部署工具。无需服务器、无需账号、无需设置。 无论是构建静态站点、使用 AI 生成页面,还是导出前端项目 — 只需一条命令或者拖动文件夹上传即可即时部署。 PinMe

ArcGIS Pro 添加底图的方式

^ 关注我,带你一起学GIS ^ 前言 之前通过Map菜单下的Basemap选项便可以直接添加底图,便捷又省力。可恶的是我的底图这里竟然是空空如也,连天地图也消失不见。 还是得自立更生啊。去网上找了一

🌰在 OpenLayers 中实现图层裁切

做地图的时候,常会给地图套个“遮罩”——把不关心的地方调暗,把想看的地方突出出来。但有时我们并不想动整张地图的视觉效果,只想把某一张图层按不规则形状展示出来,比如只在某块多边形里显示指定

ArcGIS Pro 实现影像波段合成

^ 关注我,带你一起学GIS ^ 前言 本节主要讲解如何在ArcGIS Pro中实现TIFF影像波段合成。 1. 软件环境 本文使用以下软件环境,仅供参考。 时间:2026 年 操作软件:ArcGIS

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,功能相对单一 追求极简、前瞻性实验

二、 方案详解

  1. 现代替代者: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 模式非常智能,重启速度在毫秒级。

  1. 开发者体验天花板: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连接可复用
      });
    }
    

    请谨慎使用此类代码。

  1. 官方正统: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
    }
  }
}

请谨慎使用此类代码。


四、 总结:我该选哪个?

  1. 如果你只想快速写个接口,不想折腾配置:请直接使用 tsx。它是 2026 年 nodemon 的完美继承者。
  2. 如果你在做复杂全栈项目,或者有大量的路径别名:请使用 vite-node。它能让你在 Node 端获得跟前端 React/Vue 编写时一样丝滑的 HMR 体验。
  3. 如果是为了部署生产环境:无论开发环境用什么,生产环境请务必通过 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 接口名(如 invokestream)请依据你所用的 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.bodyctx.status
  • CORS:若跨域,确保后端 CORS 配置允许 Content-TypeAuthorization 等必要 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]

❌