Node.js 实战项目

学了 12 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可运行的项目——HTTP 静态服务器、REST API、CLI 文件处理,分别覆盖 Node.js 三大主力场景:Web 服务、接口 API、命令行工具。三个项目都零依赖(只用内置模块),复制到本地 node xxx.js 就能跑。每个项目含完整代码、运行结果和要点讲解。建议你亲手跑一遍,再改改需求,这才是真正"上手"。

实战 1:HTTP 静态文件服务器(http + fs + MIME)

需求:不用任何框架,用内置 http + fs 写一个静态文件服务器——能把指定目录下的 HTML/CSS/JS/图片按正确 Content-Type 返回,访问根路径自动找 index.html。支持 node server.js ./public 8080 传参。

涉及知识:http.createServer、fs.readFile 回调、path.join/resolve、URL 解析、MIME 映射、路径遍历防护、process.argv。

// static-server.js — 极简静态文件服务器
// 运行:node static-server.js ./public 8080
const http = require("http");
const fs = require("fs");
const path = require("path");

const ROOT = process.argv[2] || ".";
const PORT = parseInt(process.argv[3] || "8080", 10);

// 扩展名 → MIME 类型映射
const MIME = {
  ".html": "text/html; charset=utf-8",
  ".css":  "text/css; charset=utf-8",
  ".js":   "application/javascript",
  ".json": "application/json",
  ".png":  "image/png",
  ".jpg":  "image/jpeg",
  ".svg":  "image/svg+xml",
};

const server = http.createServer((req, res) => {
  // 1. 取路径;decodeURIComponent 处理 URL 里的中文/空格
  let urlPath = decodeURIComponent(req.url.split("?")[0]);
  if (urlPath === "/") urlPath = "/index.html";

  // 2. path.join 拼绝对路径;检查仍在 ROOT 内,防路径遍历攻击(../)
  const rootAbs = path.resolve(ROOT);
  const filePath = path.join(rootAbs, urlPath);
  if (!filePath.startsWith(rootAbs)) {
    res.writeHead(403);
    return res.end("Forbidden");
  }

  // 3. 读文件;按扩展名定 Content-Type
  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.writeHead(404);
      return res.end(`404 Not Found: ${urlPath}`);
    }
    const mime = MIME[path.extname(filePath)] || "application/octet-stream";
    res.writeHead(200, { "Content-Type": mime });
    res.end(data);
  });
});

server.listen(PORT, () => {
  console.log(`静态服务器已启动: http://localhost:${PORT}`);
  console.log("按 Ctrl+C 退出");
});
// 输出:
// 静态服务器已启动: http://localhost:8080
// 按 Ctrl+C 退出
// 浏览器访问 http://localhost:8080/ 即可看到 ./public/index.html

要点讲解:

实战 2:REST API(纯 http + JSON + 路由)

需求:用纯 http 模块(不用 Express)写一个商品 REST API——GET /products 列全部、GET /products?id=3 取单条、POST /products 新增。所有响应都是 JSON,正确设置状态码。

涉及知识:new URL() 解析、res.writeHead 设状态码头、JSON.stringify、请求 body 流式读取(用 Promise 包装)、HTTP 状态码。

// api.js — 零依赖 REST API:用纯 http 模块写商品增删查
// 运行:node api.js
// 测试:curl http://localhost:3000/products
const http = require("http");

// 内存数据(真实项目接数据库)
const products = [
  { id: 1, name: "苹果", price: 5.0 },
  { id: 2, name: "香蕉", price: 3.0 },
  { id: 3, name: "橙子", price: 4.0 },
];

// 统一 JSON 响应;JSON.stringify 第三个参数控制缩进
function sendJSON(res, data, code = 200) {
  res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
  res.end(JSON.stringify(data));
}

// 把请求 body 读成字符串(Promise 包装,配合 async/await)
function readBody(req) {
  return new Promise(resolve => {
    let data = "";
    req.on("data", chunk => data += chunk);
    req.on("end", () => resolve(data));
  });
}

const server = http.createServer(async (req, res) => {
  // URL 对象:方便解析 pathname 和 searchParams
  const url = new URL(req.url, `http://${req.headers.host}`);
  const { method } = req;

  if (url.pathname === "/products") {
    if (method === "GET") {
      // 取单条 ?id=3
      const id = url.searchParams.get("id");
      if (id) {
        const p = products.find(x => x.id === parseInt(id, 10));
        return p ? sendJSON(res, p)
                 : sendJSON(res, { error: `未找到 id=${id}` }, 404);
      }
      return sendJSON(res, products);
    }
    if (method === "POST") {
      const body = JSON.parse(await readBody(req));
      body.id = products.length + 1;
      products.push(body);
      return sendJSON(res, body, 201);
    }
  }
  sendJSON(res, { error: "路由不存在" }, 404);
});

server.listen(3000, () => console.log("API 已启动: http://localhost:3000"));

// 测试(另开终端):
// $ curl http://localhost:3000/products
// [{"id":1,"name":"苹果","price":5},{"id":2,"name":"香蕉","price":3},{"id":3,"name":"橙子","price":4}]
// $ curl 'http://localhost:3000/products?id=2'
// {"id":2,"name":"香蕉","price":3}
// $ curl -X POST http://localhost:3000/products \
//        -H 'Content-Type: application/json' -d '{"name":"葡萄","price":8}'
// {"id":4,"name":"葡萄","price":8}

要点讲解:

实战 3:CLI 文件处理(fs + path + 命令行参数)

需求:写一个命令行工具,递归扫描目录下所有 .txt 文件,统计每个文件和总计的行数、词数、字符数。支持 node wordcount.js ./docs 传参。

涉及知识:fs.promises 异步 API、path.join/basename、递归遍历目录、process.argv、async/await、fs.readdir({ withFileTypes: true })。

// wordcount.js — 递归扫描目录下的 .txt 文件,统计行/词/字符数
// 运行:node wordcount.js ./docs
const fs = require("fs").promises;   // 用 promise 版,可 async/await
const path = require("path");

// 递归遍历目录,返回所有文件的完整路径
async function walk(dir) {
  let results = [];
  for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      results = results.concat(await walk(full));   // 目录:递归
    } else {
      results.push(full);                            // 文件:收集
    }
  }
  return results;
}

async function main() {
  // 1. 从命令行参数取目录
  const root = process.argv[2] || ".";
  const files = (await walk(root)).filter(f => f.endsWith(".txt"));
  console.log(`扫描到 ${files.length} 个 .txt 文件`);

  // 2. 逐个统计;累加总计
  let totalLines = 0, totalWords = 0, totalChars = 0;
  for (const file of files) {
    const content = await fs.readFile(file, "utf-8");
    const lines = content.split("\n").length;             // 按换行符数行数
    const words = content.trim().split(/\s+/).filter(Boolean).length;  // 按空白分词
    const chars = content.length;
    totalLines += lines;
    totalWords += words;
    totalChars += chars;
    console.log(`  ${path.basename(file)}: ${lines} 行, ${words} 词, ${chars} 字符`);
  }

  // 3. 输出汇总
  console.log(`合计: ${totalLines} 行, ${totalWords} 词, ${totalChars} 字符`);
}

main().catch(err => {
  console.error("出错:", err.message);
  process.exit(1);
});
// 运行:node wordcount.js ./docs
// 输出:
// 扫描到 3 个 .txt 文件
//   readme.txt: 42 行, 318 词, 2047 字符
//   notes.txt: 15 行, 96 词, 612 字符
//   todo.txt: 8 行, 52 词, 311 字符
// 合计: 65 行, 466 词, 2970 字符

要点讲解:

小结

三个项目分别对应 Node.js 三大主力场景:Web 服务(静态文件)、API 服务(JSON 接口)、CLI 工具(自动化脚本)。它们都用到了内置 http/fs/path——这是 Node 的"地基",所有框架(Express/Koa/Fastify)都建在上面。建议至少把其中一个改造成自己的版本(给静态服务器加缓存、给 API 接 SQLite、给 CLI 加 Markdown 支持和 HTML 输出),做过一遍比读十遍更扎实。

恭喜!到这里 Node.js 系列就真正完结了。接下来挑一个方向深入——Web 框架(Express / Koa / NestJS)、数据库集成(Prisma / TypeORM)、实时通信(Socket.io / WebSocket)、或前端工具链(Vite / Webpack 内部都是 Node)——边做边学,进步最快。

← 上一篇 Node.js 调试

返回 Node.js 教程目录

✈️💬