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

以前的老教程里全是 requiremodule.exports,那是 CommonJS 规范。现在都2024年了,咱们得跟上时代,ES Modules (ESM) 才是主流。Node.js 20 对 ESM 的支持已经非常完善了,咱们直接用 import/export 语法,写起来跟前端 React/Vue 一模一样。

不过这里有个坑,Node 默认还是把文件当 CommonJS 处理。怎么开启 ESM 模式?有两个办法:

我推荐第一种,毕竟谁也不想满屏都是 .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 + 事件驱动

啥叫非阻塞?举个例子,你要去读一个很大的文件或者查数据库。

这个“捞起来”的过程就是 事件循环 (Event Loop)。它跟浏览器的事件循环有点像,但更复杂一些。Node.js 把任务分成了几个阶段(Timers, Pending Callbacks, Idle/Prepare, Poll, Check, Close Callbacks)。

实际案例预警:面试里经常问 setTimeoutsetImmediate 谁先执行?大部分情况下 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 或 定时器回调');

运行这段代码,你会发现输出顺序很有意思:

这就证明了 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.jsonscripts 里加个启动命令:

"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

5. 排查与面试:内存泄漏检测与高频面试题解析

做后端开发,最怕的不是代码写不出来,而是代码跑着跑着,服务器内存爆了。Node.js虽然是单线程,但如果不注意,内存泄漏能把你搞得焦头烂额。这一章咱们就聊聊怎么抓“内存小偷”,顺便把面试里那些绕晕人的问题给盘明白。

内存泄漏排查:别让内存偷偷溜走

简单来说,内存泄漏就是你的程序申请了一块内存,用完了却没还回去,或者因为某些引用没断掉,垃圾回收机制(GC)没法回收它。在Node.js里,最常见的坑就是全局变量滥用闭包引用以及未清理的监听器

#### 如何检测?

来个典型的“坑爹”代码示例,看看怎么通过代码监控内存:

// 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)往上找,基本就能定位到是哪个变量没释放。

面试考点梳理解析

面试的时候,面试官特别爱问Node.js的底层机制。咱们结合AI知识库里的热点,扒一扒这几个必考题。

#### 1. 事件循环(Event Loop)和浏览器有啥区别?

这是必考题!简单来说,虽然都叫事件循环,但Node.js的更“偏执”一点。

面试回答技巧:你可以说,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依然是首选,因为生态太成熟了。但对于新项目或者边缘计算场景,Deno和Bun确实很有吸引力。不过考虑到团队学习和维护成本,Node.js 22.x 的新特性(如原生TS尝试)也在努力追赶,目前还是Node.js最稳妥。