# 基础应用
30 秒速记
Node擅长I/O密集(接口转发、读写文件、查库),不擅长CPU密集,同步计算会卡住所有请求- 一个请求只能
res.end()一次,每条分支(包括出错)都得有响应,不然客户端一直挂着 - 异步错误要一路传到请求边界统一处理,别在半路
console.log吞掉 - 最小应用也要有:参数校验、超时、日志、未知路由返回
404 - 入口文件只管启动和装配,业务按模块拆开
用原生 Node 写基础应用,核心是理解它是单线程加事件循环,所以要守住两条线:别阻塞主线程,每个请求都要有且只有一次响应。 Node 处理 I/O 很高效,几千个请求同时在等数据库也没问题;但只要有一段同步代码跑几百毫秒,比如 JSON.parse 一个几十兆的字符串,这期间所有请求都得排队。另一条是响应,if 分支里漏了 res.end(),客户端会一直转圈直到超时;多调一次又会报 ERR_STREAM_WRITE_AFTER_END。路由、请求转发、爬虫、async/await 这些基础玩法都建立在这两条上。
理解「基础应用」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1接口里同步解析一个 50MB 的 JSON,为什么别的接口也跟着变慢?
Node 的 JS 跑在一个主线程上,JSON.parse 是同步的,解析的那几百毫秒里事件循环被堵住,其他请求的回调都排不上。大数据用流式解析,或者丢给 worker_threads。
追问 2某个接口偶尔一直转圈直到网关超时,代码里没报错,先看什么?
先看是不是有分支没调 res.end(),比如参数校验失败只 return 了没响应,或者 catch 里只打了日志。每条路径都要以响应结束,出错也要回一个 500。
追问 3日志里出现 ERR_HTTP_HEADERS_SENT 是什么意思?
响应已经发出去了又想再写头或再发一次,通常是 if 里响应完没 return,代码继续往下走又响应了一遍。响应语句后面紧跟 return 就好。
追问 4CPU 密集的活,比如生成报表、压缩图片,在 Node 里怎么做?
放到 worker_threads 里跑,主线程只负责收发请求;量大的话干脆拆成独立服务或者走任务队列,接口先返回「已受理」。
追问 5未捕获的异常会怎样?用 process.on('uncaughtException') 兜住行不行?
默认进程直接退出,Node 15+ 未处理的 Promise 拒绝也会让进程退出。uncaughtException 里只适合记日志然后退出,让 PM2 拉起来,别在里面吞掉继续服务,进程状态可能已经不可信了。
# 路由
30 秒速记
- 路由 = 根据请求的「方法 + 路径」决定交给哪个函数处理
- 服务端能拿到的是
method、pathname、查询参数和请求体;#hash浏览器根本不会发给服务端 - 原生写法:
new URL(req.url, 'http://localhost')拿pathname,再switch或查表分发 - 匹配不到返回
404;路径对但方法不对返回405 - 路由多了就用映射表或框架(
Express/Koa/Fastify),别写一个巨大的switch
路由说白了就是一张对照表:什么方法访问什么路径,就交给哪个函数处理。 比如 GET /students 查列表,POST /login 走登录。在原生 Node 里,路由就是自己解析 req.url 拿到 pathname,再结合 req.method 分发。有个常被问的点是 # 后面的锚点,它只在浏览器里用,请求根本不会带上,所以服务端没法按 hash 路由,前端单页应用的 hash 路由就是利用了这一点。接口少的时候 switch 够用,多了我会用一个 { 'GET /students': handler } 这样的映射表,再大就上框架。
在 BS 架构中,路由的概念都是一样的,可理解为根据客户端请求的 URL 映射到不同的方法实现,更多的一般都是针对 URL 中的路径,或者是参数,又或者是锚点这些信息进行映射。
面试官追问
追问 1前端访问 /orders#detail,后端能按 #detail 走不同逻辑吗?
不能,# 后面的部分浏览器不会放进请求里,服务端收到的只有 /orders。真要区分就改成 /orders/detail 或 ?view=detail。
追问 2只按 pathname 分发,GET /students 和 DELETE /students 进了同一个函数,问题在哪?
不同方法语义完全不同,一个查一个删,权限也不一样。路由键要带方法,比如 'GET /students',路径存在但方法不对就回 405,别默默当成查询处理。
追问 3/orders 能访问,/orders/ 却 404 了,怎么处理?
这是尾斜杠没做规范化。在分发前统一去掉末尾的 /,或者对带斜杠的请求做 301 跳转到不带斜杠的版本,全站统一一种规则就行。
追问 4/students/8 这种带参数的路径,原生 switch 怎么匹配?
switch 只能精确匹配,得用正则,比如 /^\/students\/(\d+)$/ 拿到 id。这种场景多了基本就该上 Express 之类的框架了,它们的 /students/:id 就是干这个的。
追问 5接口有几十个了,还在一个文件里 switch,你会怎么改?
按业务模块拆,每个模块导出自己的路由表,入口合并注册;合并时检查有没有重复的「方法 + 路径」。到这个规模直接用 Koa / Express 的 Router 更省事,中间件、参数解析都现成。
# 场景
30 秒速记
- 四个接口:
POST /register、POST /login、GET /students、GET /orders,其余返回错误 POST的请求体是分块流过来的:data事件里累加chunk,end事件里再解析- 常见手误:写成
postData += '_data',拼进去的是字符串字面量,账号密码永远是空的 - 未知路由要设置
404状态码,只在body里写{status: false}会被监控当成成功 - 返回
JSON用JSON.stringify+Content-Type: application/json,别用util.inspect
这个场景就是用原生 http 模块,按「方法 + 路径」把注册、登录、查学生、查订单四个接口分发出去,匹配不到的返回错误。 GET 简单,参数在 URL 上,解析一下就能用。POST 要注意请求体不是一次到的,是一块块流进来的,得在 data 事件里把 chunk 拼起来,end 事件触发后再解析,比如 request.on('data', chunk => { body += chunk })。这里最容易手滑写成 '_data' 带引号的字符串,结果解析出来什么都没有。另外未知路由别只在响应体里写失败,状态码也要给 404。
- 注册一个账户 --> [post] --> http://localhost:88/register
- 注册成功的情况下跳转到登录界面进行登录 --> [post] --> http://localhost:88/login
- 登录成功进行获取学生信息 --> [get] --> http://localhost:88/students
- 同时可以获取订单信息 --> [get] --> http://localhost:88/orders
- 如何访问不存在的路由则抛出错误信息。
const http = require('http')
const url = require('url')
const qs = require('querystring');
const util = require('util');
http.createServer((request, response) => {
let urlObj = url.parse(request.url, true);
let pathname = urlObj.pathname;
let method = request.method.toUpperCase();
let params = urlObj.query;
if(method == 'POST'){
let postData = '';
request.on('data', (_data) => {
postData += '_data';
})
request.on('end', () => {
postData = qs.parse(postData);
let result = {};
switch(pathname){
case '/login':
//连接数据库,实现登陆逻辑
result = {status: true};
break;
case '/register':
//连接数据库,实现注册逻辑
result = {status: true};
break;
default :
result = {status: false, message: '没有对应的请求'};
break;
}
response.end(util.inspect