Node.js 20 LTS入门:环境搭建与ES Modules配置
搞后端开发,第一步肯定是先把环境搞定。打个比方,选对版本比写代码还重要。现在都2024年了,如果你还在用 Node.js 14 或者 16,那真的得赶紧升级了。目前 Node.js 22.x 是 Current 版本(2024年4月刚发),功能很新但可能不太稳。对于咱们正儿八经搞开发或者生产环境,Node.js 20.x LTS (代号 Iron,2023年10月发布) 才是官方推荐的首选,稳定性没得说,性能也强。
安装与版本管理
新手直接去 Node.js 官网下那个 .msi 或者 .pkg 安装包也行,但我强烈建议用 nvm (Node Version Manager)。这玩意儿能让你在电脑上同时装好几个版本的 Node,切起来跟换衣服一样方便。
安装好 nvm 后,打开终端敲这行命令,直接装 20 版本:
nvm install 20
nvm use 20
装完输入 node -v,看到 v20.x.x 的字样就对了。
告别 CommonJS,拥抱 ES Modules
以前的老教程里全是 require 和 module.exports,那是 CommonJS 规范。现在都2024年了,咱们得跟上时代,ES Modules (ESM) 才是主流。Node.js 20 对 ESM 的支持已经非常完善了,咱们直接用 import/export 语法,写起来跟前端 React/Vue 一模一样。
不过这里有个坑,Node 默认还是把文件当 CommonJS 处理。怎么开启 ESM 模式?有两个办法:
- 在
package.json 里加一行 "type": "module"。
- 把文件后缀名改成
.mjs。
我推荐第一种,毕竟谁也不想满屏都是 .mjs 文件。
实战配置示例
咱们来搭个最简单的架子。先初始化项目:
mkdir my-node-app
cd my-node-app
npm init -y
然后修改生成的 package.json,注意看 "type" 字段:
{
"name": "my-node-app",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {}
}
接着新建一个 index.js,写点现代语法试试:
// index.js
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
// 注意,Node 20 支持 node: 协议前缀,更清晰
const filePath = join(import.meta.url.replace('file://', ''), '../package.json');
try {
// 读取当前目录的 package.json
const data = readFileSync(new URL('./package.json', import.meta.url), 'utf-8');
const json = JSON.parse(data);
console.log(`项目名:${json.name}`);
console.log(`版本号:${json.version}`);
console.log(`当前使用的是 ES Modules 规范`);
} catch (err) {
console.error('读取文件出错了', err);
}
运行 node index.js,如果打印出了项目信息,说明你的 ESM 环境已经跑通了。
📌 要点提醒
值得留意的是,在 ESM 里,没有 __dirname 和 __filename 这两个变量。很多新手会经验之谈。如果你需要获取当前文件路径,得用 import.meta.url。另外,导入本地文件时,必须写全后缀名(比如 ./utils.js),不能像 CommonJS 那样省略 .js,否则会报找不到模块的错误。
核心机制解析:非阻塞I/O与事件循环深度剖析
很多前端同学转后端,听到“单线程”就慌了,心想:“单线程能抗住高并发吗?不是必死无疑吗?” 其实这就是 Node.js 最迷人的地方。换个角度看,Node.js 虽然是单线程执行 JS 代码,但它背后的 Libuv 库帮你搞定了多线程的脏活累活。
单线程与事件循环 (Event Loop)
Node.js 的核心逻辑是:非阻塞 I/O + 事件驱动。
啥叫非阻塞?举个例子,你要去读一个很大的文件或者查数据库。
- 阻塞模式(传统 Java/PHP 同步写法):代码执行到读文件,线程就卡在那儿等着,文件读完之前,啥也干不了。
- 非阻塞模式(Node.js):代码发起读文件的操作,然后立马往下执行,去处理别的请求。等文件读完了,系统会发个通知,Node 再通过事件循环把这个后续处理(回调函数或者 Promise)捞起来执行。
这个“捞起来”的过程就是 事件循环 (Event Loop)。它跟浏览器的事件循环有点像,但更复杂一些。Node.js 把任务分成了几个阶段(Timers, Pending Callbacks, Idle/Prepare, Poll, Check, Close Callbacks)。
实际案例预警:面试里经常问 setTimeout 和 setImmediate 谁先执行?大部分情况下 setImmediate 在 I/O 循环里会比 setTimeout 快,但在最外层执行顺序是不确定的。不过咱们平时写业务逻辑,别去纠结这个,知道它在 Poll 阶段后会进入 Check 阶段执行 setImmediate 就行。
代码演示并发处理
咱们写个代码看看,Node 是怎么一边等定时器,一边还能干别的事的。
// event-loop-demo.js
import { readFile } from 'node:fs';
console.log('1. 开始执行,我是同步代码');
// 模拟一个耗时的 I/O 操作
readFile('./event-loop-demo.js', 'utf-8', (err, data) => {
if (err) throw err;
console.log('4. 文件读取完成 (I/O 操作结束)');
});
// 定时器
setTimeout(() => {
console.log('3. 定时器 0ms 到了');
}, 0);
// 利用 Promise 的微任务
Promise.resolve().then(() => {
console.log('2. 我是微任务 (Microtask),比宏任务先执行');
});
console.log('5. 同步代码执行完毕,进入事件循环等待 I/O 或 定时器回调');
运行这段代码,你会发现输出顺序很有意思:
- 同步代码先跑。
- Promise 的微任务插队,比定时器快。
- 定时器回调。
- 最后才是文件读取完的回调(因为读文件是 I/O 操作,需要时间)。
这就证明了 Node.js 并没有被 readFile 阻塞住,它先跑完了下面的代码,等文件读完了再回头处理。
⚡ 效率提示
关键点:虽然 Node.js 能处理高并发 I/O,但它不适合干 CPU 密集型的活儿。比如你写个巨大的 for 循环做加密计算,或者处理几 G 的图片,那这个单线程就被卡死了,后面的请求都得排队等着。如果你的业务里有大量计算,要么拆成微服务,要么用 Worker Threads(工作线程),别把主线程给堵了。
实战:使用pnpm与Express构建RESTful API
光说不练假把式。咱们现在来实战一下,从零搭一个 API 服务。现在包管理器圈子挺热闹的,npm 虽然稳,但 pnpm 凭借着“硬链接”和“符号链接”的黑科技,在磁盘占用和安装速度上吊打 npm 和 yarn,现在社区里用的人越来越多了,咱们也赶个时髦。
初始化项目与安装依赖
首先,确保你装了 pnpm。没装的话跑一下 npm install -g pnpm。
mkdir node-api-demo
cd node-api-demo
pnpm init
记得在 package.json 里加上 "type": "module",开启 ESM 模式。
然后安装 Express。虽然现在有个新秀叫 Fastify 很快,但 Express 依然是生态最丰富、最好上手的框架,特别适合入门。
pnpm add express
编写 API 服务代码
咱们搞一个简单的“待办事项” (Todo List) API,包含获取列表和新增两个功能。
新建一个 server.js 文件:
// server.js
import express from 'express';
const app = express();
const PORT = 3000;
// 中间件:解析 JSON 格式的请求体
app.use(express.json());
// 临时数据库(存在内存里,重启就没了)
let todos = [
{ id: 1, title: '学习 Node.js 20 LTS', done: true },
{ id: 2, title: '搞定 Express 框架', done: false }
];
// 路由:获取所有 Todos
app.get('/api/todos', (req, res) => {
res.json({
success: true,
data: todos
});
});
// 路由:新增一个 Todo
app.post('/api/todos', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ success: false, message: 'title 是必填项' });
}
const newTodo = {
id: todos.length + 1,
title,
done: false
};
todos.push(newTodo);
res.status(201).json({ success: true, data: newTodo });
});
// 启动服务
app.listen(PORT, () => {
console.log(`🚀 服务器已启动,监听 http://localhost:${PORT}`);
console.log(`测试接口: GET http://localhost:${PORT}/api/todos`);
});
运行与测试
在 package.json 的 scripts 里加个启动命令:
"scripts": {
"dev": "node server.js"
}
然后运行 pnpm dev。打开浏览器或者使用 Postman/Apifox 访问 http://localhost:3000/api/todos,你就能看到 JSON 数据了。
⚡ 效率提示
关键点:在 Express 里,中间件(Middleware)是个核心概念。其实,它就是一层层的“过滤器”或“加工厂”。上面的 express.json() 就是一个内置中间件,它的作用就是把前端发过来的 JSON 字符串转成 JS 对象,挂在 req.body 上。如果你不写这一句,你拿到的 req.body 就是 undefined,这是新手最容易踩的坑之一。另外,返回值一定要用 return res.json(...) 或者确保后面没有代码执行,不然很容易造成“发送两次响应”的报错。
顺便提一嘴,现在 Node.js 20 的 fetch API 已经原生支持了,你甚至不用装 axios 就能直接在前端或者 Node 端发请求,生态越来越统一了。
4. 进阶:原生TypeScript支持与BFF层架构实践
说到Node.js的进阶玩法,2024年最值得关注的肯定是原生TypeScript支持了。以前我们写TS,还得装个ts-node或者用tsc编译一遍,挺麻烦的。现在Node.js 22.x(2024年4月刚发布的Current版本)已经在这方面迈出了很大一步,虽然还没到完全不需要配置的地步,但实验性的支持已经很香了。简单来说,就是你可以尝试直接跑.ts文件了,这对开发体验的提升是巨大的。
再聊聊BFF层(Backend for Frontend)。很多新手容易懵,这玩意儿到底是干嘛的?其实很简单,现在前端框架(React/Vue)火得一塌糊涂,前端直接调后端微服务,经常遇到一个问题:一个页面要调五六个接口,前端兄弟得写一堆请求,还要拼数据。BFF层就是解决这个问题——它就是一个专门服务于前端页面的Node.js后端,帮前端把数据聚合好,前端只要调一个接口就行。注意,BFF不是替代后端,而是前端的“贴身秘书”。
原生TypeScript的实验性体验
虽然Node.js 20.x LTS (Iron) 还是主流生产推荐,但如果你想尝鲜,Node.js 22.x 提供了--experimental-strip-types标志。这意味着Node.js可以剥离TypeScript的类型注解直接运行,而不需要完整的编译步骤。虽然这个功能还在迭代,但代表了未来的趋势(参考AI知识库提到的2024-2026发展趋势)。
我们来看个简单的例子,假设你装了Node.js 22.x:
// server.ts
import { createServer } from 'node:http';
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
];
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
});
// 监听端口
server.listen(3000, () => {
console.log('TypeScript服务跑在 http://localhost:3000 啦!');
});
运行方式(注意加flag):
node --experimental-strip-types server.ts
当然,目前生产环境还是建议用ts-node或者tsc编译,毕竟这功能还没稳。不过这释放了一个信号:Node.js正在努力向“开箱即用”靠拢,未来几年我们可能真的就告别繁琐的编译配置了。
实战:用Node.js搭建一个BFF层
BFF层的核心逻辑是“聚合”。假设你的前端需要展示用户信息和订单信息,这两个数据分别来自不同的后端微服务。
我们来写一个简单的BFF聚合接口。这里我直接用Node.js内置的http模块加上fetch(注意,Node.js 18+ 已经原生支持Fetch API了,不用再装axios了,这也是Web APIs兼容的大趋势)。
// bff-server.mjs
import { createServer } from 'node:http';
// 模拟两个后端微服务的地址
const USER_SERVICE = 'https://jsonplaceholder.typicode.com/users/1';
const POSTS_SERVICE = 'https://jsonplaceholder.typicode.com/posts?_limit=2';
const server = createServer(async (req, res) => {
// 简单路由,只处理 /api/dashboard
if (req.url === '/api/dashboard' && req.method === 'GET') {
try {
// 并行请求,性能更好
const [userRes, postsRes] = await Promise.all([
fetch(USER_SERVICE),
fetch(POSTS_SERVICE)
]);
const user = await userRes.json();
const posts = await postsRes.json();
// BFF层的价值:数据清洗和聚合
const dashboardData = {
userName: user.name,
email: user.email,
recentPosts: posts.map(p => ({ id: p.id, title: p.title }))
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(dashboardData));
} catch (error) {
res.writeHead(500);
res.end('BFF层挂了,后端服务可能没响应');
}
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3001, () => {
console.log('BFF服务启动在 3001 端口,快来 http://localhost:3001/api/dashboard 看看!');
});
运行这个文件(因为用了import和顶层await,记得文件后缀用.mjs或者在package.json里设置"type": "module",Node.js 20.x LTS 对此支持得很好):
node bff-server.mjs
- 📌 要点提醒:做BFF层时,千万别把复杂的业务逻辑写在这里,比如什么数据库事务、复杂的权限校验。BFF只负责数据的组装、格式的转换和简单的代理。如果业务逻辑太重,就丢给真正的后端微服务去处理,否则BFF层就会变成下一个维护噩梦。
5. 排查与面试:内存泄漏检测与高频面试题解析
做后端开发,最怕的不是代码写不出来,而是代码跑着跑着,服务器内存爆了。Node.js虽然是单线程,但如果不注意,内存泄漏能把你搞得焦头烂额。这一章咱们就聊聊怎么抓“内存小偷”,顺便把面试里那些绕晕人的问题给盘明白。
内存泄漏排查:别让内存偷偷溜走
简单来说,内存泄漏就是你的程序申请了一块内存,用完了却没还回去,或者因为某些引用没断掉,垃圾回收机制(GC)没法回收它。在Node.js里,最常见的坑就是全局变量滥用、闭包引用以及未清理的监听器。
#### 如何检测?
- 看监控:最直接的就是看部署平台的内存曲线,如果一直涨不跌,肯定有问题。
- 用工具:
node-memwatch(虽然老了点)或者Chrome DevTools。
- 代码层面:利用
process.memoryUsage()。
来个典型的“坑爹”代码示例,看看怎么通过代码监控内存:
// leak-demo.mjs
const bigArray = [];
function leakyFunction() {
// 值得留意的是,这里往全局数组里塞东西,GC永远回收不了
// 这就是典型的内存泄漏
bigArray.push(new Array(1000000).join('*'));
}
setInterval(() => {
leakyFunction();
// 打印当前内存使用情况
const mem = process.memoryUsage();
console.log(`堆内存使用: ${(mem.heapUsed / 1024 / 1024).toFixed(2)} MB`);
}, 1000);
如果你跑这个代码,会发现内存蹭蹭往上涨。
#### 排查实战建议
如果你发现线上服务内存异常,别慌。首先,Node.js 20.x LTS 提供了更好的诊断报告功能。你可以给进程发个信号生成报告,或者直接用--inspect参数启动,然后用Chrome浏览器打开chrome://inspect,连接上去。
在DevTools的Memory面板里,你可以拍个堆快照(Heap Snapshot)。对比两次快照,看看哪个对象(Object)数量或者大小涨得离谱。通常你会发现一堆string或者array关不掉,顺着引用链(Retainers)往上找,基本就能定位到是哪个变量没释放。
- 🔧 实战技巧:在写代码时,尽量避免往全局变量挂东西。如果是缓存,记得设置过期时间(TTL)。如果是EventEmitter,记得在不需要的时候
removeListener。我踩过的坑里,最多的就是socket连接断了,但监听器还在,导致那块内存一直被引用着,死活清不掉。
面试考点梳理解析
面试的时候,面试官特别爱问Node.js的底层机制。咱们结合AI知识库里的热点,扒一扒这几个必考题。
#### 1. 事件循环(Event Loop)和浏览器有啥区别?
这是必考题!简单来说,虽然都叫事件循环,但Node.js的更“偏执”一点。
- 浏览器:主要是渲染、用户交互、JS执行。宏任务主要是
setTimeout、setInterval、requestAnimationFrame。
- Node.js:基于Libuv库。它的事件循环分好几个阶段(Timers, Pending Callbacks, Idle/Prepare, Poll, Check, Close Callbacks)。
- 关键区别:Node.js有
process.nextTick(微任务,但在Promise之前执行)和setImmediate(在Poll阶段之后,Check阶段执行)。
面试回答技巧:你可以说,Node.js的事件循环更复杂,专门为了非阻塞I/O设计。比如,I/O操作大多在Poll阶段处理,而setImmediate是Node.js特有的,用来在当前轮询阶段完成后执行回调。
#### 2. 中间件(Middleware)原理是什么?
如果你用过Express或Koa,面试官肯定问这个。可以这么理解,中间件就是一个个函数,像流水线一样处理请求。
拿Koa举例(因为Koa的洋葱模型比较好理解):
// 伪代码演示洋葱模型原理
const middlewareStack = [];
function use(fn) {
middlewareStack.push(fn);
}
// 模拟执行
function compose(middlewares) {
return function(context) {
let index = -1;
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
let fn = middlewares[i];
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
面试官想听的是:中间件通过next()函数串联,形成一个调用链。请求进来,先经过第一个中间件,执行next()进入下一个,最后一个中间件执行完再倒序返回执行前面的代码(洋葱圈模型)。
#### 3. 运行时之争:Bun/Deno vs Node.js
这是2024年的热门话题。面试官可能会问你怎么看。
- Node.js:生态无敌,npm/pnpm包最全,20.x LTS稳定,适合大部分企业级应用。
- Deno:主打安全,默认无文件权限,原生支持TS。
- Bun:主打速度,启动快,自带打包和测试。
面试回答:我会说,Node.js依然是首选,因为生态太成熟了。但对于新项目或者边缘计算场景,Deno和Bun确实很有吸引力。不过考虑到团队学习和维护成本,Node.js 22.x 的新特性(如原生TS尝试)也在努力追赶,目前还是Node.js最稳妥。
- 💡 经验总结:面试前,一定要把事件循环的几个阶段记清楚,特别是
Timers(执行setTimeout回调)和Poll(等待新事件)的区别。这能显得你不仅会用Node.js,还懂它的“心”。