☰
手写Node服务器与Gulp构建:HTTP请求响应原理实战
2026/10/16 10:33:16 网站建设 项目流程

1. 项目概述:这不是讲协议的PPT,是让你亲手“看见”请求怎么飞、响应怎么落

你有没有在调试接口时盯着控制台里一闪而过的GET /api/user发呆?明明前端写了fetch('/api/user'),后端也写了app.get('/api/user', ...),可数据就是不回来——浏览器 Network 面板里状态码是 404,或者干脆卡在 pending;又或者返回了数据,但前端then()里拿到的却是undefined。这时候翻文档、查教程,满屏都是“HTTP 是超文本传输协议”“请求由请求行、请求头、请求体组成”……听起来都对,但跟你的实际问题之间,像隔着一层毛玻璃。

这个标题里的“前后端交互(二)| Node+Gulp | (3)请求响应原理及HTTP 协议”,说的不是教科书里的 HTTP 理论,而是一套能让你亲手拆开、装上、再看着它跑起来的实操闭环。Node.js 是你的后端发动机,Gulp 是帮你把静态资源、编译、监听打包成流水线的机械臂,而 HTTP 协议,就是它们之间那条看得见摸得着的“数据公路”。你不需要背 RFC 文档,但必须清楚:当用户点下按钮那一刻,数据包从浏览器出发,经过哪几道门、被谁签收、怎么拆包、又如何封装回传——每一步,你都能在本地复现、打断点、改参数、看日志。

我带过不少刚转前端的同学,他们卡在“会写 Vue 组件,但不会调通一个真实 API”的临界点。问题往往不在框架,而在对这条“公路”的陌生。比如,为什么加了Content-Type: application/json后端才能JSON.parse(req.body)?为什么fetch默认不带 cookie,而XMLHttpRequest可以?为什么 Gulp 的watch能实时刷新页面,但改了 Node 接口却要手动重启?这些都不是玄学,是 HTTP 协议里几个关键字段和 Node.js 运行机制共同作用的结果。这篇文章,就是带你用 Node 搭一个极简服务器,用 Gulp 构建一个带自动刷新的前端页面,然后用最原始的curl和浏览器开发者工具,一帧一帧地“拍摄”下请求与响应的全过程。你会看到,所谓的“协议”,不过是几行文本的约定;所谓的“交互”,不过是两个程序按规则交换字符串。当你亲手让一个200 OK响应从自己写的res.writeHead(200)里真正流出来,那种“原来如此”的手感,比读十篇理论文章都管用。

2. 整体设计思路:为什么选 Node + Gulp 而不是 Express 或 Webpack?

2.1 抛弃“全家桶”,回归通信本质的三层结构

很多教程一上来就甩出 Express + Webpack + Vue CLI 的组合,看似高效,实则把 HTTP 协议这层最关键的“玻璃”彻底糊住了。Express 封装了req和res对象,让你连res.write()都很少写;Webpack Dev Server 自动处理了所有静态资源和热更新,你根本看不到304 Not Modified是怎么被触发的。这就像学开车,直接给你一辆自动驾驶汽车,你当然能从 A 到 B,但永远不知道油门、离合、档位之间是怎么配合的。

所以本项目的整体设计,是刻意做“减法”,构建一个清晰的三层结构:

  • 底层:原生 Node.js HTTP 模块
    不用 Express,直接用require('http')创建服务器。这样你能亲手调用res.writeHead()设置状态码和响应头,用res.write()逐块写入响应体,最后用res.end()关闭连接。每一个 HTTP 状态码、每一个响应头字段,都由你亲手敲出来,而不是靠框架的res.status(200).json({})一行代码掩盖过去。

  • 中层:Gulp 作为构建与协作枢纽
    Gulp 在这里不扮演“打包器”的角色,而是作为前后端通信的协调员。它的核心任务有三个:第一,监听前端 HTML/JS 文件变化,自动刷新浏览器(模拟真实开发流);第二,启动并管理 Node.js 后端进程(避免你手动node server.js再Ctrl+C);第三,将前端构建产物(如压缩后的 JS)自动复制到后端服务的静态资源目录。这三层职责,让 Gulp 成为连接前后端的“胶水”,而不是一个黑盒构建工具。

  • 上层:纯 HTML + 原生 Fetch API
    前端不引入任何框架,只用一个index.html和内联<script>。所有数据请求,全部使用原生fetch(),并显式配置method、headers、credentials等关键选项。这样,你在 Network 面板里看到的每一个请求细节,都和你代码里写的完全一致,没有框架的二次封装干扰。

这个设计的核心逻辑是:越少的抽象层,越强的掌控力。当你用原生 Node 处理一个请求时,你必须决定:是否要解析 URL 查询参数?是否要监听data事件来收集 POST 请求体?是否要在响应头里设置Access-Control-Allow-Origin?每一个“必须决定”,都在强迫你直面 HTTP 协议的细节。而 Gulp 的加入,不是为了炫技,而是为了把这种“直面”变得可持续——你不用在改完前端 JS 后,再切到终端去Ctrl+C重启服务,Gulp 会帮你完成这一切,让你的注意力始终聚焦在“请求-响应”这个核心链路上。

2.2 为什么 Gulp 比 Webpack 更适合作为教学载体?

很多人会问:现在主流都是 Webpack/Vite,为什么还用 Gulp?答案很实在:Gulp 的配置是“线性可读”的,而 Webpack 的配置是“树状嵌套”的。

举个例子,你想让 Gulp 在文件变化时执行“刷新浏览器”操作,代码是这样的:

const browserSync = require('browser-sync').create(); gulp.task('serve', () => { browserSync.init({ server: './dist', port: 3000 }); gulp.watch('./src/**/*.html').on('change', browserSync.reload); });

这段代码,从上到下读下来,逻辑是平铺直叙的:启动一个本地服务器 → 监听 HTML 文件 → 文件一变就刷新。你一眼就能看出数据流向。

而同样的功能,在 Webpack Dev Server 里,你需要配置devServer对象,里面嵌套hot、liveReload、watchFiles等多个属性,还要配合HtmlWebpackPlugin插件来注入 script 标签。新手很容易迷失在module.exports = { devServer: { ... } }的层层大括号里。

更重要的是,Gulp 的task机制,天然适合模拟 HTTP 的“请求-响应”生命周期。你可以定义一个task专门处理“接收请求”,另一个task专门处理“生成响应”,再用gulp.series()把它们串起来。这种“任务流”的思维,和 HTTP 的“请求进来 → 服务处理 → 响应出去”流程高度吻合。我在某高校带实训课时,让学员用 Gulp 模拟一次完整的登录流程:gulp login:check(验证用户名密码)、gulp login:generate-token(生成 JWT)、gulp login:send-response(返回 token),他们立刻就理解了“状态码 200 和 401 是怎么由不同任务分支决定的”。

所以,选择 Gulp,不是怀旧,而是因为它像一把解剖刀,能把复杂的开发流程,切成几块清晰、可触摸、可单独调试的“组织”。当你需要向别人解释“为什么跨域请求会多发一次 OPTIONS 预检”,你可以直接在 Gulp 的watch任务里加一行console.log('OPTIONS request received'),然后用curl -X OPTIONS http://localhost:3000/api/login去触发它——这种“所见即所得”的教学体验,是任何高度封装的现代构建工具都难以替代的。

3. 核心细节解析:HTTP 协议的四个关键“锚点”,全在代码里

3.1 锚点一:状态码不是数字,是服务端的“决策快照”

HTTP 状态码常被简化为“200 是成功,404 是没找到,500 是服务器炸了”。但这只是结果,不是原因。真正的价值在于:状态码是你在服务端代码里,对当前请求做出的即时决策的快照。

在我们的 Node.js 服务器里,状态码不是框架自动给的,而是你亲手writeHead()写进去的。我们来看一个真实的登录接口片段:

// server.js const url = require('url'); const querystring = require('querystring'); server.on('request', (req, res) => { const parsedUrl = url.parse(req.url, true); if (req.method === 'POST' && parsedUrl.pathname === '/api/login') { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { const data = querystring.parse(body); // 【锚点体现】状态码在这里被决策 if (data.username === 'admin' && data.password === '123') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, token: 'abc123' })); } else { // 注意:这里不是 400,而是 401 res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Unauthorized' })); } }); } });

看到没?res.writeHead(401, ...)这一行,就是服务端在“检查完用户名密码后”,拍板决定“这个请求不被授权”的瞬间。它不是一个预设的错误码,而是业务逻辑执行完毕后,你根据if/else分支主动写入的结论。这就是为什么 401 和 400 不能混用:400 是“请求格式错了”(比如 JSON 缺少字段),401 是“格式没错,但人不对”。如果你把登录失败写成400,前端fetch().catch()里就无法区分“是用户输错了密码”,还是“网络请求根本没发出去”。

我在某公司做内部培训时,曾让学员修改这段代码,把401改成403,然后用curl -v -X POST http://localhost:3000/api/login -d "username=wrong"去测试。结果发现,浏览器 Network 面板里,fetch的catch回调确实触发了,但response.status显示是0,而不是403。为什么?因为403 Forbidden表示服务器理解请求,但拒绝授权;而浏览器出于安全策略,对某些跨域响应状态码做了限制,403在特定条件下会被拦截。这个“意外”,恰恰暴露了状态码背后的真实含义:它不仅是给前端看的,更是浏览器、代理服务器、CDN 共同遵守的“交通规则”。你写的每一个状态码,都在告诉整个网络链路上的所有中间件:“请按这个规则处理后续动作”。

提示:在 Node.js 原生 HTTP 模块中,res.writeHead()的第二个参数是响应头对象。务必记得设置'Content-Type': 'application/json',否则即使你res.end('{ "ok": true }'),前端response.json()也会报错,因为浏览器默认按text/plain解析,而JSON.parse()无法解析纯文本。

3.2 锚点二:请求头不是装饰,是客户端的“身份声明书”

前端同学常有个误区:fetch就是发个请求,headers选项可有可无。但现实是,请求头是你在 HTTP 公路上亮出的“身份证”和“通行证”。它决定了你的请求能不能进门、走哪条道、享受什么服务。

我们用 Gulp 启动的前端页面,其fetch调用是这样写的:

// dist/index.html fetch('/api/user', { method: 'GET', credentials: 'include', // 【关键!】 headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } }) .then(res => res.json()) .then(data => console.log(data));

这里credentials: 'include'是第一个关键锚点。它告诉浏览器:“请把当前域名下的 Cookie 一起带上”。如果没有这一行,即使后端设置了Set-Cookie,下次请求时 Cookie 也不会发送,导致登录态丢失。而headers里的'Accept': 'application/json',则是明确告知后端:“我只接受 JSON 格式的响应,如果返回 HTML,请别怪我没处理好”。

更微妙的是'X-Requested-With': 'XMLHttpRequest'。这是一个历史遗留的自定义头,很多老后端框架(如 PHP 的 Laravel)会用它来判断“这是不是 AJAX 请求”,从而决定返回 JSON 还是重定向。虽然现代标准已不推荐,但它在真实项目中依然广泛存在。我们在 Node 服务器里可以这样读取它:

// server.js req.on('end', () => { const xRequestedWith = req.headers['x-requested-with']; if (xRequestedWith === 'xmlhttprequest') { // 返回 JSON } else { // 返回 HTML 页面 } });

这就是请求头的威力:它不改变请求的“内容”,但彻底改变了请求的“语义”。同一个/api/user地址,带上X-Requested-With头,后端返回{ "name": "Alice" };不带这个头,后端可能返回一个完整的<html><body>...</body></html>页面。你写的每一行headers,都在参与这场语义协商。

注意:credentials: 'include'有一个硬性要求——后端响应头里必须包含'Access-Control-Allow-Credentials': 'true',否则浏览器会直接拒绝响应,Network 面板里显示CORS error。这是浏览器的安全沙箱机制,不是 Bug,是 Feature。你必须在res.writeHead()里同步设置它,否则前端永远拿不到数据。

3.3 锚点三:响应头不是尾巴,是服务端的“服务说明书”

如果说请求头是客户端的“声明”,那么响应头就是服务端的“说明书”。它不包含业务数据,却决定了前端如何“使用”这些数据。

在我们的 Node 服务器中,响应头的设置是这样进行的:

res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', // 【说明书1:数据类型和编码】 'Access-Control-Allow-Origin': 'http://localhost:3000', // 【说明书2:谁能访问】 'Access-Control-Allow-Credentials': 'true', // 【说明书3:是否允许带凭证】 'Cache-Control': 'no-cache, no-store, must-revalidate', // 【说明书4:缓存策略】 'X-Response-Time': Date.now() - startTime + 'ms' // 【说明书5:自定义监控】 });
  • 'Content-Type': 'application/json; charset=utf-8':这行代码,是前端response.json()能成功执行的前提。它告诉浏览器:“接下来的数据是 JSON 格式,并且用 UTF-8 编码”。如果漏掉charset=utf-8,而你的 JSON 里有中文,前端解析时就可能出现乱码。这不是 Node 的错,是浏览器严格按照响应头指示来解码的结果。

  • 'Access-Control-Allow-Origin': 'http://localhost:3000':这是 CORS(跨域资源共享)的核心。它不是“允许跨域”,而是“声明:只有来自http://localhost:3000这个源的请求,才被允许读取我的响应”。注意,它不能写成*(星号)当credentials: 'include'存在时,否则浏览器会直接报错。这是 HTTP 协议里一个非常严格的“契约”。

  • 'Cache-Control': 'no-cache, no-store, must-revalidate':这行代码,是防止前端拿到过期数据的关键。no-cache表示“每次用之前必须向服务器确认是否过期”,no-store表示“别存我,我太敏感”,must-revalidate表示“就算你缓存了,也必须先校验”。在开发阶段,尤其是调试登录态、用户信息等动态数据时,必须关掉缓存,否则你会陷入“我明明改了代码,为什么页面还是旧的”这种幻觉。

我在某电商公司的接口重构项目中,就遇到过一个经典案例:商品详情页的库存数据,后端响应头里忘了加Cache-Control: no-cache,而 CDN 层级又默认缓存了 5 分钟。结果运营同学修改了库存,前端页面却要等 5 分钟才刷新——用户下单时发现“库存不足”,其实后台已经补货了。这个坑,就是没把响应头当回事的代价。

3.4 锚点四:请求体与响应体不是管道,是需要“组装”的数据包

很多初学者以为fetch()发送的数据,后端req对象里直接就有req.body。但在 Node.js 原生 HTTP 模块中,请求体是流(Stream),需要你手动“拼接”;响应体是缓冲区(Buffer),需要你手动“写入”。这正是理解 HTTP “分块传输”本质的关键。

看这段处理 POST 请求的代码:

req.on('data', chunk => { console.log('收到数据块:', chunk.length, '字节'); body += chunk; // chunk 是 Buffer,+ 操作会隐式转换为字符串 }); req.on('end', () => { console.log('请求体接收完毕,总长度:', body.length); // 此时 body 才是完整的字符串 const data = querystring.parse(body); // 如果是表单数据 // 或 const data = JSON.parse(body); // 如果是 JSON 数据 });

这里有两个关键点:第一,req.on('data')可能被触发多次,每次chunk的大小由 TCP 包决定,可能是 1KB,也可能是 1MB。你不能假设“一次data事件就拿到全部”。第二,chunk是Buffer类型,不是字符串。直接console.log(chunk)会输出一堆乱码,必须用chunk.toString()或body += chunk(后者会自动转换)。

同样,响应体也不是res.end('hello')就完事。res.write()可以分多次写入:

res.writeHead(200, { 'Content-Type': 'text/html' }); res.write('<html><body>'); res.write('<h1>Hello World</h1>'); res.write('<p>Current time: ' + new Date().toISOString() + '</p>'); res.end('</body></html>');

这种“分块写入”的能力,是实现“服务端推送”(SSE)和“流式响应”的基础。比如,你要做一个实时日志查看页面,后端就可以不断res.write('log line 1\n')、res.write('log line 2\n'),前端用EventSource接收,而不用轮询。这背后,就是 HTTP 协议支持“长连接”和“分块传输”的能力。

实操心得:在调试请求体时,不要只依赖console.log(body)。一定要在req.on('data')里加console.log('chunk:', chunk),观察每次收到的chunk内容。你会发现,一个简单的{"name":"张三"}JSON,可能被拆成{"name":"和"张三"}两块发送。这就是网络传输的真实模样——没有“完整”,只有“尽力而为”。

4. 实操过程:从零搭建一个可调试的 HTTP 交互环境

4.1 环境准备:三步建立“请求-响应”最小闭环

搭建这个环境,目标不是追求功能完备,而是建立一个“改动即可见”的最小闭环。整个过程只需三步,每一步都对应一个 HTTP 的核心环节。

第一步:初始化 Node.js 服务器(建立通信信道)
创建server.js,内容如下:

const http = require('http'); const url = require('url'); const server = http.createServer((req, res) => { console.log(`【HTTP 信道】${new Date().toISOString()} | ${req.method} ${req.url}`); // 记录请求头,看清客户端“声明”了什么 console.log('【请求头】', req.headers); // 简单路由:/ 返回欢迎页,/api/test 返回 JSON if (req.url === '/' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(` <!DOCTYPE html> <html> <head><title>HTTP 交互实验室</title></head> <body> <h1>欢迎来到 HTTP 交互实验室</h1> <button onclick="fetch('/api/test').then(r=>r.json()).then(console.log)">点击发送 GET 请求</button> <div id="result"></div> <script> // 前端 fetch,与后端形成闭环 document.querySelector('button').onclick = () => { fetch('/api/test') .then(r => { console.log('响应状态码:', r.status); console.log('响应头:', Object.fromEntries(r.headers.entries())); return r.json(); }) .then(data => { document.getElementById('result').innerText = JSON.stringify(data); }); }; </script> </body> </html> `); } else if (req.url === '/api/test' && req.method === 'GET') { // 【关键】在这里设置所有响应头,看清服务端“说明书” res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': 'true', 'Cache-Control': 'no-cache' }); res.end(JSON.stringify({ message: 'Hello from Node.js!', timestamp: Date.now() })); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); server.listen(3000, () => { console.log('✅ Node.js 服务器已启动,监听 http://localhost:3000'); });

运行node server.js,打开浏览器访问http://localhost:3000。此时,你已经拥有了一个最简 HTTP 信道:浏览器发起请求 → Node.js 接收 → Node.js 返回响应 → 浏览器渲染。控制台里打印的req.headers和res.writeHead()的参数,就是你观察协议的第一手资料。

第二步:安装并配置 Gulp(建立自动化协作)
全局安装 Gulp CLI,然后在项目根目录初始化:

npm install --global gulp-cli npm init -y npm install --save-dev gulp gulp-cli gulp-connect gulp-sourcemaps

创建gulpfile.js:

const gulp = require('gulp'); const connect = require('gulp-connect'); // 【任务1】启动 Node 服务器(复用上面的 server.js) gulp.task('server', (done) => { const childProcess = require('child_process'); const server = childProcess.spawn('node', ['server.js']); server.stdout.on('data', (data) => { console.log(`[Node] ${data}`); }); server.stderr.on('data', (data) => { console.error(`[Node Error] ${data}`); }); // 服务器启动后,启动前端服务 setTimeout(() => { gulp.series('frontend')(); }, 1000); done(); }); // 【任务2】启动前端服务(提供自动刷新) gulp.task('frontend', () => { connect.server({ root: './', port: 3000, livereload: true }); }); // 【任务3】监听文件变化,自动刷新 gulp.task('watch', () => { gulp.watch(['*.html', '*.js'], gulp.series('reload')); }); gulp.task('reload', (done) => { connect.reload(); done(); }); // 默认任务:启动服务器 + 前端 + 监听 gulp.task('default', gulp.parallel('server', 'frontend', 'watch'));

运行gulp,Gulp 会自动启动 Node 服务器,并开启一个支持 LiveReload 的前端服务。此时,你修改server.js里的res.end()内容,保存后,浏览器会自动刷新,同时控制台会打印出新的请求日志。这个“改-存-刷-看”的闭环,就是高效调试 HTTP 的基石。

第三步:用 curl 和浏览器工具“透视”每一次交互
不要只依赖console.log。打开浏览器开发者工具的 Network 面板,点击按钮,你会看到:

  • 一个GET /api/test请求,状态码200,Headers 标签页里清晰列出Request Headers(你前端fetch里设置的)和Response Headers(你res.writeHead()里写的)。
  • Preview 标签页里,是格式化后的 JSON 响应体。
  • Timing 标签页里,Stalled、DNS Lookup、Initial connection、Request sent、Waiting (TTFB)、Content Download这些时间点,就是 HTTP 请求在网络中真实经历的旅程。

再用命令行curl -v http://localhost:3000/api/test,你会看到完整的 HTTP 原始报文:

> GET /api/test HTTP/1.1 > Host: localhost:3000 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 < Access-Control-Allow-Origin: http://localhost:3000 < Cache-Control: no-cache < Date: Mon, 15 Apr 2024 08:23:45 GMT < Connection: keep-alive < Keep-Alive: timeout=5 < Transfer-Encoding: chunked < {"message":"Hello from Node.js!","timestamp":1713169425123}

这个原始报文,就是 HTTP 协议的“裸体”。它证明了:所谓协议,不过是几行文本的约定;所谓交互,不过是两个程序按规则交换字符串。当你能熟练阅读curl -v的输出,你就真正“看见”了 HTTP。

4.2 核心环节实现:手写一个带登录态的完整交互流程

现在,我们把前面的“锚点”全部串起来,实现一个真实的登录-获取用户信息流程。这个流程会覆盖 HTTP 的核心状态码、请求/响应头、Cookie 传递、JSON 解析等所有关键点。

后端实现(server.js):

const http = require('http'); const url = require('url'); const querystring = require('querystring'); // 模拟一个内存中的 session 存储(仅用于演示) const sessions = new Map(); const server = http.createServer((req, res) => { const parsedUrl = url.parse(req.url, true); const pathname = parsedUrl.pathname; // 【环节1:登录 POST 请求】 if (req.method === 'POST' && pathname === '/api/login') { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { const data = querystring.parse(body); console.log('【登录请求体】', data); // 简单验证(生产环境请用 bcrypt 加密) if (data.username === 'demo' && data.password === 'pass') { // 生成一个随机 session ID const sessionId = Math.random().toString(36).substr(2, 9); sessions.set(sessionId, { username: data.username, loginTime: Date.now() }); // 【关键响应头】设置 Set-Cookie,将 session ID 发送给浏览器 res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': `session_id=${sessionId}; HttpOnly; Path=/; Max-Age=3600`, // 1小时有效期 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': 'true', 'Cache-Control': 'no-store' }); res.end(JSON.stringify({ success: true, message: 'Login successful', user: { username: data.username } })); } else { res.writeHead(401, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': 'true' }); res.end(JSON.stringify({ error: 'Invalid credentials' })); } }); } // 【环节2:获取用户信息 GET 请求,需验证 Cookie】 else if (req.method === 'GET' && pathname === '/api/user') { // 从请求头中读取 Cookie const cookies = req.headers.cookie; console.log('【用户请求携带的 Cookie】', cookies); let sessionId = null; if (cookies) { // 解析 Cookie 字符串,提取 session_id cookies.split(';').forEach(cookie => { const [name, value] = cookie.trim().split('='); if (name === 'session_id') sessionId = value; }); } if (sessionId && sessions.has(sessionId)) { const user = sessions.get(sessionId); res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': 'true', 'Cache-Control': 'no-cache' }); res.end(JSON.stringify({ success: true, user: { username: user.username, loginTime: new Date(user.loginTime).toISOString() } })); } else { // 未登录或 session 过期 res.writeHead(401, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': 'true' }); res.end(JSON.stringify({ error: 'Unauthorized: Please login first' })); } } // 【环节3:登出,清除 Cookie】 else if (req.method === 'POST' && pathname === '/api/logout') { // 清除 Cookie,通过设置 Max-Age=0 res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': 'session_id=; HttpOnly; Path=/; Max-Age=0', 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': 'true' }); res.end(JSON.stringify({ success: true, message: 'Logout successful' })); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); server.listen(3000, () => { console.log('✅ 完整交互服务器已启动,监听 http://localhost:3000'); });

前端实现(index.html):

<!DOCTYPE html> <html> <head><title>HTTP 登录交互演示</title></head> <body> <h1>HTTP 登录交互演示</h1> <!-- 登录表单 --> <div id="login-form"> <h2>登录</h2> <input type="text" id="username" placeholder="用户名" value="demo"><br><br> <input type="password" id="password" placeholder="密码" value="pass"><br><br> <button onclick="doLogin()">登录</button> </div> <!-- 用户信息区域 --> <div id="user-info" style="display:none;"> <h2>用户信息</h2> <p id="user-display"></p> <button onclick="doLogout()">登出</button> </div> <script> // 【关键】fetch 配置必须包含 credentials: 'include' async function doLogin() { const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`, credentials: 'include' // 【再次强调】必须带上 Cookie }); const data = await res.json(); console.log('登录响应:', data); if (res.status === 200 && data.success) { document.getElementById('login-form').style.display = 'none'; document.getElementById('user-info').style.display = 'block'; document.getElementById('user-display').innerText = `欢迎,${data.user.username}!`; // 登录成功后,立即获取用户信息 await getUserInfo(); } else { alert('登录失败: ' + (data.error || '未知错误')); } } catch (err) { console.error('登录异常:', err); alert('网络错误,请检查服务器是否运行'); } } async function getUserInfo() { try { const res = await fetch('/api/user', { method: 'GET', credentials: 'include' // 【再次强调】必须带上 Cookie }); const data = await res.json(); console.log('用户信息响应:', data); if (res.status === 200 && data.success) { document.getElementById('user-display').innerText = `欢迎,${data.user.username}!\n登录时间:${data.user.loginTime}`; } else { // 401 表示未登录,跳转回登录页 if (res.status === 401) { alert('会话已过期,请重新登录'); document.getElementById('user-info').style.display = 'none'; document.getElementById('login-form').style.display = 'block'; } } } catch (err) { console.error('获取用户信息异常:', err); } } async function doLogout() { try { const res = await fetch('/api/logout', { method: 'POST', credentials: 'include' }); const data = await res.json(); console.log('登出响应:', data); if (res.status === 200

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询