← TTY 模块 NestJS →

Node.js HTTP服务器搭建教程:Web应用四层架构与静态文件服务实现

著
原创 2026-10-13 Node.js 已有人查阅

Web服务器到底在做什么

浏览器地址栏敲下回车,到页面呈现在屏幕上,中间有一个角色始终在默默工作——Web服务器。它本质上是一段软件程序,监听某个端口,接收HTTP客户端(浏览器、移动端App、爬虫脚本等)发来的HTTP请求,解析请求行、请求头、请求体,然后决定返回什么内容。

返回的内容通常不只是一份HTML文档。现在页面往往同时拉取样式表、脚本文件、图片、字体、接口数据,这些请求都会落到Web服务器上。服务器需要根据请求的URL路径,找到对应的资源,附带正确的Content-Type响应头,再写回响应体。

有一类请求服务器自己处理不了:需要查数据库、做复杂计算、调用第三方接口。这时候Web服务器会把请求转交给应用服务器(ApplicationServer),由后者完成业务逻辑,再把结果回传给Web服务器,最终由Web服务器封装成HTTP响应发给客户端。Apache是这类传统Web服务器的代表,开源、稳定、模块丰富,至今仍大量部署在production环境中。

Web应用的四层结构

把一套Web应用拆开看,可以分成四个层次,每层职责边界清晰:

客户端层:浏览器、移动端WebView、原生App、Postman这类HTTP客户端。它们发起请求,等待响应,负责渲染或消费数据。

服务器层:Web服务器所在的位置。它拦截客户端请求,做反向代理、负载均衡、静态资源缓存、TLS终止,然后把动态请求转发给下一层。

业务层:应用服务器。真正的业务逻辑在这里执行——订单校验、权限判断、数据聚合。它通过数据库驱动或HTTP客户端与数据层通信。

数据层:数据库、缓存、对象存储、消息队列,以及任何提供数据的外部系统。

这个分层不是理论摆设。实际排障时,先定位问题出在哪一层,能省掉大量无效排查。比如页面404,可能是服务器层路径映射错了;接口500,多半在业务层;数据不一致,往往要往数据层查。

Node.js的http模块能做什么

Node.js内置的http模块同时提供HTTP客户端和服务器能力。创建服务器用http.createServer(),传入一个回调函数,回调接收request和response两个对象。

request是http.IncomingMessage的实例,包含请求方法、URL、请求头、客户端IP等信息。response是http.ServerResponse的实例,用来设置状态码、响应头,并写入响应体。

配合fs模块读取文件、url模块解析请求路径,就能实现一个最基础的文件服务器。下面这份代码是很多Node.js教程的起点,但其中藏着不少值得推敲的细节。

一个可运行的静态文件服务器

新建server.js:

var http = require('http');
var fs = require('fs');
var url = require('url');

http.createServer(function (request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

    fs.readFile(pathname.substr(1), function (err, data) {
        if (err) {
            console.log(err);
            response.writeHead(404, {'Content-Type': 'text/html'});
        } else {
            response.writeHead(200, {'Content-Type': 'text/html'});
            response.write(data.toString());
        }
        response.end();
    });
}).listen(8081);

console.log('Server running at http://127.0.0.1:8081/');

同目录下新建index.html:

<html>
<head>
<title>Sample Page</title>
</head>
<body>
Hello World!
</body>
</html>

命令行执行nodeserver.js,浏览器打开http://127.0.0.1:8081/index.html,就能看到页面内容。

这段代码能跑通,但距离一个能用的静态服务器还有距离。下面逐条说问题。

几个容易被忽略的坑

路径解析的隐患。url.parse()在新版Node.js中已被标记为遗留API,推荐用newURL(request.url,'http://'+request.headers.host)替代。更关键的是,pathname.substr(1)直接拼接文件路径,存在目录穿越风险。请求/../../etc/passwd这类路径时,服务器可能读到项目目录之外的文件。正确做法是用path.join()拼接根目录,再用path.resolve()校验最终路径是否仍在允许范围内。

Content-Type写死了。所有文件都返回text/html,浏览器拿到CSS或JS文件时无确解析。实际项目中需要根据文件扩展名映射MIME类型,至少覆盖.html、.css、.js、.json、.png、.jpg、.svg、.woff2这些常见格式。

同步阻塞与异步错误。fs.readFile是异步的,这点没问题。但data.toString()对二进制文件(如图片)会破坏原始字节。应该直接response.write(data),或者用流式写法fs.createReadStream(filePath).pipe(response),后者内存占用更低,大文件场景优势明显。

没有处理请求方法。上面的代码对POST、PUT、DELETE一视同仁,都去读文件。生产环境需要判断request.method,非GET/HEAD请求应当返回405。

缺少缓存控制。静态资源没有Cache-Control或ETag,浏览器每次都要重新下载。加上Cache-Control:public,max-age=3600能显著减少重复请求。

现在工程中的惯用写法

直接用http模块裸写服务器,在教学场景下有助于理解底层机制。但真实项目里,通常会选择Express、Koa、Fastify这类框架,或者用Nginx托管静态资源,Node.js只负责API。

以Express为例,静态文件服务一行搞定:

app.use(express.static('public'));

框架内部做了MIME映射、路径安全校验、缓存头设置、Range请求支持。自己实现这些不是不可以,但要考虑投入产出比。

如果确实需要用原生模块,建议用fs.promises配合async/await,代码可读性更好,也便于统一错误处理:

const fs = require('fs').promises;
const path = require('path');

async function serveFile(filePath, response) {
    try {
        const data = await fs.readFile(filePath);
        response.writeHead(200, { 'Content-Type': 'text/html' });
        response.end(data);
    } catch (err) {
        response.writeHead(404, { 'Content-Type': 'text/plain' });
        response.end('Not Found');
    }
}

错误处理与类型安全

Node.js的回调风格容易导致错误被吞掉。fs.readFile的err参数必须检查,不能想当然认为文件一定存在。未捕获的异常会触发uncaughtException,进程直接退出。合理的做法是在服务器层面加一层兜底:

process.on('uncaughtException', (err) => {
    console.error('Uncaught Exception:', err);
});

但更推荐用try/catch或Promise的.catch()在具置处理,不要依赖全局兜底。全局兜底只适合记录日志后优雅退出,不适合让进程继续带病运行。

TypeScript项目中,http.IncomingMessage和http.ServerResponse的类型定义由@types/node提供。请求对象的url属性类型是string|undefined,直接使用前需要判空。这类类型约束能在编译期拦掉一批低级错误。

本节课程知识要点

  • Web服务器负责接收HTTP请求并返回响应,动态请求可转交应用服务器处理。

  • Web应用可分为客户端层、服务器层、业务层、数据层,排障时先定位层次。

  • Node.js的http模块可创建HTTP服务器,配合fs、url模块实现文件服务。

  • 路径拼接必须做安全校验,防止目录穿越。

  • Content-Type应根据文件类型动态设置,不能写死。

  • 二进制文件不要用toString()转换,优先使用流式传输。

  • 生产环境建议用框架或Nginx处理静态资源,原生模块适合学习与轻量场景。

练习与思考

  1. 把示例中的url.parse()替换为newURL(),并处理解析失败的情况。

  2. 为服务器添加MIME类型映射表,至少支持HTML、CSS、JS、JSON、PNG五种格式。

  3. 用fs.createReadStream()改写文件读取逻辑,观察大文件传输时的内存占用变化。

  4. 思考:如果请求路径是/,应该返回什么?如何优雅地处理默认首页?

  5. 在Express中实现同样的静态文件服务,对比两种写法的代码量与可维护性。

← TTY 模块 NestJS →
分享笔记 (共有 篇笔记)
验证码:
微信公众号