Node深入浅出(圣思园教育) 001【学习笔记】

node

从异步编程范式理解 Node.js

Node.js 的定位与核心思想

  • 基于 V8 引擎 + libuv 事件驱动库,将 JavaScript 从浏览器带到服务器侧。
  • 采用单线程事件循环处理 I/O,最大化利用 CPU 等待 I/O 的时间片,特别适合高并发、I/O 密集型场景。
  • “不要阻塞主线程”是设计哲学:尽量把耗时操作交给内核或线程池,回调结果再回到事件循环。

事件循环与任务调度

  1. 阶段划分:事件循环按阶段依次处理 timerssetTimeout/setInterval)、pending callbacksidle/preparepoll(大部分 I/O 回调在此阶段执行)、checksetImmediate)、close callbacks
  2. 微任务队列:每个阶段结束前都会清空微任务(process.nextTick、Promise 回调)。process.nextTick 优先级最高,Promise 微任务次之,需要避免递归调用导致主循环“饿死”。
  3. 线程池与内核协作:libuv 内部维护一个默认 4 线程的线程池,实现文件系统、DNS 等阻塞任务的异步化;网络 I/O 则直接交给内核的事件通知机制(epoll/kqueue/IOCP)。
  4. 背压与流控:基于事件循环的任务调度,需要在消费端感知生产速率,Node.js 提供 Stream 接口(readable.pause()/resume()pipeline())来避免内存爆炸。

常见异步 API 谱系

  • I/O APIfs, net, http, tls, dns 等默认提供回调式异步接口,可搭配 require('node:util').promisify 转换为 Promise。
  • Timer APIsetTimeout, setInterval, setImmediate,遵循事件循环阶段,不保证严格精确时间,尤其在主线程繁忙时会延迟。
  • 微任务相关process.nextTick 用于当前阶段尾部插队;Promise 的 then/catch/finally 在微任务队列执行;queueMicrotask 可跨平台触发微任务。
  • 事件与流EventEmitter 用于发布订阅;Stream(Readable/Writable/Duplex/Transform)封装了基于事件的背压处理,是处理大文件、网络传输的首选模型。
  • 并行能力补充worker_threads 适合 CPU 密集型任务;cluster 利用多进程共享 1 个监听端口;child_process 在需要调用外部命令或隔离环境时使用。

控制流模式的演进

  1. 回调(Callback)时代:以 error-first 回调 ((err, data) => {}) 为约定,简单但容易陷入回调地狱,需要注意错误链路。
  2. Promise:提供状态机与链式调用,使得异步流程更易组合,搭配 Promise.all/any/allSettled 进行批量并发控制。
  3. async/await:语法糖进一步贴近同步代码结构,错误处理可以配合 try/catch。需要记住 await 会阻塞当前函数的微任务执行,适合串行逻辑。
  4. 更高级的组合模式:RxJS 等响应式库、生成器(coasync)、基于迭代器的 for await...of 处理异步可迭代对象,帮助管理复杂数据流。

异步中的设计考量

  • 错误处理:统一捕获异常(domain 已废弃,推荐使用 async_hooks 或自定义中间件);异步回调必须第一时间检查 err
  • 资源与并发控制:使用 p-limitBottleneck 等限制并发数,避免把线程池耗尽;合理配置 UV_THREADPOOL_SIZE
  • 可观测性:借助 async_hooks 跟踪异步上下文,结合 diagnostics_channelperf_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 latestn 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.jsonpackageManager 字段声明版本,团队成员执行 corepack install 时即可统一工具链。

版本与依赖协作的最佳实践

  • 在 CI/CD 上使用与本地一致的 Node 版本,可通过 .nvmrc + nvm installvolta install 保证一致性。
  • 锁文件应纳入版本控制,确保跨环境的依赖解析一致;升级依赖时使用 npm update 等命令并重新生成锁文件。
  • 针对多仓库或微服务体系,可结合版本管理器 + monorepo 包管理器(如 pnpm workspace、Yarn workspace)统一依赖;并利用 npm run/pnpm run 维护脚本统一入口。
  • 定期 npm auditpnpm audit 检查安全漏洞;对于私有 registry,配置 .npmrcyarnrc.yml 确保认证信息安全管理。

Node 学习大纲与要点说明

  • Node 的安装与配置,使用 NPM:从零搭建运行环境,熟悉 nodenpm 基础命令,为后续开发铺路。
  • 使用 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 cipruneaudit 等高级能力,保持依赖安全稳定。
  • Node 异步编程详解:对比回调、Promise、async/await 等模式,掌握错误处理与并发控制。
  • Node 流分析:学习 Readable/Writable/Transform 流以及背压控制处理大文件与网络传输。
  • 输入与输出:掌握文件 IO 与标准输入输出 API,构建 CLI 或脚本工具。
  • Node 网络功能:使用 http/https/net 模块搭建网络服务,理解底层 Socket 工作方式。
  • Node 的控制台:利用 console 家族进行调试、计时、表格输出等快速诊断。
  • 事件循环机制:掌握事件循环各阶段与微任务执行顺序,避免阻塞主线程。
  • Node 调试:使用内置调试器、Chrome DevTools 或 VS Code 断点调试定位问题。
  • 使用 exports 对象:区分 exportsmodule.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.jsonmain 字段指向 CommonJS 入口;若存在 exports 字段,则优先生效并可定义子路径导出(如 "./api": "./dist/api.js")。
  • 未指定入口时,Node 会尝试 index.jsindex.jsonindex.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 = valueexport { 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') 用于确认端口绑定情况,如需在连接建立或关闭时扩展逻辑,可继续监听 connectionclose 等事件。

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.parseurl.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.parsequerystring.stringifyquerystring.escapequerystring.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.typesutil.inherits 等工具也常出现在底层库中:前者用于精准判断 Buffer、TypedArray 等结构,后者用于帮助 ES5 风格的原型继承。

小结:开发调试时可以通过 util.format/util.inspect 提升日志质量(搭配 colors: true 让对象输出带颜色),使用 util.promisify/util.callbackify 统一异步调用风格,并借助 util.debuglogutil.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.reverselookupService:前者根据 IP 做反向解析,后者将 IP + 端口解析为主机名与服务名(基于 /etc/services)。
  • dns.promisesResolver:提供 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

(0)
Walker的头像Walker
上一篇 2025年11月25日 16:00
下一篇 2025年11月25日 15:00

相关推荐

  • Go工程师体系课 007【学习笔记】

    商品微服务 实体结构说明 本模块包含以下核心实体: 商品(Goods) 商品分类(Category) 品牌(Brands) 轮播图(Banner) 品牌分类(GoodsCategoryBrand) 1. 商品(Goods) 描述平台中实际展示和销售的商品信息。 字段说明 字段名 类型 说明 name String 商品名称,必填 brand Pointer …

    个人 2025年11月25日
    6100
  • Nuxt3_扫盲 入门与原理介绍【学习笔记】

    Nuxt 3 入门与原理介绍 💡 什么是 Nuxt 3? Nuxt 3 是基于 Vue 3 和 Vite 打造的全栈前端框架,支持: 服务端渲染(SSR) 静态站点生成(SSG) 单页应用(SPA) 构建全栈应用(支持 API) Nuxt 3 是 Vue 的“加强版”,帮你简化项目结构和开发流程。 🔧 核心原理 功能 Nuxt 如何处理 ✅ 页面路由 自动根…

    个人 2025年4月6日
    1.8K00
  • 深入理解ES6 009【学习笔记】

    javascript中的类 function PersonType(name){ this.name = name; } PersonType.prototype.sayName = function(){ console.log(this.name) } var person = new PersonType("Nicholas") p…

    个人 2025年3月8日
    1.1K00
  • 向世界挥手,拥抱无限可能 🌍✨

    站得更高,看到更远 生活就像一座座高楼,我们不断向上攀登,不是为了炫耀高度,而是为了看到更广阔的风景。图中的两位女孩站在城市之巅,伸展双手,仿佛在迎接世界的无限可能。这不仅是一次俯瞰城市的旅程,更是对自由和梦想的礼赞。 勇敢探索,突破边界 每个人的生活都是一场冒险,我们生而自由,就该去探索未知的风景,去经历更多的故事。或许路途中会有挑战,但正是那些攀爬的瞬间…

    个人 2025年2月26日
    1.1K00
  • Go工程师体系课 004【学习笔记】

    需求分析 后台管理系统 商品管理 商品列表 商品分类 品牌管理 品牌分类 订单管理 订单列表 用户信息管理 用户列表 用户地址 用户留言 轮播图管理 电商系统 登录页面 首页 商品搜索 商品分类导航 轮播图展示 推荐商品展示 商品详情页 商品图片展示 商品描述 商品规格选择 加入购物车 购物车 商品列表 数量调整 删除商品 结算功能 用户中心 订单中心 我的…

    个人 2025年11月25日
    5100

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
欢迎🌹 Coding never stops, keep learning! 💡💻 光临🌹