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

相关推荐

  • 深入理解ES6 011【学习笔记】

    Promise与异步编程 因为执行引擎是单线程的,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列中,每当一段代码准备执行时,都会被添加到任务队列中,每当引擎中的一段代码结束执行,事件循环会执行队列中的一下个任务。 Promise相当于异步操作结果占位符,它不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个Promise,就像这样…

    个人 2025年3月8日
    1.1K00
  • Go工程师体系课 003【学习笔记】

    grpc grpc grpc-go grpc 无缝集成了 protobuf protobuf 习惯用 Json、XML 数据存储格式的你们,相信大多都没听过 Protocol Buffer。 Protocol Buffer 其实是 Google 出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json、XML 真的强!太!多! protobuf…

    个人 2025年11月25日
    15600
  • Node深入浅出(圣思园教育) 002【学习笔记】

    node 的包管理机制和加载机制 npm search xxxnpm view xxxnpm install xxx nodejs 文件系统操作的 api Node.js 的 fs 模块提供同步(Sync)与基于回调/Promise 的异步 API,可以操作本地文件与目录。日常开发中常用的能力包括读取、写入、追加、删除、遍历目录、监听变化等。以下示例基于 C…

    个人 2025年11月24日
    19000
  • Go工程师体系课 005【学习笔记】

    微服务开发 创建一个微服务项目,所有的项目微服务都在这个项目中进行,创建joyshop_srv,我们无创建用户登录注册服务,所以我们在项目目录下再创建一个目录user_srv 及user_srv/global(全局的对象新建和初始化)user_srv/handler(业务逻辑代码)user_srv/model(用户相关的 model)user_srv/pro…

    个人 2025年11月25日
    18300
  • Go工程师体系课 018【学习笔记】

    API 网关与持续部署入门(Kong & Jenkins) 对应资料目录《第 2 章 Jenkins 入门》《第 3 章 通过 Jenkins 部署服务》,整理 Kong 与 Jenkins 在企业级持续交付中的实战路径。即便零基础,也能顺着步骤搭建出自己的网关 + 持续部署流水线。 课前导览:什么是 API 网关 API 网关位于客户端与后端微服务…

    个人 2025年11月25日
    15000

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

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

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