主题
Node.js 案例与 http 模块
本页对应 Node.js 案例与 http 模块,共 4 段视频,约 46 分钟。
学习目标
- 能使用 Node.js 内置
http模块创建 Web 服务、监听指定端口,并通过浏览器访问验证服务启动成功。 - 能根据
req.url判断请求资源路径,使用res.setHeader设置Content-Type与UTF-8编码,并分别返回 JSON 数据、HTML 页面或 404 提示。 - 能使用
fs.readFile与path.join读取本地 JSON 文件,将省份列表数据作为 JSON 字符串返回给请求方。 - 能使用
querystring.parse解析 URL 查询参数,提取pname省份名称,并根据该名称从城市数据对象中返回对应城市列表。 - 能读取并返回压缩后的 HTML 网页资源,使浏览器正确解析页面内容,并通过浏览器或查看源代码验证响应结果。
- 能在修改后端代码后终止并重新启动 Node.js 服务,使用浏览器分别验证正常路径、查询参数路径和不存在路径的响应结果。
知识点
目标与需求
本节要基于 Node.js 的 Web 服务,编写一个“省份列表数据接口”:浏览器请求本机服务后,服务端读取 province.json 中的省份数据,并把 JSON 数据响应给请求方。需要先明确请求地址的组成:本机域名是 localhost,服务端口假设为 3000,资源路径为 /api/province,所以最终访问地址是 http://localhost:3000/api/province。

- 为什么需要:前端请求数据时,需要知道访问哪个服务、哪个路径;后端也要根据路径决定返回什么内容。
- 怎么想:域名和端口定位到本机上的某个 Web 服务,路径定位到该服务中的某个接口。
- 怎么做:用
localhost:3000启动服务,用/api/province作为省份列表接口路径。 - 易错点:不要把域名、端口和资源路径混在一起判断;
req.url获取的是端口号后面的路径部分,不包含前面的域名和端口。
创建 Web 服务并判断请求路径
- 为什么需要:只有先创建 HTTP 服务并监听请求,后端才能接收浏览器发来的请求。
- 怎么想:使用 Node.js 内置的
http模块创建服务对象,再监听request事件;请求到来时,通过req.url判断本次请求的资源路径。 - 怎么做:引入
http模块,调用http.createServer()创建服务,使用server.on('request', (req, res) => { ... })处理请求。当req.url === '/api/province'时,进入省份列表接口逻辑;否则返回资源不存在的提示。 - 易错点:事件名要写对,是
request;req.url的值不包含域名和端口号;一次请求只能对应一次响应,不能在同一请求流程中多次调用res.end()。虽然代码中if和else里都有res.end(),但一次请求只会走其中一个分支。
读取 province.json 并返回 JSON 数据
- 为什么需要:省份列表数据存放在本地 JSON 文件中,接口需要把文件内容读出来再返回给请求方。
- 怎么想:使用
fs.readFile()读取文件;路径要用path.join(__dirname, 'data/province.json')拼成绝对路径,避免因启动目录不同导致读取失败。读出的data是 Buffer,需要用data.toString()转成字符串再响应。 - 怎么做:在
/api/province分支中读取文件,设置响应头Content-Type: application/json;charset=utf-8,再调用res.end(data.toString())返回数据。 - 易错点:返回的是 JSON 字符串,不是 HTML 标签字符串,因此内容类型要写成
application/json;中文要正确显示,需要带charset=utf-8;读取文件路径不要写成相对路径。
js
/**
* 目标:基于 Web 服务,开发-省份列表数据接口
* 步骤:
* 1. 创建 Web 服务
* 2. 使用 req.url 获取请求的资源路径,读取 json 文件数据返回
* 3. 其他请求的路径,暂时返回不存在的提示
* 4. 运行 Web 服务,用浏览器请求地址查看效果
*/
const fs = require('fs')
const path = require('path')
// 1. 创建 Web 服务
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 2. 使用 req.url 获取请求的资源路径,读取 json 文件数据返回
if (req.url === '/api/province') {
fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(data.toString())
})
} else {
// 3. 其他请求的路径,暂时返回不存在的提示
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end('你要访问的资源路径不存在')
}
})
server.listen(3000, () => {
console.log('Web 服务启动了')
})其他路径的兜底提示
- 为什么需要:服务可能收到不存在的路径请求,需要给请求方一个明确提示,而不是没有响应。
- 怎么想:在
else分支中返回“你要访问的资源路径不存在”,并设置普通文本或 HTML 的内容类型。 - 怎么做:设置
Content-Type: text/html;charset=utf-8,再res.end('你要访问的资源路径不存在')。 - 易错点:这里返回的不是 JSON,不要继续使用
application/json;中文提示同样需要charset=utf-8,否则可能乱码。
运行服务与调试测试
- 为什么需要:代码写完后必须启动服务,并通过浏览器实际请求,验证路径判断和响应内容是否正确。
- 怎么想:在终端用
node命令执行入口文件;先测试/api/province能否返回省份 JSON,再测试一个不存在的路径能否进入else分支。 - 怎么做:例如在对应目录执行
node 09/index.js,看到“Web 服务启动了”后,在浏览器访问http://localhost:3000/api/province;再访问类似/api/123的路径查看兜底提示。修改 Node.js 代码后,需要先在终端终止旧进程,再重新运行服务,新代码才会生效。 - 易错点:忘记重启服务会误以为代码没生效;路径拼写要准确,例如
/api/province不要写错;端口被占用时要先处理旧进程。
小结
后端接口的运行过程可以概括为:判断前端请求的路径,执行对应代码,并返回需要的数据。本节中,服务端通过 req.url 判断是否为 /api/province,是则读取 province.json 并返回 JSON 数据;其他路径暂时返回不存在的提示。
需求与效果
为什么需要:前端要展示某个省下的城市列表,就必须让后端知道“查哪个省”。这个省名不能写死在后端,而是由前端通过查询参数传给后端。
怎么想:浏览器访问 http://localhost:3000/api/city?pname=辽宁省,后端从 req.url 中取出 pname,再拿它去 city.json 中匹配对应城市数组。
怎么做:在已有 Web 服务的请求处理里增加一个 /api/city 分支;读取 city.json;用省名作为 key 取城市列表;最后以 JSON 字符串响应。
易错点:不要把“路径”和“查询字符串”混在一起理解;req.url 此时是 /api/city?pname=辽宁省,不是单纯的 /api/city。

为什么不能直接用等号判断 req.url
为什么需要:城市列表接口要接收查询参数,所以请求地址后面会带 ?pname=辽宁省。如果还像省份接口那样写 req.url === '/api/province',城市接口就会匹配失败。
怎么想:转写稿里明确提醒,req.url 获取到的是“资源路径 + 查询字符串”的整个字符串;它和 /api/city 并不相等,所以不能用 === 或 ==。
怎么做:用字符串的 startsWith 做前缀匹配:req.url.startsWith('/api/city')。这样只要路径以 /api/city 开头,就进入城市列表接口。
易错点:startsWith 的参数要写完整前缀 /api/city;不要写成 /api/City,大小写不一致会匹配失败。调试时可先在该分支里 res.end('cityok'),浏览器能收到 cityok 就说明前缀匹配成功,再补后续业务。
解析查询字符串拿到 pname
为什么需要:后端虽然拿到了整个 req.url,但真正要用的只是“辽宁省”这个值,必须把查询字符串解析出来。
怎么想:先用 ? 分割,取下标 1,得到 pname=辽宁省;但不要再用 = 手动分割,因为以后可能有多个查询参数,手写分割不方便。Node.js 内置的 querystring 模块可以专门把查询字符串格式化成对象。
怎么做:引入 const qs = require('querystring');写 const str = req.url.split('?')[1];再 const query = qs.parse(str);最后 const pname = query.pname。转写稿中建议先 console.log(query),确认打印出 { pname: '辽宁省' },再取 query.pname。
易错点:split('?')[1] 在缺少问号时可能是 undefined,实际项目应做兜底;querystring 是 Node 内置模块,不要写成浏览器里的 URLSearchParams;参数名必须和后端读取的属性名一致,这也是“查询参数名字由后端规定”的原因。
读取 city.json 并匹配城市列表
为什么需要:查询参数只是“条件”,真正的数据还在本地 JSON 文件里。后端要根据省名找到对应城市数组。
怎么想:city.json 是一个大对象,key 是省份名,value 是城市列表;所以读出文件后先 JSON.parse 转成对象,再用 obj[pname] 取 value。
怎么做:用 fs.readFile(path.join(__dirname, 'data/city.json'), ...) 读取;在回调里 const obj = JSON.parse(data.toString());再 const cityList = obj[pname]。转写稿中先 console.log(cityList) 确认辽宁省对应城市数组,确认后再返回给前端。
易错点:路径要用绝对路径,借助 path.join(__dirname, ...);JSON.parse 要把 Buffer 先 toString();如果 pname 与 JSON 里的 key 不完全一致,cityList 会是 undefined,实际应加兜底判断。
返回 JSON 并调试
为什么需要:浏览器需要知道返回的是 JSON 数据,否则中文或内容类型可能解析不正确。
怎么想:res.end 只能接收字符串或 Buffer,不能直接传对象,所以城市数组要 JSON.stringify(cityList);同时设置 Content-Type: application/json;charset=utf-8。
怎么做:在 res.end 前设置响应头,再 res.end(JSON.stringify(cityList))。修改代码后要 Ctrl+C 停止旧服务,重新执行 node 10/index.js,再到浏览器测试辽宁省、广东省。
易错点:res.setHeader 要放在 res.end 之前;一次请求只对应一次响应,测试用的 res.end('cityok') 要删掉;不要忘记重启服务。
完整代码
js
/**
* 目标:基于 Web 服务,开发-城市列表数据接口
* 步骤:
* 1. 判断 req.url 资源路径+查询字符串,路径前缀匹配/api/city
* 2. 借助 querystring 模块的方法,格式化查询参数字符串
* 3. 读取 city.json 城市数据,匹配省份名字下属城市列表
* 4. 返回城市列表,启动 Web 服务测试
*/
const qs = require('querystring')
const fs = require('fs')
const path = require('path')
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 省份列表接口
if (req.url === '/api/province') {
fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(data.toString())
})
// 1. 判断 req.url 资源路径+查询字符串,路径前缀匹配/api/city
} else if (req.url.startsWith('/api/city')) {
// 城市列表接口
// 2. 借助 querystring 模块的方法,格式化查询参数字符串
// req.url: '/api/city?pname=辽宁省'
// 以?分隔符分割,拿到'pname=辽宁省'查询参数字符串
const str = req.url.split('?')[1]
// 把查询参数字符串 转成 JS 对象结构
const query = qs.parse(str)
// 获取前端发来的省份名字
const pname = query.pname
// 3. 读取 city.json 城市数据,匹配省份名字下属城市列表
fs.readFile(path.join(__dirname, 'data/city.json'), (err, data) => {
// 把 JSON 文件内对象格式字符串,转成对象结构
const obj = JSON.parse(data.toString())
// 省份名字作为 key,去obj对象里取到对应城市列表 value 值
const cityList = obj[pname]
// 4. 返回城市列表,启动 Web 服务测试
// 响应的是 JSON 字符串内容
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(JSON.stringify(cityList))
})
} else {
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end('你要访问的资源路径不存在')
}
})
server.listen(3000, () => {
console.log('Web 服务启动了')
})小结
查询参数的名字为什么由后端规定?因为后端要通过 query.pname 访问对应属性来拿到前端传来的数据;前端传参名必须和后端读取的属性名一致,否则后端取不到省名,也就匹配不到城市列表。
目标与需求
为什么需要:前面的案例主要让 Web 服务返回 JSON 数据。实际 Web 服务除了接口数据,还可以返回网页资源。本案例要让浏览器请求 /index.html 时,服务端读取时钟案例页面并返回,从而体验“后端提供网页资源”的过程。
怎么想:浏览器访问本机 Web 服务时,要用 localhost 加端口定位服务,再用资源路径定位要访问的网页。笔记中的目标页面如下:

怎么做:基于 http 模块创建 Web 服务,监听请求;当 req.url 为 /index.html 时,读取 dist/index.html 文件内容并返回;其他路径暂时返回不存在提示;最后启动服务,用浏览器发起请求。
易错点:req.url 获取的是端口号后面的资源路径和查询参数,不包含域名和端口号;本案例判断的是 /index.html,不要写成完整网址。
创建 Web 服务并监听请求
为什么需要:要接收浏览器请求,必须先创建 Web 服务对象,并监听 request 事件。只有请求发生时,回调函数才会执行。
怎么想:转写稿中先引入 fs、path、http 三个模块,再调用 http.createServer() 创建服务对象,给服务对象绑定 request 事件,回调中接收请求对象 req 和响应对象 res。
怎么做:引入模块,创建服务,绑定请求事件,在回调中先写判断逻辑;最后调用 server.listen(8080, ...) 监听端口。转写稿中先用其他路径返回 404 测试 if/else 是否生效,再补读取文件的代码。
易错点:request 事件名不要写错;一次请求只能对应一次响应,不能在同一次请求处理中多次调用 res.end();修改代码后要重启服务,转写稿中用 Ctrl+C 终止旧进程再重新运行。
判断请求路径并读取网页
为什么需要:Web 服务可能收到很多不同路径的请求。只有明确请求的是 /index.html,才返回时钟网页;否则应给出不存在提示。
怎么想:用 req.url === '/index.html' 判断。命中后,用 fs.readFile 读取 dist/index.html,路径用 path.join(__dirname, 'dist/index.html') 拼接成绝对路径。读取成功后设置响应头,再用 res.end 返回内容。
怎么做:代码如下,保留本节笔记代码:
js
/**
* 目标:编写 web 服务,监听请求的是 /index.html 路径的时候,返回 dist/index.html 时钟案例页面内容
* 步骤:
* 1. 基于 http 模块,创建 Web 服务
* 2. 使用 req.url 获取请求资源路径,并读取 index.html 里字符串内容返回给请求方
* 3. 其他路径,暂时返回不存在提示
* 4. 运行 Web 服务,用浏览器发起请求
*/
const fs = require('fs')
const path = require('path')
// 1. 基于 http 模块,创建 Web 服务
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 2. 使用 req.url 获取请求资源路径,并读取 index.html 里字符串内容返回给请求方
if (req.url === '/index.html') {
fs.readFile(path.join(__dirname, 'dist/index.html'), (err, data) => {
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end(data.toString())
})
} else {
// 3. 其他路径,暂时返回不存在提示
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end('你要访问的资源路径不存在')
}
})
server.listen(8080, () => {
console.log('Web 服务启动了')
})易错点:读取文件要用绝对路径,__dirname 表示当前文件所在目录;返回 HTML 时 Content-Type 应设置为 text/html;charset=utf-8,否则中文可能乱码;res.end() 的参数要是字符串,所以代码中用了 data.toString()。
其他路径处理与运行调试
为什么需要:如果请求路径不是 /index.html,服务端也要给出明确响应,避免浏览器一直等待。转写稿中先访问其他路径看到 404 或提示,再访问 /index.html 验证网页是否返回。
怎么想:在 else 中设置响应头并返回“你要访问的资源路径不存在”。启动服务后,在浏览器访问 http://localhost:8080/index.html,查看页面是否正常显示;也可以按 Ctrl+U 查看网页源代码。
怎么做:终端执行对应 JS 文件启动服务,看到“Web 服务启动了”后,在浏览器请求 /index.html。若修改代码,先停止服务再重新运行。转写稿中强调:压缩整合后的网页源码可能只有一行,但浏览器仍能正常解析。
易错点:端口号要与代码中 listen 一致;路径前要带 /;不要把“Web 服务”误写成“外包服务”,也不要将 index.html 写成 indice.html 等错误拼写。
小结
为什么需要:本案例把“创建服务、判断路径、读取文件、设置响应头、返回内容”串起来,是理解后端提供网页资源的基础。
怎么想:Web 服务不仅能返回 JSON 数据,也能返回 HTML 等网页资源;请求什么路径、返回什么内容,由后端代码判断决定。
怎么做:记住四步:创建 Web 服务;用 req.url 判断资源路径;读取文件并返回;启动服务用浏览器测试。
易错点:一次请求只响应一次;修改代码后要重启;返回 HTML 要设置正确的内容类型和编码。转写稿最后也提到,返回网页资源和接口的 Web 服务可以整合到一个服务中,通过多个 if/else 判断不同路径。
本节要解决的两个问题
这一节的重点有两个:一是理解前端工程化思想,即基于 Node.js 环境对前端代码进行优化处理,例如把 HTML、样式和脚本压缩整合;二是理解 Web 服务程序的作用,它既可以返回 JSON 接口数据,也可以返回网页资源等。转写稿里的四个案例,正是围绕这两点展开:先用 Node.js 写 Web 服务提供省份、城市 JSON 数据,再让 Web 服务返回压缩整合后的网页资源。
为什么要先建立这个认识?因为很多同学会把“前端工程化”和“后端接口”混在一起。实际上,Node.js 在这里承担了两类工作:一类是对前端代码做压缩整合,另一类是编写后端 Web 服务。怎么想?可以把 Web 服务看成一个“根据请求路径决定返回什么”的程序:浏览器请求哪个资源路径,后端就判断并返回对应数据。怎么做?核心是使用 Node.js 内置的 http、fs、path、querystring 等模块。易错点是:不要把 req.url 当成完整网址,它不包含域名和端口号;修改后端代码后通常要停止进程再重新运行。
案例一:省份列表 JSON 接口
为什么需要?浏览器要获取省份列表,后端就要提供一个 JSON 数据接口。怎么想?本机域名是 localhost,要定位自己编写的 Web 服务还要加端口号,转写稿中使用 3000;资源路径为 /api/province。后端检测到请求这个路径时,读取本地的 province.json,再返回给请求方。怎么做?先引入 http、fs、path,创建 Web 服务并监听 request 事件,在回调中判断 req.url。关键代码骨架可按转写稿整理为:
js
const http = require('http');
const fs = require('fs');
const path = require('path');
http.createServer((req, res) => {
if (req.url === '/api/province') {
fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(data.toString());
});
} else {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end('你要访问的资源路径不存在');
}
}).listen(3000, () => {
console.log('Web服务启动了');
});调试时,转写稿先让接口返回 OK,确认 if 判断能命中,再补充读取 JSON 文件的代码;修改代码后按 Ctrl+C 终止服务并重新执行。易错点:req.url 的值不包含前面的域名和端口号;一次请求只能对应一次响应;返回 JSON 字符串时要设置 Content-Type: application/json; charset=utf-8;读取文件要用绝对路径,借助 __dirname 拼接;中文提示要正确设置编码。
案例二:城市列表接口与查询参数
为什么需要?获取城市列表时,前端要把省份名字传给后端,通常放在 URL 问号后面的查询参数里,例如 pname=辽宁省。怎么想?此时 req.url 不只是 /api/city,后面还带着查询参数,所以不能用 === 直接比较,而要用 startsWith('/api/city') 判断前缀。接着把查询参数字符串取出来,再转成对象,拿到省份名字。怎么做?转写稿中使用 querystring 模块的 parse 方法。关键代码骨架为:
js
const qs = require('querystring');
if (req.url.startsWith('/api/city')) {
const str = req.url.split('?')[1];
const query = qs.parse(str);
const province = query.pname;
fs.readFile(path.join(__dirname, 'data/city.json'), (err, data) => {
const cityObj = JSON.parse(data.toString());
const cityList = cityObj[province];
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(cityList));
});
}调试过程也很重要:先返回一个 cityOK,确认前端请求 /api/city 开头能进入 if;再打印 qs.parse 的结果,确认 pname 能取到“辽宁省”;最后读取 city.json,用省份名字作为 key 取出城市列表。易错点:带查询参数时不能用全等比较;split('?')[1] 取的是问号后的查询串;querystring.parse 得到的是对象;res.end 只能接收字符串,所以城市列表要用 JSON.stringify 转成 JSON 字符串;响应头同样不能漏。
案例三:返回网页资源
为什么需要?Web 服务不只可以提供 JSON 接口,也可以返回网页资源。转写稿中的“浏览时钟”案例,就是请求 /index.html 时返回经过 Node.js 压缩整合后的网页。怎么想?判断 req.url === '/index.html',读取对应 HTML 文件,把内容返回给浏览器;浏览器收到标签、样式和脚本后继续解析执行。怎么做?案例中端口换成 8080,返回时设置 Content-Type: text/html; charset=utf-8。关键代码骨架为:
js
if (req.url === '/index.html') {
fs.readFile(path.join(__dirname, 'dist/index.html'), (err, data) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(data.toString());
});
} else {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end('你要访问的资源路径不存在');
}调试时先访问其他路径看是否返回 404,再访问 /index.html 看网页是否正常显示;还可以按 Ctrl+U 查看源代码,观察压缩整合后只有一行代码,但浏览器仍能正确解析。易错点:返回网页时内容类型是 text/html,不是 application/json;中文提示要设置 UTF-8;压缩成一行不影响浏览器解析;多个接口和网页资源可以整合到同一个 Web 服务中,通过多个 if/else 判断不同资源路径。
总结与易错点清单
这一节要记住:Node.js 可用于前端工程化,对前端代码进行压缩整合;Web 服务程序可以返回 JSON 接口数据,也可以返回网页资源。写接口时,先创建 Web 服务,再监听请求,根据 req.url 判断资源路径,读取对应文件并返回;涉及查询参数时,用 startsWith 判断路径前缀,用 querystring.parse 解析参数;返回 JSON 要设置 application/json; charset=utf-8,返回网页要设置 text/html; charset=utf-8。最后,改完后端代码要重启服务,一次请求只响应一次,中文编码不要漏。
今日作业说明
本节作业要求参考作业文件夹中的 md 要求完成并提交。核心任务是把转写稿中的四个案例跑通:省份列表接口、城市列表接口的查询参数获取、城市列表接口的数据响应,以及浏览时钟网页资源返回。下面按“为什么需要→怎么想→怎么做→易错点”整理成可操作教程。
案例一:省份列表接口
为什么需要:前面已经学过 Web 服务的基本概念,这个案例用来体验后端如何根据请求路径返回 JSON 数据,相当于完成以前 AJAX 阶段使用的后台接口。
怎么想:浏览器访问本机服务,域名是 localhost,端口假设为 3000,资源路径设为 /api/province。后端通过 req.url 判断请求路径,命中后读取本地 province.json,再用 res.end() 返回数据。
怎么做:先引入 http、fs、path 三个模块,创建 Web 服务并监听 request 事件。先写测试分支:命中 /api/province 返回 OK,其他路径返回 404,启动后到浏览器验证判断是否生效。确认后再补读取文件逻辑:
js
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
if (req.url === '/api/province') {
fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(data.toString());
});
} else {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end('你要访问的资源路径不存在');
}
});
server.listen(3000, () => {
console.log('Web 服务启动了');
});易错点:req.url 不包含域名和端口号,只从端口号后面开始匹配;一次请求只能对应一次响应;修改 Node 代码后必须终止旧进程并重新运行;返回 JSON 时要设置 application/json; charset=utf-8,否则中文可能乱码。
案例二:城市列表接口获取查询参数
为什么需要:获取城市列表时,前端要告诉后端“查哪个省”,这个省份名通常放在 URL 问号后面的查询参数中,例如 ?pname=辽宁省。后端必须能提取查询参数。
怎么想:这次不能再用 req.url === '/api/city' 判断,因为 req.url 实际是 /api/city?pname=辽宁省,后面多了查询字符串。因此应使用 startsWith('/api/city') 判断前缀。拿到完整 URL 后,用问号分割取出查询参数字符串,再用 querystring.parse() 转成对象。
怎么做:先写 if (req.url.startsWith('/api/city')),暂时返回 city ok 测试是否命中;命中后再解析:
js
const querystring = require('querystring');
if (req.url.startsWith('/api/city')) {
const str = req.url.split('?')[1];
const query = querystring.parse(str);
const provinceName = query.pname;
console.log(provinceName);
}调试时可先打印 query,确认它是否变成 { pname: '辽宁省' }。
易错点:不要用等号直接比较带查询参数的 URL;不要只用等号分割查询参数,因为多参数时不方便;参数名必须前后端一致,后端用 pname,前端就要传 pname;改代码后要重启服务。
案例三:城市列表接口响应数据
为什么需要:拿到省份名后,还要从 city.json 中找到该省对应的城市列表并返回给请求方。
怎么想:city.json 是一个对象,键是省份名,值是该省城市数组。读取文件后先用 JSON.parse() 把 JSON 字符串转成对象,再用省份名作为键取出城市列表。
怎么做:读取 data/city.json,在回调中解析并取值,最后用 JSON.stringify() 转成字符串返回,同时设置 JSON 响应头:
js
fs.readFile(path.join(__dirname, 'data/city.json'), (err, data) => {
const obj = JSON.parse(data.toString());
const cityList = obj[provinceName];
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(cityList));
});易错点:res.end() 只能接收字符串或 Buffer,不能直接传对象;读取 JSON 文件后要 JSON.parse();返回对象前要 JSON.stringify();测试时先打印 cityList,确认能按省份取到数组,再改为响应。
案例四:浏览时钟网页资源
为什么需要:Web 服务不仅能返回 JSON 数据,也能返回网页资源。这个案例用来体验后端返回 HTML 文件,浏览器收到后解析并展示页面。
怎么想:请求路径设为 /index.html,端口可用 8080。后端判断 req.url === '/index.html',命中后读取压缩整合后的 HTML 文件并返回。
怎么做:先测试路径判断,其他路径返回 404;命中后读取文件,设置 Content-Type 为 text/html; charset=utf-8,再 res.end(data.toString())。启动后访问 http://localhost:8080/index.html,并可用 Ctrl+U 查看源代码。
易错点:返回网页时内容类型要用 text/html,不要写成 application/json;路径要用 path.join(__dirname, ...) 拼绝对路径;修改后重启服务;返回网页资源和返回数据接口可以整合到同一个 Web 服务中,用多个 if/else 分支判断即可,但每个请求仍只能响应一次。
先把“查文档”当成一项基本功
这一节看起来只是几个链接,但它解决的是一个很实际的问题:代码写到一半,你不知道某个方法要传几个参数、回调里的 data 到底是什么类型、响应头字段该写哪个单词。案例里做省份列表接口时用了 http、fs、path,做城市列表接口时又加了 querystring,做浏览时钟时要把压缩后的 HTML 文件读出来返回给浏览器——每一步都不是靠背,而是靠确认 API 的用法。
为什么需要:API 的名字、参数顺序、返回值类型记错一个字母,程序就跑不通;而且 Node 的模块和浏览器里的对象分属两套文档,查错地方会白费时间。怎么想:先判断“这是标准 JavaScript/浏览器 Web API”,还是“Node 专有模块”,再决定去 MDN 还是 Node 文档。怎么做:把本节给的链接收藏到浏览器书签栏,按“前端标准查 MDN、后端模块查 Node”分工。易错点:拿 MDN 去查 fs.readFile,或者拿 Node 文档去查 JSON.parse,都会找不到或看得一头雾水。
MDN:前端与 Web API 的第一站
介绍里写得很清楚:MDN 是 Mozilla 基金会的开发者网络平台,提供大量关于 HTML、CSS、JavaScript 功能的开放、详细文档,以及广泛的 Web API 参考资料。
MDN(Mozilla Developer Network)网站
为什么需要:写前端代码时遇到的 URL、JSON、数组方法、字符串方法,以及浏览器提供的各种 Web API,MDN 都有标准解释和示例。怎么想:遇到“这个方法返回什么”“这个属性是只读还是可写”“浏览器兼容性如何”,先想到 MDN。怎么做:打开上面链接,用顶部搜索框直接搜方法名,例如 startsWith、JSON.stringify,进到词条后先看语法行和参数表,再看示例。易错点:MDN 主要讲标准和浏览器环境,Node 专有的 fs、path、http、querystring 不在它的覆盖范围内,别在这里死磕。
MDN 的 JavaScript 分区:系统补基础的地方
为什么需要:做接口时会反复处理字符串和对象——比如城市列表接口里要先用问号分割 req.url,再用 querystring.parse 转成对象,最后用省份名作 key 取城市数组。这些操作背后都是 JS 基础。怎么想:查单个方法去搜索框,补一整块知识(比如字符串、对象、JSON)就进 JavaScript 分区按目录看。怎么做:把它当“教材目录”,缺哪块补哪块,不要从头到尾硬读。易错点:只收藏不打开,遇到问题还是靠猜;或者把它当速查表,结果在长文里翻半天——查具体 API 时用搜索更快。
Node 官方文档与中文站:后端模块只能在这里查
为什么需要:案例里用到的 http、fs、path、querystring 全是 Node 内置模块,它们的文档只在 Node 这边有。怎么想:先确定模块名,再进对应的 API 页面;刚接触 Node 的整体概念,就先看“学习/入门”那一块。怎么做:入门看 learn 链接,查具体方法进 API 文档;笔记里给的 fs、path 链接都带 api-v16,说明文档是按版本组织的,查的时候留意版本号是否和你本机一致。易错点:不同版本的文档路径不一样,收藏了一个版本却按另一个版本的习惯去找,会打不开;另外中文翻译有时滞后,关键参数对不上时切到英文原版核对。
fs 模块文档:读文件时到底该看哪几行
为什么需要:省份接口要读 province.json,城市接口要读 city.json,时钟案例要读压缩后的 HTML 文件,全靠 fs.readFile。怎么想:查文档时重点看三件事——参数顺序(路径在前、回调在后)、回调的两个形参(错误对象在前、数据在后)、读出来的 data 是 Buffer 不是字符串。怎么做:按文档写法组织代码,读完再按需要转换:
js
const fs = require('fs')
const path = require('path')
fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(data.toString())
})易错点有三个:一是路径必须用绝对路径,所以要用 path.join(__dirname, ...) 拼,直接写相对路径容易读不到;二是 data 要 toString() 才是字符串,JSON 文件还要再 JSON.parse 才能当对象用;三是一次请求只能对应一次响应,res.end 只能调用一次,别在 if 和 else 之外又补一个。
path 模块文档:拼路径别用加号硬连
为什么需要:__dirname 拿到的是当前文件所在目录的绝对路径,但你还得往下拼 data 目录和文件名。怎么想:拼接路径是 path 模块的职责,不是字符串拼接的职责。怎么做:查 path.join 的用法,把 __dirname、目录名、文件名依次传进去。易错点:用加号手写斜杠,换到别的系统上分隔符可能对不上;另外 __dirname 只到目录,不含文件名,拼的时候别把它当成完整文件路径。
查文档的固定动作
为什么需要:链接给了不等于会用,得有套动作。怎么想:先搜关键词,再看语法行和参数表,最后看示例。怎么做:复制示例改成自己的变量名,跑一遍验证;报错时回到文档核对参数类型和返回值。易错点:只看示例不看参数说明,漏掉可选参数或搞错类型;改完 Node 代码忘记终止进程再重启,浏览器里看到的还是旧结果——转写稿里每次改完都强调先 Ctrl+C 停掉再重新 node 启动,这一步别省。
课件图示








代码示例
示例一:省份列表接口(09/index.js)
js
/**
* 目标:基于 Web 服务,开发-省份列表数据接口
* 步骤:
* 1. 创建 Web 服务
* 2. 使用 req.url 获取请求的资源路径,读取 json 文件数据返回
* 3. 其他请求的路径,暂时返回不存在的提示
* 4. 运行 Web 服务,用浏览器请求地址查看效果
*/
const fs = require('fs')
const path = require('path')
// 1. 创建 Web 服务
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 2. 使用 req.url 获取请求的资源路径,读取 json 文件数据返回
if (req.url === '/api/province') {
fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(data.toString())
})
} else {
// 3. 其他请求的路径,暂时返回不存在的提示
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end('你要访问的资源路径不存在')
}
})
server.listen(3000, () => {
console.log('Web 服务启动了')
})req.url拿到的只是端口号之后的路径部分(如/api/province),不含http://localhost:3000,所以用===直接比对即可。path.join(__dirname, 'data/province.json')拼绝对路径,__dirname是当前文件所在文件夹,避免相对路径随终端所在目录变化而读不到文件。- 返回 JSON 必须
res.setHeader('Content-Type', 'application/json;charset=utf-8'),否则浏览器按文本处理、中文可能乱码。 - 易错点:一次请求只能有一次响应,
res.end()不能在 if 命中后又走 else;分支结构保证只走其一。 - 易错点:改完代码必须
Ctrl + C终止旧进程后重新node 09/index.js,否则跑的仍是旧代码。
示例二:城市列表接口(10/index.js)
js
/**
* 目标:基于 Web 服务,开发-城市列表数据接口
* 步骤:
* 1. 判断 req.url 资源路径+查询字符串,路径前缀匹配/api/city
* 2. 借助 querystring 模块的方法,格式化查询参数字符串
* 3. 读取 city.json 城市数据,匹配省份名字下属城市列表
* 4. 返回城市列表,启动 Web 服务测试
*/
const qs = require('querystring')
const fs = require('fs')
const path = require('path')
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 省份列表接口
if (req.url === '/api/province') {
fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(data.toString())
})
// 1. 判断 req.url 资源路径+查询字符串,路径前缀匹配/api/city
} else if (req.url.startsWith('/api/city')) {
// 城市列表接口
// 2. 借助 querystring 模块的方法,格式化查询参数字符串
// req.url: '/api/city?pname=辽宁省'
// 以?分隔符分割,拿到'pname=辽宁省'查询参数字符串
const str = req.url.split('?')[1]
// 把查询参数字符串 转成 JS 对象结构
const query = qs.parse(str)
// 获取前端发来的省份名字
const pname = query.pname
// 3. 读取 city.json 城市数据,匹配省份名字下属城市列表
fs.readFile(path.join(__dirname, 'data/city.json'), (err, data) => {
// 把 JSON 文件内对象格式字符串,转成对象结构
const obj = JSON.parse(data.toString())
// 省份名字作为 key,去obj对象里取到对应城市列表 value 值
const cityList = obj[pname]
// 4. 返回城市列表,启动 Web 服务测试
// 响应的是 JSON 字符串内容
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(JSON.stringify(cityList))
})
} else {
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end('你要访问的资源路径不存在')
}
})
server.listen(3000, () => {
console.log('Web 服务启动了')
})- 带查询参数时
req.url是/api/city?pname=辽宁省,不能用===比较,必须用req.url.startsWith('/api/city')做前缀匹配。 req.url.split('?')[1]取到pname=辽宁省;再用qs.parse(str)转成对象{ pname: '辽宁省' },比手工按=再分割更稳(多参数时也正确)。JSON.parse(data.toString())把文件里的对象字符串转成 JS 对象,然后obj[pname]用省份名当 key 取城市列表。- 返回前用
JSON.stringify(cityList)转回字符串——res.end()只能接收字符串或 Buffer。 - 易错点:
pname这个名字是后端代码里写死的,前端传参名必须一致,否则query.pname是undefined,obj[undefined]取不到数据。 - 易错点:
Content-Type的设置写在fs.readFile回调里,保证“读到数据后再设置响应头并结束响应”。
示例三:浏览时钟(返回网页资源,11/index.js)
js
/**
* 目标:编写 web 服务,监听请求的是 /index.html 路径的时候,返回 dist/index.html 时钟案例页面内容
* 步骤:
* 1. 基于 http 模块,创建 Web 服务
* 2. 使用 req.url 获取请求资源路径,并读取 index.html 里字符串内容返回给请求方
* 3. 其他路径,暂时返回不存在提示
* 4. 运行 Web 服务,用浏览器发起请求
*/
const fs = require('fs')
const path = require('path')
// 1. 基于 http 模块,创建 Web 服务
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 2. 使用 req.url 获取请求资源路径,并读取 index.html 里字符串内容返回给请求方
if (req.url === '/index.html') {
fs.readFile(path.join(__dirname, 'dist/index.html'), (err, data) => {
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end(data.toString())
})
} else {
// 3. 其他路径,暂时返回不存在提示
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end('你要访问的资源路径不存在')
}
})
server.listen(8080, () => {
console.log('Web 服务启动了')
})- 本案例端口用 8080,浏览器访问
http://localhost:8080/index.html才能命中 if 分支。 - 返回网页资源时
Content-Type用text/html;charset=utf-8,浏览器才会把字符串当 HTML 解析执行,而不是显示源码。 - 读的是工程化压缩整合后的
dist/index.html,一整行也不影响浏览器解析,反而打开更快。 - 易错点:
res.end(data.toString())里data是 Buffer,不转字符串会出问题;读文件失败时data为undefined,实际项目里应先判err。 - 一个 Web 服务可以同时提供接口数据和网页资源,把多个
if / else if判断写在一起即可。
常见错误
1. 改完代码刷新浏览器,结果还是旧的
- 现象:明明把
text/html改成了application/json,浏览器响应头没变;或者新增的else if分支根本进不去。 - 原因:Node 进程启动时已把 JS 加载进内存,改磁盘文件不会热更新,跑的还是旧代码。
- 修法:终端
Ctrl + C终止进程后重新node xxx.js;调试期可装nodemon,保存即重启。养成「改代码→重启→再测」的习惯。
2. 请求 /api/city?pname=辽宁省 却掉进了「资源不存在」分支
- 现象:URL 后面一带查询参数,接口就失效;去掉
?pname=...反而正常。 - 原因:
req.url拿到的是「路径 + 查询串」的完整字符串,用=== '/api/city'永远不相等。 - 修法:带参数的接口一律用
req.url.startsWith('/api/city')做前缀匹配;只有无参数接口才用全等。
3. 报 ERR_STREAM_WRITE_AFTER_END 或「Cannot set headers after they are sent」
- 现象:控制台红字报错,或响应内容被截断、只返回了半截数据。
- 原因:一次请求里调用了两次
res.end()/res.setHeader()(常见于先用res.end('测试')验证分支,之后补业务代码时忘了删)。 - 修法:一次请求只对应一次响应。每个
if / else分支里只保留唯一一处res.end(),测试用的临时res.end()必须删干净。
4. 中文乱码,省份名和提示语显示成问号或方块
- 现象:
辽宁省变成è¾½å®å或???。 - 原因:
setHeader('Content-Type', ...)漏写charset=utf-8,或把charset拼错、写在引号外面。 - 修法:返回 JSON 用
'application/json;charset=utf-8',返回页面/提示语用'text/html;charset=utf-8',且必须在res.end()之前调用。
5. fs.readFile 报 ENOENT: no such file or directory
- 现象:路径看着没错,但就是找不到文件。
- 原因:用了相对路径
'./data/province.json',而相对路径的基准是终端启动目录,不是当前 JS 文件所在目录。 - 修法:统一
path.join(__dirname, 'data/province.json'),__dirname保证以文件自身位置为基准,换目录启动也不会错。
6. 接口返回的 JSON 前端 JSON.parse 报错,或 cityList 是 undefined
- 现象:前端解析失败;或者换个省份就查不到城市。
- 原因:两个坑——① 忘了
data.toString()/JSON.stringify(cityList),把 Buffer 或对象直接塞给res.end;② 查询参数名与后端取值名不一致(前端写pName,后端取query.pname),大小写敏感。 - 修法:读出文件先
toString()再JSON.parse,返回前JSON.stringify;参数名前后端逐字对齐,取到值后先console.log(pname)验证再往下写。
7. 启动报 EADDRINUSE: address already in use :::3000
- 现象:换了个案例目录运行,端口起不来。
- 原因:上一个案例的服务进程还占着 3000 端口。
- 修法:回到原终端
Ctrl + C关掉旧服务;或本次改用 8080 等其它端口。同一端口同一时刻只能被一个服务监听。
练习
练习 1|异常兜底 给省份列表接口补上错误处理:fs.readFile 回调里先判断 err,若出错则返回状态码 500 和 {"code":500,"msg":"服务器内部错误"} 的 JSON,而不是让进程崩溃或返回空内容。要求:把 data/province.json 临时改名验证一次效果。
练习 2|参数缺失的健壮性 请求 /api/city(不带 pname)时目前会返回 undefined。改造为:先判断 pname 是否存在,不存在则返回 {"code":400,"msg":"缺少 pname 参数"};存在但查不到该省份时返回空数组 [] 而不是 undefined。用广东省、西藏自治区、随便编的省名各测一次。
练习 3|三合一服务 把「省份接口」「城市接口」「时钟网页」合并进同一个 server.js,端口 3000,并额外要求:访问 /(根路径)时默认返回 dist/index.html 的内容,其它未知路径仍返回「资源路径不存在」提示。提示:注意 req.url === '/' 这个分支要放在最前面判断,且根路径返回的 Content-Type 是 text/html。
练习 4|按扩展名自动设置 Content-Type 让服务支持返回 dist 目录下的 .html / .css / .js / .png 四类文件:根据 req.url 拼出文件路径,用 path.extname() 取扩展名,通过一张映射表查对应的 Content-Type(如 .css → text/css;charset=utf-8,.png → image/png)。要求浏览器打开页面时样式和图片都能正常加载,F12 Network 面板中四个请求的状态码均为 200。
练习 5|抓包对照表 不写新代码,用浏览器或 curl 对以下四个地址各发一次请求,把「响应头 Content-Type」和「响应体首行内容」记录成表格并解释差异: /api/province、/api/city?pname=辽宁省、/index.html、/api/xxx。 (思考点:为什么同样是 200,浏览器对前两个是当数据、对第三个是当页面渲染?)
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 案例-省份列表接口 | 15:42 |
| 2 | 0.案例-城市列表接口-获取查询参数 | 15:15 |
| 3 | 1.案例-城市列表接口-响应数据 | 06:27 |
| 4 | 案例-浏览时钟 | 09:19 |
小结
- 后端接口的本质只有三件事:看请求路径 → 找对应数据 → 写响应。所有案例代码都是这个骨架的展开,不必死记行数。
req.url是「路径 + 查询串」的整体,无参接口用全等比较,带参接口必须用startsWith前缀匹配——这一条判断错,后面全错。Content-Type决定了浏览器怎么对待响应体:application/json当数据、text/html当页面。类型设错,数据再对前端也用不了。- 中文必须靠
charset=utf-8保底,Content-Type的设置在res.end()之前,且一次请求只能有一次响应。 - 读文件一律
path.join(__dirname, ...),路径基准是「文件在哪」,不是「终端在哪」。 - 查询参数名是前后端之间的契约,由后端取值代码决定,前端不能随意改名——这也是为什么接口文档里参数名必须写死。
- Node 服务没有热更新,任何代码改动都要重启进程才生效;「代码是对的但没生效」十有八九是忘了重启。
- 一个 Web 服务可以同时提供 JSON 接口和网页/静态资源,多个
if / else分支并列即可,不必为每个功能单独起服务。