Node.js 开发手册NODE.JS

CHAPTER 01语言基础 · BASICS

Node.js 与语言基础

模块系统、全局对象、Buffer、事件与 process。理解"单线程事件驱动"的执行模型,是后续掌握异步与并发的前提。

开始阅读难度入门先修JS 基础约2小时
01.1

Node 的执行模型

Node.js 是一个用 V8 跑 JS、用 libuv 提供异步 I/O 的运行时。主线程跑 JS,I/O 与耗时任务交给 libuv 线程池与操作系统,完成后以事件形式回到主线程。

单线程不代表低效

"单线程"指的是 JS 执行在主线程;网络、文件、定时器等的等待由 libuv 在后台处理,完成时再排队回到主线程执行回调。所以阻塞主线程(如大循环、同步读大文件)才是性能杀手。

01.2

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)); // 3
循环依赖

CommonJS 在循环依赖时可能拿到"未完成的 exports"。尽量让依赖单向,必要时改用函数延迟调用打破环。

01.3

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 最常见的迁移坑。

01.4

全局对象与 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));
01.5

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。

01.6

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 预警。

01.7

路径与文件系统

用 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 上出错。

01.8

常见陷阱与 FAQ

ESM 里没有 __dirname 怎么办?

用 fileURLToPath(import.meta.url) 配合 dirname 推导,见 01.4 示例。或从 Node 20.11 起使用实验性 import.meta.dirname。

CommonJS 和 ESM 能混用吗?

可以。ESM 能用 import 加载 CommonJS 默认导出;CommonJS 用 require 加载 ESM 需走动态 import()。命名导出在跨体系时语义有差异,迁移时优先统一到 ESM。

为什么我的 CPU 密集型任务卡住了?

主线程被长时间计算占用,事件循环无法处理其他请求。应把重计算放进 worker_threads(见性能章)或拆成小任务用 setImmediate 让出。

01.9

实战练习与资源

完成以下练习,你就能独立搭起 Node 项目骨架。

延伸资源