news 2026/10/18 2:08:57

HTTP协议与Node原生服务:从报文到实战一次搞懂

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HTTP协议与Node原生服务:从报文到实战一次搞懂

1. 为什么要专门花一天搞懂HTTP和原生Node服务

大概每个写前端或者刚开始接触后端的朋友都有过这种阶段:框架用得很顺手,接口也调得通,但一旦遇到跨域、浏览器缓存、大文件上传、接口鉴权这类问题就懵了。你搜了一圈资料,帖子都写得云里雾里,最后只能改个配置碰运气,过了就过,不过就继续搜。

其实问题的根源不在框架,而在HTTP本身。

HTTP协议就是浏览器和服务器之间对话的语法和规矩。你写的每一个接口、每一次fetch请求、浏览器里每一个资源的加载,骨子里都在走这套协议。框架只是帮你把规矩封装好了,让你少写几行代码,但规矩本身从来没变过。

Node.js 原生服务就是把这层封装拆开,让你直接用最底层的http模块创建服务器、接收请求、返回响应。它没有路由,没有中间件,没有模板引擎,什么都没有,但恰恰是因为什么都没有,你才能看见HTTP请求从进入到离开的完整路径。

我记得第一次用http.createServer跑起来一个服务的时候,最大的感受是:原来我平时提交表单、加载图片、请求接口,背后发生的是一整套这么明确的过程。浏览器把你要访问的地址、要带的参数、要用的方法打包成一段符合约定的文本,通过网络送到服务器,服务器解析完,再把结果打包成另一段文本送回来。整个过程有来有回,通通是明文可见的。

Day 6 这个主题刚好卡在从"会用工具"到"理解原理"的分界线上。这篇内容适合两类人:一是刚学完JavaScript基础、准备往后端迈进的新手,二是已经在用框架、但想补一下底层原理的开发者。看完你能自己用原生代码稀里哗啦写出一个支持路由、能返回HTML和JSON的小服务,顺便把HTTP的核心知识点串成一条线。

2. 一次请求的背后:HTTP的核心脉络

HTTP这东西,拆开来看其实特别直白。它规定了客户端和服务端怎么对话:客户端先开口,服务端听完再回话。一次完整的HTTP事务,说穿了就是这么一件事。

2.1 一次请求到响应的完整过程

你在浏览器地址栏输入一个网址,按下回车,背后发生的事情是这样的:

  1. 浏览器解析URL,拆出协议、域名、端口、路径和查询参数。比如http://localhost:3000/user?id=123,协议是http,域名是localhost,端口是3000,路径是/user,查询参数是id=123。
  2. 浏览器根据域名找到服务器的IP地址。本地开发时就是127.0.0.1,线上环境则要通过DNS解析(DNS解析是另一个话题,先不展开)。
  3. 浏览器与服务器建立TCP连接。这个连接是一条可靠的传输通道,HTTP报文就是在这条通道里传递的。用不严谨但好懂的话说:TCP保证了数据能完整到达,HTTP规定了到达后怎么理解。
  4. 连接建立后,浏览器把请求报文通过这个连接发过去。报文里包含请求行、请求头、请求体,对应着你要做什么、你希望对方怎么回复。
  5. 服务器收到报文,解析出方法和路径,执行对应的业务逻辑(查数据库、读文件、单纯算个结果),然后生成一份响应报文返回。
  6. 浏览器收到响应报文,根据状态码判断这次请求是否成功,再根据响应头里的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.json

public目录放静态文件。浏览器访问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原生服务这条线,就算真正打通了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 2:08:40

Dify实战:大模型应用开发、RAG知识库与Agent工作流指南

1. Dify是什么&#xff1a;先解决一个最实际的问题先说个场景。一个做电商运营的朋友&#xff0c;想搞一个"客服机器人"&#xff0c;能自动回答用户关于退货政策、物流进度、优惠券使用的问题。他一开始的路径是&#xff1a;找大模型API、写Prompt、建知识库、处理对…

作者头像 李华
网站建设 2026/10/18 2:07:30

Linux内核的四大思维支柱与设计哲学

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 2:04:16

GPT-4 token可视化:从BPE分词到API成本优化实践

这期大模型连载&#xff0c;我来说一个自己从零做的、并且一直在用的项目&#xff1a;GPT-4 的 token 可视化网站。起因很现实&#xff0c;我用 API 做批量文本处理时&#xff0c;同一个 prompt 在不同语言、不同排版下&#xff0c;消耗的 token 数差别很大&#xff0c;而账单又…

作者头像 李华
网站建设 2026/10/18 2:03:47

蓝牙音频芯片选型:系统级取舍而非参数堆砌

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 2:02:48

智能汽车芯片选型指南:从座舱到智驾的评估框架与避坑实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华