核心模块与异步编程——操控系统与掌控时间
摘要:本篇将系统学习 Node.js 最常用的核心模块:fs(文件系统)、path(路径)、http(网络)等,同时深入理解异步编程的演进:从回调地狱到 Promise、async/await,以及 Node 事件循环的运行机制。掌握了这些,你就真正站在了中级 Node.js 开发者的起点。
一、核心模块总览
Node.js 把一系列功能封装为内置模块,不需要安装即可使用。最重要的有:
fs:文件系统操作,读写文件、目录管理等。
path:路径拼接、解析、规范化,跨平台。
http/https:创建 HTTP(S) 服务器和客户端请求。
os:操作系统信息,CPU、内存等。
events:事件触发器,实现发布订阅模式。
stream:流接口,处理大文件或数据流。
crypto:加密和哈希算法。
process:当前进程信息、环境变量等。
我们先从最常用的fs和path开始。
二、path 模块:路径处理的艺术
不同操作系统路径分隔符不同(Windows 是\,类 Unix 是/),手动拼接极易出错。path模块帮我们消除平台差异。
const path = require('path'); // 路径拼接 const fullPath = path.join('/users', 'alice', 'docs', 'file.txt'); console.log(fullPath); // 在 Windows 下是 \users\alice\docs\file.txt // 获取文件名、扩展名、目录名 console.log(path.basename(fullPath)); // file.txt console.log(path.extname(fullPath)); // .txt console.log(path.dirname(fullPath)); // /users/alice/docs // 解析为对象 console.log(path.parse(fullPath)); /* { root: '/', dir: '/users/alice/docs', base: 'file.txt', ext: '.txt', name: 'file' } */ // 规范化怪异路径 console.log(path.normalize('/foo/bar//baz/asdf/quux/..')); // /foo/bar/baz/asdf绝对路径与相对路径的转换:
// 解析相对路径为绝对路径(以当前工作目录为基础) console.log(path.resolve('src', 'app.js')); // 如当前在 /home/project,输出 /home/project/src/app.js三、fs 模块:操作文件系统
fs模块提供同步和异步两套 API,异步 API 又有回调、Promise 两种形式(Node 10+ 支持fs.promises)。
3.1 读取文件
创建example.txt,内容任意。然后:
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'example.txt'); // 异步回调方式 fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error('读取失败', err); return; } console.log('文件内容:', data); }); // 同步方式(会阻塞,仅在小脚本或启动时用) try { const data = fs.readFileSync(filePath, 'utf8'); console.log(data); } catch (err) { console.error(err); } // Promise 方式(现代推荐) const fsp = fs.promises; async function read() { try { const data = await fsp.readFile(filePath, 'utf8'); console.log(data); } catch (err) { console.error(err); } } read();3.2 写入与追加
const fs = require('fs'); const content = 'Hello, Node.js 文件操作!\n'; // 写(覆盖) fs.writeFile('output.txt', content, 'utf8', (err) => { if (err) throw err; console.log('写入成功'); }); // 追加 fs.appendFile('output.txt', '追加一行\n', 'utf8', (err) => { if (err) throw err; });3.3 目录操作
const fs = require('fs'); // 创建目录 fs.mkdir('new-folder', { recursive: true }, (err) => { if (err) throw err; console.log('目录已创建'); }); // 读取目录内容 fs.readdir('.', (err, files) => { if (err) throw err; console.log('当前目录内容:', files); }); // 删除目录(需为空目录) fs.rmdir('new-folder', (err) => { if (err) throw err; console.log('目录已删除'); }); // 更强大的删除(Node 14.14+) fs.rm('some-folder', { recursive: true, force: true }, () => {});四、http 模块深入:构建一个简单的静态服务器
第一篇我们已经创建了最简服务器,现在加入路由处理和静态文件服务。
const http = require('http'); const fs = require('fs'); const path = require('path'); const server = http.createServer((req, res) => { if (req.url === '/' || req.url === '/index.html') { const filePath = path.join(__dirname, 'public', 'index.html'); fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('服务器错误'); } else { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(data); } }); }else if (req.url === '/api/hello') { // 简单的 JSON 接口 res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: '你好,这是 API 响应' })); } else { res.writeHead(404, { 'Content-Type': 'text/html' }); res.end('<h1>404 页面未找到</h1>'); } }); server.listen(3000, () => { console.log('静态服务器运行在 http://localhost:3000'); });从这里你可以看到,Node 的原生 http 模块比较底层,路由、静态资源、参数解析都要自己处理。这也解释了为什么会有 Express、Koa 等框架出现(下篇讲解)。
五、理解 Node.js 的异步编程模型
5.1 回调函数(Callback)
Node 最早期的异步模式就是 error-first 回调:(err, result) => {}。
fs.readFile('file.txt', 'utf8', (err, data) => { if (err) return console.error(err); // 处理 data });当异步操作嵌套时,就形成了臭名昭著的“回调地狱”:
doA(function(resultA) { doB(resultA, function(resultB) { doC(resultB, function(resultC) { console.log('最终结果', resultC); }); }); });5.2 Promise 登场
Promise 是一个代表异步操作最终完成或失败的对象。它有三种状态:pending、fulfilled、rejected。
const readFilePromise = (filePath) => { return new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (err, data) => { if (err) reject(err); else resolve(data); }); }); }; readFilePromise('file.txt') .then(data => console.log(data)) .catch(err => console.error(err));链式调用避免了深层嵌套,且错误可以被统一捕获。
5.3 async/await:终极解决方案
async/await是 Promise 的语法糖,让异步代码看起来像同步。
const fsp = fs.promises; async function processFile() { try { const data = await fsp.readFile('input.txt', 'utf8'); const processed = data.toUpperCase(); await fsp.writeFile('output.txt', processed); console.log('处理完成'); } catch (err) { console.error('出错:', err); } } processFile();await会暂停函数执行直到 Promise 完成,不会阻塞事件循环。错误处理使用try/catch。
六、事件循环与事件驱动
“Node.js 是单线程的”,这句话经常引起误解。更准确的说法是:JavaScript 代码执行在单个主线程上,但 I/O 等操作由底层线程池(libuv)异步执行。事件循环是协调这些任务的调度中心。
6.1 libuv 和线程池
Node.js 底层使用 libuv 库提供事件循环和异步 I/O。对于文件操作、DNS 解析等阻塞任务,libuv 使用线程池(默认 4 个线程)来执行,从而不阻塞主线程。网络 I/O 则由操作系统提供异步原语,不需要占用线程。
6.2 事件循环的阶段
事件循环分为多个阶段,每个阶段处理特定的回调队列:
timers:执行
setTimeout、setInterval的回调。pending callbacks:执行延迟到下一个循环迭代的 I/O 回调(某些系统操作)。
idle, prepare:内部使用。
poll:检索新的 I/O 事件;执行 I/O 相关回调。
check:执行
setImmediate的回调。close callbacks:如
socket.on('close', ...)。
微任务(Microtasks)在每个阶段结束后执行,包括process.nextTick和 Promise 回调。nextTick优先级高于 Promise。
演示一下执行顺序:
console.log('1'); setTimeout(() => console.log('2'), 0); setImmediate(() => console.log('3')); Promise.resolve().then(() => console.log('4')); process.nextTick(() => console.log('5')); console.log('6');输出:1, 6, 5, 4, 2, 3或1, 6, 5, 4, 3, 2(timers 与 check 的先后受性能影响,通常setImmediate在setTimeout(fn,0)之前或之后,取决于执行环境,但微任务总是在中间)。
理解事件循环能帮助你写高效代码,避免阻塞循环。
七、events 模块:发布/订阅模式
许多 Node 核心模块如http、stream都继承自EventEmitter。
const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); // 注册监听 myEmitter.on('event', (arg1, arg2) => { console.log('事件触发', arg1, arg2); }); // 触发 myEmitter.emit('event', 'hello', 42); // 输出:事件触发 hello 42常用方法:
on(event, listener)/addListeneronce(event, listener):只触发一次emit(event, [args])removeListener(event, listener)
八、总结
本篇内容较多但非常重要。建议你亲手将每个示例敲一遍,特别是异步写法。可以这样练习:写一个脚本,读取目录下所有.txt文件,拼接内容后写入一个新文件,过程中体会fs、path和async/await的配合。结合事件循环的演示代码,在node下运行观察输出顺序。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。
