Node.js 基础复习 - 速通版 02
- 2026-10-12 09:12:20
// CommonJS(Node 默认,历史包袱)const fs = require('node:fs');module.exports = { sayHi() {} };// ESM(现代标准,浏览器 / Node 通用)import fs from 'node:fs';export function sayHi() {}
require) | import) | |
|---|---|---|
if 里 | import 必须在顶层(动态 import() 除外) | |
exports | undefined | |
__dirname / __filename / require | import.meta.url 拼 | |
// 特点:代码执行到 require() 这一行才加载模块,同步阻塞。// 可以放在条件判断、函数内部。if (true) {const util = require('./util') // ✅ 合法,运行到这里才加载console.log(util.msg)}
// 特点:JS 引擎执行代码前扫描所有静态 import,提前解析依赖。// 静态 import不能放在 if / 函数里面。// if(true) { import {msg} from './util' } // ❌ 语法报错!静态import不能写if里import { msg } from './util' // ✅ 只能写顶层// 想要条件加载,用动态 import(),它返回Promiseif(true) {const util = await import('./util') // ✅ 动态import,运行时异步加载console.log(util.msg)}
导出的是 值的副本。原始模块内部变量后续修改,导入方拿到的 不会跟着变。
// util.cjslet count = 1module.exports = {count}setTimeout(()=>{count = 100}, 100)// main.cjsconst m = require('./util')console.log(m.count) // 1setTimeout(()=>{console.log(m.count) // 仍然是1!不会变成100(简单类型值拷贝)},200)
注意:如果导出 对象引用,修改对象属性会变,因为拷贝的是对象引用,不是对象本身。
// util.cjslet obj = {count:1}module.exports = {obj}setTimeout(()=> obj.count=100,100)// main.cjsconst m = require('./util')// 200ms后 m.obj.count =>100,拷贝的是对象引用,不是基础值
不是拷贝值,建立 变量引用绑定。原模块变量修改,导入方 自动看到 最新值。
// util.mjsexport let count = 1setTimeout(()=>{count = 100},100)// main.mjsimport { count } from './util'console.log(count) // 1setTimeout(()=>{console.log(count) // ✅ 100!自动同步,live binding},200)
总结:CJS 导出 值快照(指针值/浅拷贝);ESM 导出 变量的绑定关系
CommonJS:顶层 this 指向 `module.exports` (本质上被一层看不见的函数包裹着)
console.log(this === module.exports) // truethis.name = "cjs"console.log(module.exports.name) // cjs
ESM:顶层 this = undefined
// esm.mjsconsole.log(this) // undefined
1.4 __dirname /__filename (两个都是绝对路径)
// xxx.cjsconsole.log(__filename) // 当前文件绝对路径console.log(__dirname) // 当前文件所在目录const fs = require("fs") // require直接可用
// xxx.mjsimport { fileURLToPath } from 'node:url'import { dirname, join } from 'node:path'const __filename = fileURLToPath(import.meta.url)const __dirname = dirname(__filename)console.log(__filename, __dirname)
CommonJS:不支持 Tree-Shaking
require 是运行时动态引入,打包工具(webpack/rollup)无法静态判断哪些导出会被用到(无法构建依赖图),不能删除未引入代码。
// util.cjsexports.foo = ()=>{}exports.bar = ()=>{}// main.cjsconst { foo } = require('./util')// 打包时:bar 函数代码依旧被打包进去,删不掉
ESM:支持 Tree-Shaking
静态`import/export`编译期可分析,打包器能识别哪些导出 没有被导入,打包时直接删除 未引入的代码。
// util.mjsexport const foo = ()=>{}export const bar = ()=>{}// main.mjsimport { foo } from './util'// bar没有被引入,打包时直接删掉bar代码,减少包体积
1.6 模块化特性
文件扩展名怎么判断模块系统
.cjs | ||
.mjs | ||
.js | package.json | "type": "module" |
模块缓存:require 第二次不会重新执行
// counter.jslet count = 0;module.exports.add = () => ++count;module.exports.get = () => count;// main.jsconst a = require('./counter');const b = require('./counter'); // 同一个实例!a.add(); a.add();console.log(b.get()); // 2,不是 1// 缓存对象挂在 require.cache,key 是文件绝对路径
ESM 里的 `import` 本来就可以导入 CommonJS(很早就支持)
// util.cjs 【CommonJS模块】const hello = "来自CJS的hello"function sayHi() {console.log("hi from cjs")}module.exports = {hello,sayHi}// main.mjs 【ESM模块,package.json 需要 "type":"module"】// ✅ ESM import 引入 CJSimport cjsMod from './util.cjs'console.log(cjsMod.hello) // 来自CJS的hellocjsMod.sayHi() // hi from cjs
示例2
// main.mjsimport { hello, sayHi } from './util.cjs'console.log(hello)
CJS 不存在真正的命名导出,这种只是语法糖。不能用于 Tree-Shaking!
ESM 导入 CJS 时: CJS 的 `module.exports` 对应 ESM 的默认导出;CJS 没有真实命名导出,`import {xx}` 只是 Node 帮你从 default 对象解构,无法 tree-shake。
示例3
// util.cjsmodule.exports = 123// main.mjsimport num from './util.cjs'console.log(num) // 123
2)Node 22.12+ 默认支持 require() 加载 ESM
// 旧版本// CJS 旧版本写法,只能异步const m = await import('./esm.mjs') // 异步,返回Promise
// 新版本(22.12+)// esm.mjs (ESM模块,package.json "type":"module")export const msg = "我是ESM模块"// ⚠️ 重点:ESM内部**不能有顶层 await**!// 如果有 top-level await,// require 加载直接抛 ERR_REQUIRE_ASYNC_MODULE// index.cjs(CommonJS模块)const esmModule = require('./esm.mjs') // ✅ Node>=22.12 可以直接写console.log(esmModule.msg) // 我是ESM模块
注意:ESM 一旦有顶层 await,它本质是异步模块;而 `require` 是同步函数,无法等待,直接报错:
`ERR_REQUIRE_ASYNC_MODULE`
这种场景依旧只能用动态 `import()`。
2. 核心模块 path / fs / http
import path from 'node:path';path.join('src', 'index.js'); // 拼接(相对/绝对都行)path.resolve('src', 'index.js'); // 基于 cwd 解析成绝对路径path.basename('/a/b/c.js'); // 'c.js'
path.dirname('/a/b/c.js'); // '/a/b'path.extname('/a/b/c.js'); // '.js'// { dir:'/a/b', base:'c.js', name:'c', ext:'.js' }path.parse('/a/b/c.js');path.join('/a', 'b', '..', 'c'); // '/a/c'(自动规范化)
fs:同步 vs 异步
import { readFileSync } from 'node:fs';import * as fsp from 'node:fs/promises';// 同步:阻塞主线程,只在启动配置加载时用const config = readFileSync('./config.json', 'utf8');// 异步 Promise 版(推荐):不阻塞事件循环const data = await fsp.readFile('./data.txt', 'utf8');
// 常用 fs 操作(Promise 版)import * as fsp from 'node:fs/promises';await fsp.mkdir('./new-dir', { recursive: true }); // 递归创建目录await fsp.readdir('./src'); // 列目录 → ['a.js','b.js']await fsp.writeFile('./out.txt', 'hello'); // 写文件await fsp.rm('./old', { recursive: true, force: true }); // 删除目录// stat:判断是文件还是目录const s = await fsp.stat('./a.txt');s.size; // 字节数s.isFile(); // trues.isDirectory(); // false
http:手写一个最精简服务
import http from 'node:http';const server = http.createServer((req, res) => {// req: IncomingMessage(url, method, headers)// res: ServerResponse(writeHead / end)res.writeHead(200, { 'Content-Type': 'application/json' });res.end(JSON.stringify({ url: req.url, method: req.method }));});server.listen(3000, () => console.log('listening on :3000'));
带路由 + POST body 解析的完整版
import http from 'node:http';import { URL } from 'node:url';const server = http.createServer(async (req, res) => {// new URL(urlString, baseURL)// 例如浏览器访问:// `http://localhost:3000/api/users?name=zhangsan`// `req.url` 的值是:`/api/users?id=xxxxxx`const url = new URL(req.url, 'http://localhost');// GET /users?id=1if (req.method === 'GET' && url.pathname === '/users') {const id = url.searchParams.get('id');res.writeHead(200, { 'Content-Type': 'application/json' });return res.end(JSON.stringify({ id, name: 'Tom' }));}// POST /users —— req 是 Readable Stream,手动拼 chunkif (req.method === 'POST' && url.pathname === '/users') {const chunks = [];for await (const chunk of req) chunks.push(chunk);const body = JSON.parse( Buffer.concat(chunks).toString() );res.writeHead(201, { 'Content-Type': 'application/json' });return res.end(JSON.stringify({ created: body }));}res.writeHead(404, { 'Content-Type': 'application/json' });res.end(JSON.stringify({ error: 'Not Found' }));});server.listen(3000)
为什么手动收集 body?req 是 Readable Stream,POST 数据分块到达, 生产里用 Express/Nest 中间件自动解析。
在工程和计算机领域,backpressure 通常指系统处理数据流时,下游因过载 导致 上游被迫减速或暂停 的现象。
流 = 分段读写数据,不用一次性把整个文件读进内存。四种类型:
fs.createReadStream | ||
fs.createWriteStream | ||
zlib.createGzip |
// 推荐:pipe 自动处理背压import { createReadStream, createWriteStream } from 'node:fs';import { gzip } from 'node:zlib';createReadStream('big.log') // readable stream.pipe(gzip()) // transform stream.pipe(createWriteStream('big.log.gz')); // writable stream
更安全的写法:pipeline(自动处理错误),pipeline 会在任何一个流报错时自动销毁所有流,防止内存泄漏。
文件压缩:
import { pipeline } from 'node:stream/promises';import { createReadStream, createWriteStream } from 'node:fs';import { gzip } from 'node:zlib';// pipeline 会在任何一个流报错时自动销毁所有流,防止内存泄漏await pipeline(createReadStream('big.log'),gzip(),createWriteStream('big.log.gz'));console.log('压缩完成'); // 压缩文件完成
手动监听 data 事件(理解流的本质)
import { createReadStream } from 'node:fs';const readable = createReadStream('large.txt',{ encoding: 'utf8' });readable.on('data', (chunk) => {console.log('收到一块:', chunk.length);// chunk 默认是 Buffer,指定 encoding 后是 string});readable.on('end', () => console.log('读完了'));readable.on('error', (err) => console.error(err));
案例:HTTP 文件下载接口 ,用 pipe(管道) 把 文件流 直接写到响应,不会占满内存。
import http from 'node:http';import { createReadStream } from 'node:fs';const server = http.createServer((req, res) => {res.writeHead(200, { 'Content-Type': 'application/pdf' });// 用 pipe 把 文件流 直接写到响应,不会占满内存createReadStream('./report.pdf').pipe(res);})server.listen(8080);
什么是背压?
当读速(读进来) > 写速(写出去),数据在内存里堆着不释放,最终把进程吃爆。
.pipe() / pipeline() 会自动暂停可读流、等写入队列排空再继续,这就是背压处理。
Duplex 双工流:同时实现 Readable(可读)+ Writable(可写):
// server.js TCP服务端,net.Socket 是原生 Dupleximport net from 'node:net'const server = net.createServer((socket) => {console.log('客户端连接成功')// socket 是 Duplex:可读 + 可写,读写通道独立// 可读:监听客户端发来的数据socket.on('data', (chunk) => {console.log('服务端收到客户端:', chunk.toString())})// 可写:发消息给客户端socket.write('服务端:hi, I am Duplex net.Socket\n')socket.on('close', () => {console.log('连接断开')})})server.listen(8080, '127.0.0.1', () => {console.log('TCP server listen on 127.0.0.1:8080')})
// client.js TCP客户端,clientSocket同样是Dupleximport net from 'net'const clientSocket = net.connect(8080, '127.0.0.1')// 可读:接收服务端推送clientSocket.on('data', (chunk) => {console.log('客户端收到服务端:', chunk.toString())})// 可写:发送给服务端clientSocket.write('客户端 hello')clientSocket.on('close', () => {console.log('客户端连接关闭')})
先梳理这么多,下一波梳理:Buffer 二进制数据、 并发、 process 与异常处理。