node
从异步编程范式理解 Node.js
Node.js 的定位与核心思想
- 基于 V8 引擎 + libuv 事件驱动库,将 JavaScript 从浏览器带到服务器侧。
- 采用单线程事件循环处理 I/O,最大化利用 CPU 等待 I/O 的时间片,特别适合高并发、I/O 密集型场景。
- “不要阻塞主线程”是设计哲学:尽量把耗时操作交给内核或线程池,回调结果再回到事件循环。
事件循环与任务调度
- 阶段划分:事件循环按阶段依次处理
timers(setTimeout/setInterval)、pending callbacks、idle/prepare、poll(大部分 I/O 回调在此阶段执行)、check(setImmediate)、close callbacks。 - 微任务队列:每个阶段结束前都会清空微任务(
process.nextTick、Promise 回调)。process.nextTick优先级最高,Promise 微任务次之,需要避免递归调用导致主循环“饿死”。 - 线程池与内核协作:libuv 内部维护一个默认 4 线程的线程池,实现文件系统、DNS 等阻塞任务的异步化;网络 I/O 则直接交给内核的事件通知机制(epoll/kqueue/IOCP)。
- 背压与流控:基于事件循环的任务调度,需要在消费端感知生产速率,Node.js 提供 Stream 接口(
readable.pause()/resume()、pipeline())来避免内存爆炸。
常见异步 API 谱系
- I/O API:
fs,net,http,tls,dns等默认提供回调式异步接口,可搭配require('node:util').promisify转换为 Promise。 - Timer API:
setTimeout,setInterval,setImmediate,遵循事件循环阶段,不保证严格精确时间,尤其在主线程繁忙时会延迟。 - 微任务相关:
process.nextTick用于当前阶段尾部插队;Promise 的then/catch/finally在微任务队列执行;queueMicrotask可跨平台触发微任务。 - 事件与流:
EventEmitter用于发布订阅;Stream(Readable/Writable/Duplex/Transform)封装了基于事件的背压处理,是处理大文件、网络传输的首选模型。 - 并行能力补充:
worker_threads适合 CPU 密集型任务;cluster利用多进程共享 1 个监听端口;child_process在需要调用外部命令或隔离环境时使用。
控制流模式的演进
- 回调(Callback)时代:以 error-first 回调 (
(err, data) => {}) 为约定,简单但容易陷入回调地狱,需要注意错误链路。 - Promise:提供状态机与链式调用,使得异步流程更易组合,搭配
Promise.all/any/allSettled进行批量并发控制。 - async/await:语法糖进一步贴近同步代码结构,错误处理可以配合
try/catch。需要记住 await 会阻塞当前函数的微任务执行,适合串行逻辑。 - 更高级的组合模式:RxJS 等响应式库、生成器(
co、async)、基于迭代器的for await...of处理异步可迭代对象,帮助管理复杂数据流。
异步中的设计考量
- 错误处理:统一捕获异常(
domain已废弃,推荐使用async_hooks或自定义中间件);异步回调必须第一时间检查err。 - 资源与并发控制:使用
p-limit、Bottleneck等限制并发数,避免把线程池耗尽;合理配置UV_THREADPOOL_SIZE。 - 可观测性:借助
async_hooks跟踪异步上下文,结合diagnostics_channel、perf_hooks进行性能分析,避免隐式阻塞。 - 避免阻塞操作:
fs.readFileSync,crypto.pbkdf2Sync等同步 API 会阻塞事件循环;CPU 密集型逻辑尽量下沉到 Worker 或使用原生扩展。 - 撰写可测试异步代码:利用
jest/mocha的 async 测试能力,关注未处理的 Promise 拒绝(unhandledRejection)和异常(uncaughtException)。
典型应用场景与限制
- 适配场景:高并发 API 网关、实时推送、聊天系统、微服务网关、前后端同构渲染、脚本和 CLI 工具。
- 不适用场景:重度 CPU 运算、图像/视频编解码、紧耦合多线程共享内存场景。若必须使用,可借助 Worker Threads 或调用原生模块。
掌握 Node.js 的异步编程范式,关键在于理解事件循环调度、合理组合异步控制流,并通过工具链监控异步代码的行为。只有在不阻塞主线程的前提下,Node 才能发挥高并发的最大潜力。
Node.js 版本管理工具与包管理工具
常见的版本管理工具
- nvm (Node Version Manager):使用最广泛的 Bash 脚本版管理器,支持
nvm install <version>、nvm use <version>,方便在项目间切换;Windows 需安装专用的 nvm-windows。 - n (TJ Holowaychuk):基于 npm 安装的轻量工具,命令简洁(
n latest、n lts),适合 macOS/Linux;通过全局 npm 权限管理 Node 安装路径。 - fnm (Fast Node Manager):用 Rust 编写,下载速度快且支持多平台;可配合
fnm use、.node-version文件自动切换,兼容 fish/powershell。 - Volta:定位于“工具链管理”,同时固定 Node、npm/Yarn/pnpm 版本,适合团队协作;通过
volta pin node@18把版本写入package.json。 - 核心思路:在项目根目录写入
.nvmrc、.node-version或利用 Volta 的package.json配置,确保团队成员使用同一运行时,避免版本差异导致的行为不一致。
包管理工具的选择与特点
- npm:Node.js 官方自带,从 v7 起支持 Workspaces,多数生态默认兼容;使用
npm ci可基于package-lock.json进行可重复安装。 - Yarn:
- Yarn Classic (v1):以并行安装、
yarn.lock锁文件著称,提供workspaces支持 monorepo; - Yarn Berry (v2+):引入 Plug'n'Play (PnP) 模式,消除
node_modules,需要额外配置与 IDE 支持。 - pnpm:通过内容寻址存储节省磁盘空间和提升安装速度,默认生成
node_modules的符号链接结构;优点是天然适合多包仓库与 monorepo,大幅减少重复依赖。 - 核心命令对照:初始化(
npm init/yarn init/pnpm init)、安装依赖(npm install/yarn add/pnpm add)、锁定版本(package-lock.json/yarn.lock/pnpm-lock.yaml)。 - Corepack:Node.js 16.9+ 自带的工具,允许通过
corepack enable激活后自动管理 npm、Yarn、pnpm 版本;在package.json的packageManager字段声明版本,团队成员执行corepack install时即可统一工具链。
版本与依赖协作的最佳实践
- 在 CI/CD 上使用与本地一致的 Node 版本,可通过
.nvmrc+nvm install或volta install保证一致性。 - 锁文件应纳入版本控制,确保跨环境的依赖解析一致;升级依赖时使用
npm update等命令并重新生成锁文件。 - 针对多仓库或微服务体系,可结合版本管理器 + monorepo 包管理器(如 pnpm workspace、Yarn workspace)统一依赖;并利用
npm run/pnpm run维护脚本统一入口。 - 定期
npm audit或pnpm audit检查安全漏洞;对于私有 registry,配置.npmrc或yarnrc.yml确保认证信息安全管理。
Node 学习大纲与要点说明
- Node 的安装与配置,使用 NPM:从零搭建运行环境,熟悉
node与npm基础命令,为后续开发铺路。 - 使用 nvm 管理 Node 与 npm:掌握多版本切换、
.nvmrc配置,避免项目间运行时冲突。 - nvm 配置与重要命令解读:重点记住
nvm install/use/ls等命令,知道如何设置默认版本与镜像源。 - Node 事件与回调机制:理解事件驱动如何触发回调,弄清
EventEmitter的订阅与触发流程。 - Node 异步 IO 模型:分析 libuv 线程池和内核事件通知机制,理解非阻塞 IO 的性能优势。
- Node 的单线程模型:说明单线程 + 事件循环的协作方式,并识别其瓶颈与适用场景。
- Node 模块系统:掌握 CommonJS 导入导出、模块缓存与
require查找规则。 - npm 使用方式:熟悉依赖安装、语义化版本、
npm scripts与包发布流程。 package.json详解:逐项理解元数据、脚本、依赖配置等核心字段的作用。- 全局安装与局部安装:拆分 CLI 工具与项目依赖的安装场景,避免权限与污染问题。
- npm 重要功能详解:包括
npm ci、prune、audit等高级能力,保持依赖安全稳定。 - Node 异步编程详解:对比回调、Promise、
async/await等模式,掌握错误处理与并发控制。 - Node 流分析:学习 Readable/Writable/Transform 流以及背压控制处理大文件与网络传输。
- 输入与输出:掌握文件 IO 与标准输入输出 API,构建 CLI 或脚本工具。
- Node 网络功能:使用
http/https/net模块搭建网络服务,理解底层 Socket 工作方式。 - Node 的控制台:利用
console家族进行调试、计时、表格输出等快速诊断。 - 事件循环机制:掌握事件循环各阶段与微任务执行顺序,避免阻塞主线程。
- Node 调试:使用内置调试器、Chrome DevTools 或 VS Code 断点调试定位问题。
- 使用
exports对象:区分exports与module.exports,理解模块导出规范。 - Node 操纵文件系统:熟练使用
fs模块进行读写、监控、权限与流式操作。 - Buffer 详解:掌握二进制数据存储结构及与编码、网络传输的配合方式。
- Node 的错误处理模型:系统认识同步/异步错误捕获、全局异常与 Promise 拒绝处理。
- 使用 Node 访问 MongoDB:通过驱动或 ODM 操作文档数据库,实现 CRUD 与索引。
- 使用 Node 访问 MySQL:连接关系型数据库,学习连接池与事务控制。
- 使用 Node 访问 Redis:结合缓存、消息队列等场景设计高性能服务。
- 中间件详解:理解请求处理链与复用逻辑的拆分方式,为框架或自建服务编写中间件。
- Node Web 服务器详解:从零搭建 HTTP 服务、路由、静态资源与请求响应流程。
- WebSocket 在 Node 中的实现方式:掌握创建长连接的基础步骤与协议升级过程。
- WebSocket 数据传输:设计消息格式、心跳与断线重连策略,确保实时通信可靠。
- Socket.IO 详解:熟悉房间、命名空间等特性,加速构建实时业务。
- Express 或 KOA 全功能详解:系统学习路由、中间件、错误处理、模板与静态资源管理,夯实 Web 框架实践能力。
Node.js 模块化机制
CommonJS 基础
Node.js 最初的模块系统基于 CommonJS 规范,通过 require 导入、module.exports 导出。每个文件在首次加载时会被包裹在函数作用域中((function (exports, require, module, __filename, __dirname) {})),代码只执行一次并被缓存到 require.cache。
module.exports = value决定模块对外暴露的最终值;exports只是module.exports的快捷引用,不能整体替换。require('./foo')支持相对路径、require('node:fs')引用内置模块、require('包名')查找node_modules。require.main === module可判断当前文件是否为入口脚本,便于区分 CLI 与库逻辑。- 缓存共享:多次
require同一模块返回同一实例,适合存放单例状态,如数据库连接或配置。
// counter.js
let count = 0;
function increase() {
count += 1;
return count;
}
module.exports = {
increase,
get value() {
return count;
},
};
// app.js
const counter = require('./counter.js');
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2 - 共享缓存中的状态
模块解析与包入口
require 的查找顺序遵循:绝对路径 > 相对路径 > 内置模块 > 当前目录 node_modules > 父级目录 node_modules 逐级向上。对于目录或包:
package.json的main字段指向 CommonJS 入口;若存在exports字段,则优先生效并可定义子路径导出(如"./api": "./dist/api.js")。- 未指定入口时,Node 会尝试
index.js、index.json、index.node。 .json文件会被自动解析为对象,.node文件用于加载原生扩展。- 可以使用
require.resolve('pkg')查看实际解析到的路径,辅助调试多层依赖。
ECMAScript Modules (ESM)
Node 14+ 正式支持 ESM。启用方式包括将文件命名为 .mjs、或在 package.json 中设置 "type": "module" 后使用 .js。ESM 的特点是静态分析、异步加载与顶层 await。
- 使用
import/export语法,默认开启严格模式;__filename与__dirname不再存在,可通过import.meta.url+fileURLToPath获取。 import fs from 'node:fs';会导入模块的默认导出;命名导出使用import { readFile } from 'node:fs/promises';。export default value定义默认导出,export const name = value或export { local as alias }定义命名导出。- ESM 模块解析同样遵循
exports字段,但不再自动补全扩展名,需显式写明(如import './utils.js')。
// package.json: { "type": "module" }
import { readFile } from 'node:fs/promises';
const text = await readFile(new URL('./README.md', import.meta.url), 'utf-8');
export default text.length;
CommonJS 与 ESM 互操作
在同一工程中往往需要共存两种模块格式。常见互操作方式包括:
- 在 ESM 中使用旧模块:
import legacy from './legacy.cjs'; const { doWork } = legacy; - 在 CommonJS 中使用新模块:先引入
node:module提供的createRequire,或通过import()动态导入。 - 默认互相导入时,CommonJS 暴露的内容会映射到 ESM 的
default导出;ESM 的默认导出可以在 CommonJS 中通过module.exports = require('./esm.mjs')访问。 - 避免循环依赖导致的部分初始化,必要时拆分公共状态到独立模块或延迟调用。
// cjs-wrapper.cjs
const { createRequire } = require('node:module');
const requireESM = createRequire(__filename);
async function loadMath() {
const { sum } = await import('./math.js'); // ESM
return sum(1, 2);
}
module.exports = { loadMath, config: requireESM('./config.json') };
实践建议:尽量在新项目中统一使用 ESM,老项目逐步迁移时保持格式清晰(例如
.cjs/.mjs后缀或包级type),并利用exports字段收敛对外接口,避免深层路径耦合。
nodejs 的高性能 http
它是单线程服务(事件驱动逻辑)
// app.js
let http = require('http');
let server = http.createServer(function (request, response) {
response.writeHead(200, { 'content-type': 'text/plain' });
response.end('Hello world~ node.js');
});
server.listen(3000, 'localhost');
console.log('Node server started on port 3000');
server.on('listening', function () {
console.log('Server is listening...');
});
// connection
// close
说明:上述服务端示例使用原生
http模块直接创建服务器,response.writeHead设置响应头后立即返回纯文本字符串;server.on('listening')用于确认端口绑定情况,如需在连接建立或关闭时扩展逻辑,可继续监听connection、close等事件。
HTTP 客户端示例
const http = require('http');
let responseData = '';
const req = http.request(
{
host: 'localhost',
port: 3000,
method: 'GET',
},
function (response) {
response.on('data', function (chunk) {
responseData += chunk;
});
response.on('end', function () {
console.log(responseData);
});
}
);
req.end();
说明:客户端通过
http.request主动向localhost:3000发送GET请求,监听data事件持续累积响应内容,直到end事件触发后一次性打印responseData。与上方的服务端示例配合,即可完整验证从请求发起到响应输出的流程。
请求信息回显示例
下面的示例在处理 POST 等带请求体的场景时,会收集客户端发送的数据,并把与请求相关的关键信息拼接后返回给浏览器,便于调试和理解 http 原生模块的工作流程。
const http = require('http');
const server = http.createServer(function (request, response) {
let data = '';
request.on('data', function (chunk) {
data += chunk;
});
request.on('end', function () {
const method = request.method;
const headers = JSON.stringify(request.headers);
const httpVersion = request.httpVersion;
const requestUrl = request.url;
response.writeHead(200, { 'Content-Type': 'text/html' });
const responseData = `${method}, ${headers}, ${httpVersion}, ${requestUrl}, ${data}`;
response.end(responseData);
});
});
server.listen(3000, function () {
console.log('Node Server started on port 3000');
});
说明:
request.on('data')持续接收数据片段,当end事件触发后即可安全读取完整的请求体并发送响应;示例中把 HTTP 方法、请求头、协议版本、URL 与请求体拼接成字符串返回,实际项目中可改成结构化 JSON 或根据业务进行处理。
URL 模块与常见用法
Node.js 提供了两套 URL 处理 API:一套是符合 WHATWG 规范的 URL/URLSearchParams 类(推荐),另一套是历史遗留的 url.parse、url.format 等函数。常见的解析、构造场景可以直接使用 WHATWG 版本;只有在处理旧代码或需要兼容特殊用法时才回退到传统 API。
// WHATWG URL:解析并读取查询参数
const { URL } = require('node:url');
const userUrl = new URL('/users?role=admin&active=true', 'https://example.com');
console.log(userUrl.hostname); // example.com
console.log(userUrl.pathname); // /users
console.log(userUrl.searchParams.get('role')); // admin
console.log(userUrl.searchParams.has('active')); // true
URLSearchParams 还可以方便地构建查询字符串:
const { URLSearchParams } = require('node:url');
const params = new URLSearchParams({ page: 2, pageSize: 20 });
params.append('keyword', 'nodejs');
console.log(params.toString()); // page=2&pageSize=20&keyword=nodejs
若项目仍在使用传统 url 模块,可以通过 parse/format/resolve 完成拆解与组装:
const url = require('node:url');
const legacyParsed = url.parse(
'https://foo.com:8080/articles/list?tag=node#summary',
true
);
console.log(legacyParsed.host); // foo.com:8080
console.log(legacyParsed.query.tag); // node
const rebuilt = url.format({
protocol: 'https',
hostname: 'foo.com',
pathname: '/articles/detail',
query: { id: 123 },
});
console.log(rebuilt); // https://foo.com/articles/detail?id=123
console.log(url.resolve('https://foo.com/docs/', '../api')); // https://foo.com/api
小结:优先使用 WHATWG
URL提供的面向对象接口,可读性更高且原生支持标准行为;在维护旧项目或处理特殊格式时,再配合url.parse等旧式函数。
Querystring 工具函数
querystring 模块在 Node.js 早期用于序列化与解析查询字符串,API 风格偏向函数式。虽然在现代项目中推荐优先使用 URLSearchParams,但在处理历史代码或与旧式服务兼容时仍会遇到。核心函数包括 querystring.parse、querystring.stringify、querystring.escape 和 querystring.unescape。
const querystring = require('node:querystring');
const query = 'page=2&pageSize=20&keyword=nodejs';
const parsed = querystring.parse(query);
console.log(parsed.page); // '2'
console.log(parsed.keyword); // 'nodejs'
stringify 可以把对象转换成查询字符串,支持自定义分隔符与编码函数:
const params = { page: 2, pageSize: 20, keyword: 'node.js 入门' };
const qs = querystring.stringify(params);
console.log(qs); // page=2&pageSize=20&keyword=node.js%20入门
const custom = querystring.stringify(params, ';', ':');
console.log(custom); // page:2;pageSize:20;keyword:node.js%20入门
借助 escape/unescape 可以控制编码细节,常见于处理特殊字符或非标准编码场景:
const raw = 'name=张三&city=北京';
const escaped = querystring.escape(raw);
console.log(escaped); // name%3D%E5%BC%A0%E4%B8%89%26city%3D%E5%8C%97%E4%BA%AC
console.log(querystring.unescape(escaped)); // name=张三&city=北京
小结:
querystring为旧式代码提供兼容方案,功能覆盖解析、序列化和编码控制;在新项目中建议使用URLSearchParams获得更一致的行为和更好的国际化支持。
util 模块常见调试工具
node:util 集合了不少帮助开发和调试的辅助函数,既能提升日志的可读性,又能让旧式回调 API 更易于组合。
const util = require('node:util');
// util.format 按占位符格式化,可用于快速拼接日志
const message = util.format('User %s logged in at %d', 'alice', Date.now());
console.log(message); // User alice logged in at 1691392589123
// util.inspect 让对象在日志中打印得更清晰,支持深度与颜色控制
const config = {
db: { host: 'localhost', password: 'secret' },
features: ['sso', 'metrics'],
};
console.log(
util.inspect(config, {
depth: null,
colors: true,
showHidden: false,
compact: false,
})
);
// util.inspect.custom 可自定义对象打印逻辑
const user = {
name: 'alice',
password: 'secret',
[util.inspect.custom]() {
return `User<name=${this.name}>`;
},
};
console.log(user); // User<name=alice>
针对旧式回调函数,可以利用 util.promisify/util.callbackify 在 Promise 与回调之间转换,统一异步写法:
const fs = require('node:fs');
const readFileAsync = util.promisify(fs.readFile);
async function loadConfig() {
const content = await readFileAsync('./config.json', 'utf-8');
return JSON.parse(content);
}
const legacyFn = util.callbackify(loadConfig);
legacyFn(function (err, data) {
if (err) {
console.error('loadConfig failed', err);
} else {
console.log('config ready', data);
}
});
在排查复杂问题时,可借助 util.debuglog 创建按命名空间分类的调试日志,只需在启动前设置对应的 NODE_DEBUG 环境变量即可启用:
const debug = util.debuglog('app');
function doSomething() {
debug('processing request %d', process.pid);
}
doSomething(); // 启动时执行 NODE_DEBUG=app node app.js 才会输出
// util.getSystemErrorName 可以根据 errno 获取友好描述
console.log(util.getSystemErrorName(-4048)); // EACCES 等基于平台的错误名
此外,util.types 与 util.inherits 等工具也常出现在底层库中:前者用于精准判断 Buffer、TypedArray 等结构,后者用于帮助 ES5 风格的原型继承。
小结:开发调试时可以通过
util.format/util.inspect提升日志质量(搭配colors: true让对象输出带颜色),使用util.promisify/util.callbackify统一异步调用风格,并借助util.debuglog、util.getSystemErrorName等工具分类输出调试信息和错误提示。
DNS 模块与常见用例
Node.js 内置的 node:dns 模块负责域名解析,底层依赖 libuv 线程池与系统解析器,常用于服务发现、健康探测、监控 IP 变化等网络场景。理解以下差异尤为关键:
dns.lookup:走操作系统本地解析器(可利用/etc/hosts与系统缓存),默认只返回第一条记录,可通过{ all: true, family: 4 }返回全部 IPv4/IPv6 地址。调用会占用 libuv 线程池,密集解析时需关注UV_THREADPOOL_SIZE。dns.resolve*系列:直接对权威 DNS 服务器发 UDP 查询,跳过本地缓存。resolve4/resolve6/resolveMx/resolveTxt/resolveSrv/resolveAny等可以针对不同记录类型返回结构化结果。dns.reverse与lookupService:前者根据 IP 做反向解析,后者将 IP + 端口解析为主机名与服务名(基于/etc/services)。dns.promises与Resolver:提供 Promise 版本 API,并允许自定义递归解析服务器(resolver.setServers(['1.1.1.1'])),便于在 async/await 代码中组合调用。- 结果顺序控制:通过
dns.setDefaultResultOrder('ipv4first')指定 IPv4/IPv6 返回顺序,避免在双栈网络下因优先级导致的连接超时。
const dns = require('node:dns');
dns.lookup('nodejs.org', { all: true }, function (err, addresses) {
if (err) {
console.error('lookup failed', err);
return;
}
console.log('system resolver addresses:', addresses);
});
dns.resolve4('nodejs.org', function (err, records) {
if (err) {
console.error('resolve4 failed', err);
return;
}
console.log('authoritative A records:', records);
});
const { promises: dnsPromises } = require('node:dns');
async function inspectService(hostname) {
const addresses = await dnsPromises.lookup(hostname, { all: true });
const mxRecords = await dnsPromises.resolveMx(hostname);
const reverseHostnames = await Promise.all(
addresses.map((item) => dnsPromises.reverse(item.address))
);
console.log({ addresses, mxRecords, reverseHostnames });
}
inspectService('example.com').catch(console.error);
使用策略:高频查询可考虑在业务层做缓存或批量解析,避免消耗过多线程池资源;当需要固定使用特定 DNS 服务器(如企业内网 DNS、公共 DNS)时,优先使用
dns.promises.Resolver并结合超时和重试策略,确保解析链路稳定。
综合实例
下面通过一个最小可运行的登录服务,演示如何基于 Node.js 内置模块实现模块化分层,包含入口层、控制层与服务层。
project/
├── controllers/
│ └── authController.js
├── services/
│ └── userService.js
└── server.js
services/userService.js 负责纯业务逻辑,提供 UserService 类用于校验用户凭证:
// services/userService.js
class UserService {
constructor() {
// 模拟数据库,生产环境请替换为真实数据源
this.users = new Map([
['alice', { password: '123456' }],
['bob', { password: 'password' }],
]);
}
login(username, password) {
const record = this.users.get(username);
if (!record || record.password !== password) {
const error = new Error('用户名或密码错误');
error.statusCode = 401;
throw error;
}
return { username, password };
}
}
module.exports = new UserService();
controllers/authController.js 处理 HTTP 细节,调用服务层并返回标准化响应:
// controllers/authController.js
const userService = require('../services/userService');
function parseJsonBody(req) {
return new Promise((resolve, reject) => {
let raw = '';
req.setEncoding('utf8');
req.on('data', (chunk) => {
raw += chunk;
if (raw.length > 1e6) {
reject(new Error('请求体过大'));
req.connection.destroy();
}
});
req.on('end', () => {
try {
resolve(JSON.parse(raw || '{}'));
} catch (err) {
reject(new Error('请求体必须是合法 JSON'));
}
});
req.on('error', reject);
});
}
async function handleLogin(req, res) {
try {
const { username, password } = await parseJsonBody(req);
const user = userService.login(username, password);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: '登录成功', user }));
} catch (err) {
const statusCode = err.statusCode || 400;
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
}
module.exports = { handleLogin };
入口文件 server.js 使用内置的 http 模块启动服务并完成路由分发:
// server.js
const http = require('node:http');
const { handleLogin } = require('./controllers/authController');
const server = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/login') {
handleLogin(req, res);
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not Found' }));
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`HTTP server listening on http://localhost:${PORT}`);
});
运行 node server.js 后,可以通过 curl 发起请求验证分层逻辑:
curl -X POST http://localhost:3000/login \
-H 'Content-Type: application/json' \
-d '{"username": "alice", "password": "123456"}'
curl -X POST http://localhost:3000/login \
-H 'Content-Type: application/json' \
-d '{"username": "alice", "password": "wrong"}'
为了避免依赖外部工具,还可以编写一个使用内置 http 模块的客户端脚本,直接在 Node.js 中完成登录请求:
// client.js
const http = require('node:http');
function login(username, password) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify({ username, password });
const req = http.request(
{
hostname: 'localhost',
port: 3000,
path: '/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
},
(res) => {
let raw = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
raw += chunk;
});
res.on('end', () => {
try {
resolve({ statusCode: res.statusCode, body: JSON.parse(raw || '{}') });
} catch (err) {
reject(err);
}
});
}
);
req.on('error', reject);
req.write(payload);
req.end();
});
}
async function main() {
try {
const success = await login('alice', '123456');
console.log('登录成功响应:', success);
const failure = await login('alice', 'wrong');
console.log('登录失败响应:', failure);
} catch (err) {
console.error('请求失败', err);
}
}
main();
在启动服务端的基础上运行 node client.js,即可看到成功与失败两种响应结果。
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4765