第一个 Node.js 服务

要点

HTTP 服务注册一个处理函数;每来一个请求,函数根据方法和地址给出响应。启动成功不证明所有路径都符合约定。

步骤一:原生 http 模块

原生 http 模块是所有 Node Web 框架的底层。先写一个 15 行的服务器:

// server-native.ts
import { createServer } from "node:http";

const server = createServer((req, res) => {
  // 每个进来的请求都会触发这个回调
  console.log(`${req.method} ${req.url}`);

  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
  res.end(JSON.stringify({ message: "Hello, Node.js!" }));
});

const PORT = 3000;
server.listen(PORT, () => {
  console.log(`服务已启动: http://localhost:${PORT}`);
});

运行与验证:

node server-native.ts
curl http://localhost:3000
1. {"message":"Hello, Node.js!"}

关键概念解析

  • 回调模型:createServer(handler) 中 handler 对每个请求执行一次;事件循环能在 I/O 等待期间继续处理其他连接,但一个耗时的同步处理器仍会阻塞同一进程。连接数量取决于文件描述符、内存、背压、下游容量和实测负载,不能由“单线程”或“异步”直接推断(原理见 04-async-promises)
  • res.writeHead + res.end:写状态码/响应头,然后结束响应体;忘记 end 浏览器会一直挂起
  • 没有路由:所有路径都进同一个回调——这就是原生模块的直接体验

原生模块处理路由的痛苦

// 试着区分路径和查询参数
import { createServer } from "node:http";

const server = createServer((req, res) => {
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);

  if (url.pathname === "/hello" && req.method === "GET") {
    const name = url.searchParams.get("name") ?? "world";
    res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
    res.end(JSON.stringify({ greeting: `你好, ${name}` }));
    return;
  }

  res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
  res.end(JSON.stringify({ error: "Not Found" }));
});

server.listen(3000);

能跑,但路径多了之后 if-else 会失控,且没有中间件、错误兜底、参数解析。这正是框架要解决的问题。

可复现验收:响应契约,不是“端口能监听”

启动日志只能说明端口已绑定,不能证明路由行为正确。下面的完整程序用端口 让操作系统分配临时端口,只在同一进程的回环地址发起三次请求;检查结束后关闭 server,因此不会占用开发机的 3000 端口。它验证成功响应的 JSON 内容类型、未知路径的 404、以及同一路径错误方法的 405。

import { createServer, request } from "node:http";

function sendJson(res, status, body) {
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
  res.end(JSON.stringify(body));
}

const server = createServer((req, res) => {
  const url = new URL(req.url ?? "/", "http://localhost");
  if (url.pathname === "/health") {
    if (req.method !== "GET") return sendJson(res, 405, { error: "Method Not Allowed" });
    return sendJson(res, 200, { ok: true });
  }
  return sendJson(res, 404, { error: "Not Found" });
});

function inspect(port, method, path) {
  return new Promise((resolve, reject) => {
    const req = request({ host: "127.0.0.1", port, method, path }, (res) => {
      let text = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => { text += chunk; });
      res.on("end", () => resolve({ status: res.statusCode, type: res.headers["content-type"], text }));
    });
    req.on("error", reject);
    req.end();
  });
}

server.listen(0, "127.0.0.1", async () => {
  try {
    const { port } = server.address();
    const health = await inspect(port, "GET", "/health");
    const missing = await inspect(port, "GET", "/missing");
    const wrongMethod = await inspect(port, "POST", "/health");
    console.log(`${health.status} ${health.type} ${health.text}`);
    console.log(`${missing.status} ${missing.text}`);
    console.log(`${wrongMethod.status} ${wrongMethod.text}`);
  } finally {
    server.close();
  }
});

预期输出:

200 application/json; charset=utf-8 {"ok":true}
404 {"error":"Not Found"}
405 {"error":"Method Not Allowed"}

这不是性能、TLS、代理或真实网络的测试;它只固定 HTTP 处理器的最小路由契约。将程序保存为 server-native.mjs 时,Node 18+ 可直接用 node server-native.mjs 运行;使用 TypeScript 则应通过项目的 TypeScript 工具链或目标 Node 版本支持的类型擦除方式运行,不能假设所有旧 Node 都能执行 .ts 文件。

步骤二:Hono 4 版本

pnpm add hono @hono/node-server
// server-hono.ts
import { Hono } from "hono";
import { serve } from "@hono/node-server";

const app = new Hono();

// 路由:方法 + 路径 + 处理器(返回 Response)
app.get("/hello", (c) => {
  const name = c.req.query("name") ?? "world";
  return c.json({ greeting: `你好, ${name}` });
});

app.get("/", (c) => {
  return c.json({ message: "Hello, Node.js!" });
});

const PORT = Number(process.env.PORT) || 3000;
serve({ fetch: app.fetch, port: PORT }, (info) => {
  console.log(`服务已启动: http://localhost:${info.port}`);
});
node --watch server-hono.ts
curl "http://localhost:3000/hello?name=Node"
1. {"greeting":"你好, Node"}

框架带来了什么

能力原生 httpHono 4
路由匹配手写 if-elseapp.get("/users/:id") 模式匹配
请求体解析手动收集 chunk + JSON.parseawait c.req.json() 内建
中间件链自己实现app.use 洋葱模型
错误处理try/catch 到处写app.onError 集中处理
响应工具writeHead/endc.json() c.text() 直接返回
请求注入测试手写构造app.request() 内置

注意请求体解析是内建的——Hono 不需要像老框架那样先挂一个 json() 中间件,处理器里 await c.req.json() 即得。

Hono 值得知道的特性

  • 运行时无关:app 基于 Web 标准 API(Request/Response),同一段代码可跑 Node、Bun、Deno、Cloudflare Workers——换运行时只改 @hono/node-server 这一层
  • 处理器返回值即响应:return c.json(data, 201),第二参就是状态码,没有 .status() 链
  • 零依赖中间件生态:hono/cors、hono/logger、hono/jwt、hono/secure-headers 全部内置(清单见 Hono 核心速查)
  • 自带测试客户端:await app.request("/hello") 直接拿到 Response,不需要第三方 HTTP 测试库

改造练习代码:带优雅退出

// server-hono.ts 最终版
import { Hono } from "hono";
import { serve } from "@hono/node-server";

const app = new Hono();
app.get("/", (c) => c.json({ ok: true }));

const server = serve({ fetch: app.fetch, port: 3000 }, () => {
  console.log("listening on :3000");
});

// 优雅退出:收到信号先停止接收新请求,再处理存量请求
process.on("SIGTERM", () => {
  server.close(() => {
    console.log("服务已优雅退出");
    process.exit(0);
  });
});

serve() 返回的就是 Node 原生 http.Server 实例——server.close()、server.on() 等 API 与原生模块完全一致。

最佳实践

服务器启动时解析并校验端口等配置,给出缺失或非法值的明确错误。开发监听用于缩短反馈,生产由部署平台或监督器管理进程生命周期;重点是退出、重启和日志行为,而不是禁止命令本身。

关闭时先停止接收新请求,再等待在途工作并设置最长等待时间,最后释放资源。请求处理中的长同步计算会阻塞事件循环,用一个慢请求和一个轻请求同时测试即可观察影响。

常见问题

Q1: EADDRINUSE: address already in use :3000?

A: 端口被占用。lsof -i :3000(macOS/Linux)找到进程并结束,或换端口 PORT=3001 node server-hono.ts。

Q2: curl 请求 404,但路径明明写对了?

A: 检查请求方法是否匹配(GET vs POST);子应用挂载前缀重复(/users 里又写了 /users/:id)也会导致路径对不上。

练习与实践

练习一:原生 vs 框架

任务要求: 1. 实现原生版本 /time 接口,返回当前 ISO 时间 2. 在 Hono 版本中复刻同样功能 3. 对比两份代码行数与可读性,写下 3 条观察

练习二:迷你路由器

挑战任务: – 在原生 http 版本上实现一个支持 /users/:id 的极简路由函数(约 20 行) – 思考:参数解析、404 兜底、嵌套路由分别如何处理?

提示: 可用 URLPattern 或正则提取路径参数。


相关文档

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇