Node深入淺出(聖思園教育) 001

23次閱讀

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,即可看到成功與失敗兩種響應結果。

正文完
 0