Node 的执行模型
Node.js 是一个用 V8 跑 JS、用 libuv 提供异步 I/O 的运行时。主线程跑 JS,I/O 与耗时任务交给 libuv 线程池与操作系统,完成后以事件形式回到主线程。
"单线程"指的是 JS 执行在主线程;网络、文件、定时器等的等待由 libuv 在后台处理,完成时再排队回到主线程执行回调。所以阻塞主线程(如大循环、同步读大文件)才是性能杀手。
CommonJS 模块
传统 Node 模块系统:每个文件是一个模块,require 同步加载,module.exports 导出。模块在首次加载时执行并缓存。
// math.cjs
function add(a, b) { return a + b; }
module.exports = { add };
// index.cjs
const { add } = require('./math.cjs');
console.log(add(1, 2)); // 3CommonJS 在循环依赖时可能拿到"未完成的 exports"。尽量让依赖单向,必要时改用函数延迟调用打破环。
ESM 模块
ESM 是标准模块语法:静态 import/export,可被静态分析、tree-shaking。需在 package.json 设 "type": "module" 或用 .mjs。
// math.mjs
export function add(a, b) { return a + b; }
// index.mjs
import { add } from './math.mjs';
console.log(add(1, 2));
// 动态导入(按需、返回 Promise)
const mod = await import('./math.mjs');ESM 中 import 必须带完整扩展名(./math.mjs 而非 ./math),且不解析省略扩展名。这是与 CommonJS 最常见的迁移坑。
全局对象与 process
process 提供当前进程信息与控制:环境变量、命令行参数、退出码、信号监听。
console.log(process.version); // v20.x.x
console.log(process.env.PORT); // 读取环境变量
console.log(process.argv); // [node, 脚本, ...参数]
process.on('SIGINT', () => { // Ctrl+C
console.log('graceful shutdown');
process.exit(0);
});
// __dirname / __filename 在 ESM 中需用 url 推导
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));Buffer 与二进制
Buffer 是 Node 处理二进制数据的视图,常用于网络、文件、编码转换。不要拿普通字符串当二进制用。
const buf = Buffer.from('你好', 'utf8');
console.log(buf); // <Buffer e4 bd a0 ...>
console.log(buf.toString('utf8'));// 你好
// 拼接网络分片时务必用 Buffer.concat,避免乱码
const chunks = [];
stream.on('data', c => chunks.push(c));
stream.on('end', () => Buffer.concat(chunks));中文等多字节字符按字节切片会切断字符导致乱码。要"按消息边界"或"按行"切分,而不是按固定字节数切分 Buffer。
EventEmitter
Node 核心的发布订阅原语。很多对象(stream、server、process)都继承自它。用它解耦"产生事件"与"处理事件"。
import { EventEmitter } from 'node:events';
const bus = new EventEmitter();
bus.on('order', (id) => console.log('新订单', id));
bus.emit('order', 1001); // 新订单 1001
// 只监听一次
bus.once('ready', () => console.log('启动完成'));忘记移除监听器会堆积内存。长期订阅记得 off;或设置 setMaxListeners 预警。
路径与文件系统
用 node:path 拼接路径(跨平台安全),用 node:fs 的 promise 版做异步读写。
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
const p = join(__dirname, 'data', 'a.json');
const text = await readFile(p, 'utf8');
const json = JSON.parse(text);用 path.join / path.resolve,不要用 'dir/' + file,否则在 Windows 上出错。
常见陷阱与 FAQ
用 fileURLToPath(import.meta.url) 配合 dirname 推导,见 01.4 示例。或从 Node 20.11 起使用实验性 import.meta.dirname。
可以。ESM 能用 import 加载 CommonJS 默认导出;CommonJS 用 require 加载 ESM 需走动态 import()。命名导出在跨体系时语义有差异,迁移时优先统一到 ESM。
主线程被长时间计算占用,事件循环无法处理其他请求。应把重计算放进 worker_threads(见性能章)或拆成小任务用 setImmediate 让出。
实战练习与资源
完成以下练习,你就能独立搭起 Node 项目骨架。