1. 为什么要专门花一天搞懂HTTP和原生Node服务
大概每个写前端或者刚开始接触后端的朋友都有过这种阶段:框架用得很顺手,接口也调得通,但一旦遇到跨域、浏览器缓存、大文件上传、接口鉴权这类问题就懵了。你搜了一圈资料,帖子都写得云里雾里,最后只能改个配置碰运气,过了就过,不过就继续搜。
其实问题的根源不在框架,而在HTTP本身。
HTTP协议就是浏览器和服务器之间对话的语法和规矩。你写的每一个接口、每一次fetch请求、浏览器里每一个资源的加载,骨子里都在走这套协议。框架只是帮你把规矩封装好了,让你少写几行代码,但规矩本身从来没变过。
Node.js 原生服务就是把这层封装拆开,让你直接用最底层的http模块创建服务器、接收请求、返回响应。它没有路由,没有中间件,没有模板引擎,什么都没有,但恰恰是因为什么都没有,你才能看见HTTP请求从进入到离开的完整路径。
我记得第一次用http.createServer跑起来一个服务的时候,最大的感受是:原来我平时提交表单、加载图片、请求接口,背后发生的是一整套这么明确的过程。浏览器把你要访问的地址、要带的参数、要用的方法打包成一段符合约定的文本,通过网络送到服务器,服务器解析完,再把结果打包成另一段文本送回来。整个过程有来有回,通通是明文可见的。
Day 6 这个主题刚好卡在从"会用工具"到"理解原理"的分界线上。这篇内容适合两类人:一是刚学完JavaScript基础、准备往后端迈进的新手,二是已经在用框架、但想补一下底层原理的开发者。看完你能自己用原生代码稀里哗啦写出一个支持路由、能返回HTML和JSON的小服务,顺便把HTTP的核心知识点串成一条线。
2. 一次请求的背后:HTTP的核心脉络
HTTP这东西,拆开来看其实特别直白。它规定了客户端和服务端怎么对话:客户端先开口,服务端听完再回话。一次完整的HTTP事务,说穿了就是这么一件事。
2.1 一次请求到响应的完整过程
你在浏览器地址栏输入一个网址,按下回车,背后发生的事情是这样的:
- 浏览器解析URL,拆出协议、域名、端口、路径和查询参数。比如
http://localhost:3000/user?id=123,协议是http,域名是localhost,端口是3000,路径是/user,查询参数是id=123。 - 浏览器根据域名找到服务器的IP地址。本地开发时就是
127.0.0.1,线上环境则要通过DNS解析(DNS解析是另一个话题,先不展开)。 - 浏览器与服务器建立TCP连接。这个连接是一条可靠的传输通道,HTTP报文就是在这条通道里传递的。用不严谨但好懂的话说:TCP保证了数据能完整到达,HTTP规定了到达后怎么理解。
- 连接建立后,浏览器把请求报文通过这个连接发过去。报文里包含请求行、请求头、请求体,对应着你要做什么、你希望对方怎么回复。
- 服务器收到报文,解析出方法和路径,执行对应的业务逻辑(查数据库、读文件、单纯算个结果),然后生成一份响应报文返回。
- 浏览器收到响应报文,根据状态码判断这次请求是否成功,再根据响应头里的
Content-Type决定怎么处理响应体——是渲染成页面,还是解析成JSON数据,还是直接下载成文件。
这套流程跟你在两台电脑之间手动发消息没什么本质区别:你先说"我要xxx",对方回答"好的,给你"或"不行,因为xxx"。只是HTTP把"说"和"答"变成了固定格式的文本。
2.2 请求方法不是花架子
HTTP定义了一组请求方法,本质上是动词,代表客户端想让服务器做什么事。最常用的就这几个:
| 方法 | 语义 | 典型场景 |
|---|---|---|
| GET | 获取资源 | 打开页面、拉取列表、加载图片 |
| POST | 提交数据/创建资源 | 提交表单、发布文章、上传文件 |
| PUT | 整体替换资源 | 更新一条完整记录 |
| PATCH | 部分更新资源 | 只修改某个字段 |
| DELETE | 删除资源 | 删除一条记录 |
新手容易混的就是GET和POST。我见过很多同学把删除操作写成GET,把查询操作写成POST,功能上确实都能跑通,但语义上很别扭。GET请求应该只做读取,不应该对服务器数据产生修改,因为GET可以被浏览器预加载、被搜索引擎爬取、被缓存在本地。如果你把删除操作放在GET里,一个爬虫路过就可能把数据删了,这个事故我亲眼见过。POST则相反,它专门用来传输数据,不适合暴露在URL里,也不适合被缓存。
2.3 白话讲"无状态"
每次说HTTP无状态,新手都会问:既然无状态,那登录状态是怎么保持的?
打个比方。HTTP协议就像一家快餐店,服务员不记得你上次点了什么,你每次来都要重新点一次餐。第一次来你说"要一份汉堡",服务员给你;第二次你说"要一份可乐",服务员根本不知道你刚才吃过汉堡。服务器处理完一个请求,就把这次对话忘得一干二净,不会自动记住你的任何信息。
那网站的"登录状态"怎么来的?靠的不是HTTP记住你,而是服务器和浏览器私底下约定好的一种"凭证机制":你登录成功后,服务器发一个凭证(就是Cookie里存的那个Session ID或者Token),浏览器收下,下次请求的时候主动把这个凭证带上。服务器看见凭证,就知道"哦,这是之前登录过的那个用户"。
每次请求都带上凭证这件事,恰恰证明了HTTP本身是不记得你的,请求之间互相独立。这一点理解了,你后面看Session、JWT、OAuth这些东西就会顺畅很多。
3. 报文:HTTP传输的真正实体
报文是HTTP最核心的概念。浏览器和服务器之间传的不是抽象"数据",而是一段符合特定格式的文本。这段文本叫HTTP报文,分请求报文和响应报文两类。
3.1 请求报文的四段结构
一个完整的请求报文长这样:
POST /api/login HTTP/1.1 Host: localhost:3000 Content-Type: application/json Content-Length: 27 Connection: keep-alive {"username":"admin","password":"123456"}四段结构拆开看:
- 请求行(第一行):包含方法、路径和协议版本。
POST /api/login HTTP/1.1表示用POST方法请求/api/login,协议版本是HTTP/1.1。 - 请求头(第二行到空行之前):一组键值对,每行一个,告诉服务器各种附加信息。
Host是目标主机,Content-Type是请求体的格式,Content-Length是请求体有多长,Cookie是浏览器存的凭证,User-Agent是浏览器身份标识。 - 空行:报文头结束的标志。别小看这一行,解析器就是靠它区分"头部"和"主体"的。
- 请求体(空行之后):实际的载荷数据。GET请求一般没有请求体,POST、PUT这些方法才会有。
用http模块写原生服务时,你能最直观地体会到这个过程:请求头一行一行解析出来放到req.headers对象里,请求体通过监听req的data事件一段一段接收,拼完之后才是完整文本。
3.2 响应报文的四段结构
响应报文的结构和请求报文类似,也是四段:
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 13 Connection: keep-alive <h1>Hello</h1>区别主要在起始行。请求报文的起始行是"方法+路径+版本",响应报文的起始行是"版本+状态码+状态描述"。HTTP/1.1 200 OK表示这是HTTP/1.1协议的响应,状态码200意味着成功,OK是状态码的英文描述。
响应头里的Content-Type尤其重要。浏览器收到响应后会看这个字段,决定怎么处理响应体。如果我返回的是text/html,浏览器就把内容当HTML渲染;如果是application/json,浏览器就把内容当数据结构处理,比如fetch的.json()方法。
3.3 状态码:服务器的"三言两语"
状态码是服务器告诉客户端请求结果的方式,一共五大类,记住规律就行:
| 状态码范围 | 含义 | 常见例子 |
|---|---|---|
| 100-199 | 信息性响应 | 100 Continue |
| 200-299 | 成功 | 200 OK、201 Created、204 No Content |
| 300-399 | 重定向 | 301 Moved Permanently、302 Found、304 Not Modified |
| 400-499 | 客户端错误 | 400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found |
| 500-599 | 服务器错误 | 500 Internal Server Error、503 Service Unavailable |
很多新手遇到404就只想到"路径不存在",其实404也可能是因为服务器没有设置对应的路由,或者静态资源文件名写错了。看到403,第一反应不该是"路径对不对",而应该是"权限够不够"。状态码是排查问题的最快线索,养成看状态码的习惯能省十倍调试时间。
304这个状态码值得单独说下。它表示"资源没变,直接用浏览器缓存的吧"。浏览器缓存机制就是靠304 +ETag或Last-Modified实现的:服务器第一次返回资源时带上版本标识,浏览器存起来;下次请求时带上这个标识,服务器一看没变,就返回304和一个空响应体,省了几百KB流量。原生服务里手动实现这套逻辑很繁琐,但理解了之后再看框架里的配置就通透得多。
4. 上手实战:用原生http模块搭一个Web服务
理论说了一大堆,该动手了。这一步的目标是:不用框架,只用Node.js自带的http模块写一个能跑起来的服务,然后逐步加功能,在这个过程中把前文的理论对上号。
4.1 最小可运行服务
先来一个最简单的版本。新建一个文件叫server.js,写上:
const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Hello, HTTP'); }); server.listen(3000, () => { console.log('Server running at http://localhost:3000'); });在终端执行node server.js,然后浏览器打开http://localhost:3000,页面上会显示Hello, HTTP。就这几行代码,一个完整的HTTP服务已经跑起来了。
解读一下这段代码对应HTTP的哪些环节:
http.createServer传入的回调函数,会在每次收到请求时被调用。回调的两个参数req和res,就是请求报文的解析结果和响应报文的构建工具。req是个可读流,上面挂着请求相关的所有信息(方法、URL、请求头)。res是个可写流,你往里面写的内容会变成响应报文发送给客户端。server.listen(3000, callback)让服务器监听3000端口。端口可以随便换,但注意别和其他进程冲突。
这里有个细节值得注意:回调函数里的req和res实际上就是Node对原始HTTP报文的一层封装。Node已经把报文解析好了,把请求头放在req.headers对象里,请求行解析成req.method、req.url这些属性。你再也不用自己一行一行去解析字符串了,拿现成的就行。
4.2 解析请求里的信息
接着在上面的回调函数里加几行代码,看看能拿到什么:
const server = http.createServer((req, res) => { console.log('请求方法:', req.method); console.log('请求路径:', req.url); console.log('请求头:', req.headers); res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Hello, HTTP'); });保存后重启服务,在浏览器里刷新页面,终端会输出这次请求的方法、路径和一大堆请求头字段。你会发现光是浏览器自己带上的头就有Host、Connection、User-Agent、Accept、Accept-Encoding等等,好大一串。
req.url这个字段比较特殊。它是带查询参数的完整路径,比如你访问http://localhost:3000/user?id=1&name=admin,req.url的值是/user?id=1&name=admin。想拿到干净的路径和查询参数,需要用Node内置的url模块解析:
const url = require('url'); const server = http.createServer((req, res) => { const parsedUrl = new URL(req.url, 'http://localhost:3000'); console.log('纯路径:', parsedUrl.pathname); console.log('查询参数:', parsedUrl.searchParams.get('id')); res.end('Hello'); });new URL(req.url, 基准地址)会把相对路径和基准地址拼成完整URL,然后就能用pathname和searchParams了。这个方法在平时用框架时都是被封装好的,但这里自己解析一次,理解就深了一层。
4.3 根据路由返回不同内容
原生服务没有路由,但路由本质就是"根据路径执行不同逻辑"。手动写好判断即可:
const server = http.createServer((req, res) => { const { pathname } = new URL(req.url, 'http://localhost:3000'); if (pathname === '/' || pathname === '/index.html') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<h1>首页</h1><p>欢迎来到Day 6</p>'); } else if (pathname === '/about') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<h1>关于</h1><p>这是一篇HTTP原理与Node原生服务的博客</p>'); } else if (pathname === '/api/users') { res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify([ { id: 1, name: '张三', role: 'admin' }, { id: 2, name: '李四', role: 'user' } ])); } else { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('404 Not Found'); } });在这个例子里,/和/index.html返回HTML,/api/users返回JSON,其他路径统一返回404。注意响应头里的Content-Type不同,浏览器处理方式就不同——这就是前面讲的响应报文的作用。
JSON返回时用的是res.end(JSON.stringify(...))。JSON.stringify把JavaScript对象转成JSON字符串,然后作为响应体发送。这步不写的话,浏览器收到的是一个对象的内存表示,无法正确解析。
4.4 处理POST请求与请求体
处理GET请求只需要看req.url和req.headers就够了,但处理POST请求必须读取请求体。请求体是流式到达的,不能指望一口气拿全。正确的写法是监听data事件一段一段收集,end事件触发后再统一处理:
const server = http.createServer((req, res) => { const { pathname } = new URL(req.url, 'http://localhost:3000'); if (req.method === 'POST' && pathname === '/api/users') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { console.log('收到的请求体原文:', body); const parsed = JSON.parse(body); console.log('解析后的对象:', parsed); res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ code: 0, message: '用户创建成功', data: parsed })); }); } else { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not Found'); } });这段代码里有几个关键点:
req是流对象,data事件会触发多次,每次传来的chunk是Buffer或字符串片段。用body += chunk拼接时要注意编码,如果内容是UTF-8文本,这里没问题;如果是二进制文件,需要用Buffer.concat来处理。req.on('end')在请求体全部接收完毕后触发,此时body变量才是完整内容。- 解析JSON用
JSON.parse。如果客户端发的不是合法JSON,这里会直接抛异常,让进程崩溃。真实项目中必须用try-catch包起来。 - 响应状态码用201,语义是"资源创建成功",比200更准确。
用curl测试这个接口:
curl -X POST http://localhost:3000/api/users \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'终端会输出收到的请求体,浏览器或curl会收到刚才构造的JSON响应。整个流程走下来,你对"发送请求-读取请求体-返回响应"这条链路就有了肌肉记忆。
5. 从单文件到小项目:补全真实服务的必需能力
一个只返回固定字符串的服务没什么实际用途。真实场景里,服务需要读取文件、处理静态资源、应对各种异常。这一节把这些必需能力一个个补上。
5.1 搭建目录结构与静态资源服务
先设计一下目录结构:
my-server/ ├── server.js ├── public/ │ ├── index.html │ ├── style.css │ └── app.js └── package.jsonpublic目录放静态文件。浏览器访问http://localhost:3000/index.html时,服务器需要把public目录下的对应文件读出来,加上正确的Content-Type返回给浏览器。
先看一个能用的静态文件服务写法:
const http = require('http'); const fs = require('fs'); const path = require('path'); const server = http.createServer((req, res) => { const { pathname } = new URL(req.url, 'http://localhost:3000'); // 防止路径穿越攻击,不能允许 .. 这种路径 const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, ''); const filePath = path.join(__dirname, 'public', safePath); fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('404 Not Found'); return; } // 根据扩展名设置Content-Type const ext = path.extname(filePath).toLowerCase(); const contentType = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.png': 'image/png', '.jpg': 'image/jpeg' }[ext] || 'application/octet-stream'; res.writeHead(200, { 'Content-Type': contentType }); res.end(data); }); }); server.listen(3000);这段代码做了一件容易被忽略但很重要的事:防止路径穿越。path.join(__dirname, 'public', safePath)会把用户访问的路径拼接到public目录下,如果用户访问的是http://localhost:3000/../server.js,URL解析后的pathname会包含..,直接拼接可能导致服务器源码被读取。path.normalize和正则替换是简单有效的防线。
你可能会发现,这段代码没有写任何业务逻辑,只是把文件读出来返回。但这就够了——浏览器本身处理HTML、CSS、JS的能力已经很强大,服务器只需要把正确文件找出来送回去。
5.2 用path模块跨平台处理路径
path模块是Node原生服务里不可或缺的工具。它的核心作用就是处理路径字符串,而且能自动适配Windows和Linux的差异。
几个高频方法:
path.join('public', 'index.html'):拼接路径,自动处理分隔符。在Linux上结果是public/index.html,在Windows上结果是public\index.html。path.extname('index.html'):获取扩展名,结果是.html。path.basename('/a/b/c.html'):获取文件名,结果是c.html。path.dirname('/a/b/c.html'):获取目录名,结果是/a/b。path.normalize('/a//b/../c'):规范化路径,结果是/a/c。
写原生服务时,所有涉及文件路径的地方都用path模块处理,别用手动拼字符串。手动拼分两种情况出问题:一是跨平台分隔符不同,二是一些特殊字符(空格、中文)处理困难。用path.join一次性解决。
5.3 用nodemon让你免于手动重启的折磨
写原生Node服务有个痛点:每次改代码都要手动停掉进程再重新node server.js,非常繁琐。我强烈建议你在这一天就把nodemon用起来。
安装很简单:
npm install -g nodemon然后启动命令从node server.js改成nodemon server.js。之后每次保存代码,进程会自动重启,省掉大量重复操作。顺便说一句,nodemon只是监测文件变化然后重启进程,它本身不参与HTTP协议的任何一个环节,属于纯开发工具。
5.4 为什么原生服务仍然值得你亲手写一遍
有个问题值得想清楚:现在框架遍地都是,Express的res.send()一行就能返回JSON,为什么还要费劲手写res.writeHead和fs.readFile?
我的看法是:原生写法是理解框架的基础,也是排查问题的底牌。你用框架时看到一个报错说Content-Type不对,如果你没亲手用res.writeHead设置过响应头,你根本不知道这个字段是怎么产生的。框架的文档写得再好,也只是文档;自己亲手跑通一次,才是理解。
而且原生服务的性能并不差。http模块本身就是Node底层直接暴露出来的API,框架在上层只是加了些语法糖。真到了需要极致调优的地步,剥掉框架回到原生是常有的操作。
6. 实战中必踩的坑:问题、现象与排查方法
写原生HTTP服务一定会踩坑。有些坑我当时花了很久才搞明白原因,现在整理成速查表,希望能帮你跳过这些弯路。
6.1 中文乱码问题
现象:页面或接口返回的中文变成å�®æ–¹之类的一堆乱码。
原因很明确:响应头里的Content-Type没有带上charset=utf-8。浏览器接收到响应后不知道用什么编码去解码,只好按默认方式处理,就容易乱码。
解决方式,写响应头时永远带上字符集:
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });还有另一种情况:文件本身是UTF-8编码,但读文件时没指定编码,用fs.readFile拿到的就是Buffer而非字符串。此时用fs.readFile(filePath, 'utf8', callback)或者data.toString('utf8')处理。
6.2 POST请求的请求体"拿不到"
现象:监听req.on('data')后,没有触发,或者body一直是空字符串。
原因一般出在请求客户端那边。最常见的是用curl测试时忘了加-d参数,或者用Postman时没选择body类型。GET请求本身就没有请求体,所以监听data事件永远不会触发,这也是正常现象。
调试技巧:在req.on('end')里打印body,同时在req.on('data')里打印chunk.toString(),看数据到底有没有到达。如果data事件确实没触发,检查请求头里的Content-Length是否为0,是否为GET请求。
6.3 读文件时的路径错误
现象:fs.readFile报ENOENT,说找不到文件。
原因大概率是相对路径基准不对。fs.readFile的相对路径是相对于"进程启动时所在的目录",不是你代码文件所在的目录。如果你在项目根目录执行node src/server.js,代码里写fs.readFile('public/index.html'),它会去"项目根目录/public/index.html"找文件,而不是"src/public/index.html"。
解决方式是永远基于__dirname拼路径:
const filePath = path.join(__dirname, 'public', 'index.html');__dirname是当前模块所在目录的绝对路径,跟"在哪里启动进程"无关。用这个基准拼路径,永远不会有偏差。
6.4 端口被占用
现象:启动时报EADDRINUSE,然后进程直接退出。
原因是3000端口已经被别的进程占用了。解决方式有几种:
- 换一个端口,比如3001。
- 找到占用进程并终止它。Mac/Linux用
lsof -i :3000查出进程PID,再kill PID;Windows用netstat -ano | findstr 3000查PID,再在任务管理器结束进程。 - 让Node自动选择可用端口,监听端口传0就行,但要配合
server.address().port来获知实际端口,比较绕,平时用得不多。
6.5 浏览器缓存导致的"改动没生效"
现象:改了CSS或HTML,刷新页面还是旧版本,强刷(Ctrl+Shift+R)才正常。
原因:浏览器认为资源没变,直接用了缓存。这是HTTP缓存机制在起作用,不是服务端Bug。开发阶段有两个应对方式:
- 设置响应头
Cache-Control: no-cache,告诉浏览器每次都要向服务器确认。 - 文件名加上版本号或hash,比如
style.css?v=20250101。
原生服务里设置第一个很简单:
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-cache' });6.6 JSON.parse炸了导致进程崩溃
现象:POST请求收到非法JSON,JSON.parse(body)直接抛错,整个Node进程退出了。
原因很直接:没做异常处理。正确写法:
req.on('end', () => { try { const parsed = JSON.parse(body); res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ ok: true, data: parsed })); } catch (err) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ ok: false, message: '请求体不是合法JSON' })); } });try-catch包住JSON.parse,解析不了就返回400状态码,告诉客户端"你发的数据有问题",而不是让服务器直接挂掉。
6.7 关于Connection: keep-alive的问题
刚接触时看到浏览器请求头里有Connection: keep-alive,以为是"保持登录状态",其实是完全不同的东西。keep-alive是TCP连接层面的事,表示"这次请求结束后TCP连接不要断开,后面还可以复用"。多请求共享一个TCP连接能省掉反复建连的开销,对性能影响很大。
HTTP/1.1默认开启keep-alive,HTTP/2更进一步,同一个连接能同时承载多个并发请求。这在原生服务里不用你刻意去管,Node默认处理了,但理解它的含义对看性能指标有用。
7. 关于这一天的经验沉淀
Day 6 的内容在整条学习线里,核心不是"能写一个多复杂的功能",而是要建立三个层面的认知:
第一层,HTTP的报文结构、方法和状态码,这是你之后看任何网络相关文档的基础词汇。状态码200、301、404、500,响应头里的Content-Type、Cache-Control、Set-Cookie,这些东西在框架里被简化了,但底层逻辑永远来自HTTP协议本身。
第二层,Node原生服务的运行模型,事件循环、回调、流式读取,这些是Node最核心的运行时概念。你在这里手动处理了一次req.on('data'),后面再接触Express的body-parser、Koa的ctx.request.body,就会知道框架只是替你把流拼接和解析做了。
第三层,也是我作为过来人最想提醒的一点:框架是工具,原理是内功。写原生服务不一定是为了生产部署,更多是为了让你在遇到诡异问题时不慌。你能在原生层操作,就说明你有能力在框架层调试;你能解析报文,就说明你能看懂任何语言任何框架下的网络数据流。
我自己的经历是这样的:学完原生HTTP服务之后,再回去用框架,很多以前觉得"框架黑魔法"的功能,比如路由匹配、中间件、静态资源服务,都变成了透明的操作。不用再背配置,也不用死记API,上手新框架的速度明显变快了。
如果你在跟着做,今天就把server.js里的代码拆开改一改:试试路由里区分GET和POST,试试返回一个HTML文件,试试用curl带参数请求你的/api/users接口。多折腾几种场景,比只看文章有用得多。把这几段代码跑通之后,HTTP原理和Node原生服务这条线,就算真正打通了。