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