http.createServer() 用一个函数(请求处理函数)创建 Web 服务器。每个请求到来时,Node 都会用两个对象调用它:req(客户端发来的请求方法、URL 和请求头)和 res(你要发回的内容)。在 res 上设置状态码和响应头,然后带着响应体调用 res.end()。server.listen(3000) 开始在 3000 端口上接受连接。
第一个请求的响应是 You asked for GET /hello?name=Ada,控制台也会打印同样的请求方法和 URL。从请求栏发送 DELETE /anything:同一个处理函数会响应它,因为普通的 http 服务器没有路由。所有请求都经过这一个函数。
发送 JSON 并设置状态码
res.writeHead(status, headers) 一次调用就能设置状态码和响应头。发送 JSON 时,设置 Content-Type: application/json,然后发送 JSON.stringify() 的结果;res.end() 只接受字符串或 Buffer。
GET /users?limit=1 返回 [{"id":1,"name":"Ada"}]。req.url 是路径加查询字符串,所以用 new URL(req.url, 'http://localhost') 把它拆分成 pathname 和 searchParams(加上基础地址只是因为 URL 需要一个绝对地址)。发送 GET /nothing,状态码就会变成 404。
用 switch 做路由
一个小型 API 只需要对请求方法和路径做一次 switch。拼出 'GET /users' 这样的键,再给每个路由一个自己的 case:
发送 POST /users,请求体为 {"name": "Grace"}:响应是 201 和 {"id":2,"name":"Grace"},现在 GET /users 会列出两个用户。用请求体 hello 发送同样的 POST,你会得到 400 和 {"error":"Body must be JSON"}。
读取 POST 请求体
Node 不会替你解析请求体。req 是一个可读流,请求体会通过 data 事件分块到达,全部到齐后触发 end。上面的 readBody() 把这些块收集成一个字符串,并返回一个 Promise,这样处理函数就可以 await 它。由于 req 是异步可迭代对象,下面这个循环也能完成同样的工作:
let data = '';
for await (const chunk of req) data += chunk;
const body = JSON.parse(data);
真实的服务器还会限制大小(一旦 data.length 超过上限,就停止读取并返回 413),并在调用 JSON.parse 之前检查 req.headers['content-type']。
常见错误
忘记调用 res.end()。 某个分支设置了状态码却没有结束响应,客户端就会一直等到超时。
在响应体之后设置响应头。 一旦 res.end() 或第一次 res.write() 运行过,响应头就已经发出,res.setHeader() 会抛出 ERR_HTTP_HEADERS_SENT。像示例那样,提前发送响应时使用 return。
拿 req.url 和路径比较。 req.url 包含查询字符串,所以对于 /users?limit=1,req.url === '/users' 为 false。应该比较 pathname。
什么时候改用 Express
做健康检查、接收 webhook 或学习 HTTP 的工作原理,http 模块就够了。一旦你需要 /users/:id 这样的路由参数、JSON 请求体解析、中间件和错误处理,你最后会自己写出一个小框架。Express 就是这样一个框架,它基于同样的 req 和 res 对象构建;Express 路由 用 app.get() 和 app.post() 展示了同样的 API。
常见问题
res.write 和 res.end 有什么区别?
res.write() 发送响应体的一块内容,并保持响应打开,所以可以多次调用。res.end() 发送可选的最后一块内容,并结束响应。每个响应都需要恰好一次 res.end()。
如何停止 Node HTTP 服务器?
调用 server.close()。它会立即停止接受新连接,并在已打开的连接都结束后运行回调,这正是收到 SIGTERM 时优雅关闭所需要的。
如何在 http 模块中使用 HTTPS?
改用 https 模块:https.createServer({ key, cert }, handler) 接收同样的处理函数,外加一个私钥和一个证书。在生产环境中,通常由 Nginx 这样的代理或托管平台终止 TLS,你的 Node 服务器继续监听普通 HTTP。
Node 服务器应该监听哪个端口?
任何 1024 以上的空闲端口都不需要管理员权限;开发时常用 3000 和 8080。托管平台通常会通过环境变量告诉你端口,所以写成 server.listen(process.env.PORT || 3000)。