# 环境搭建
30 秒速记
- 目标就一句:开发机、
CI、生产跑同一个Node主版本、装出同一份依赖 - 版本钉死:项目根目录放
.nvmrc,package.json写engines - 依赖钉死:提交
lockfile,CI用npm ci不用npm install - 原生依赖(
sharp、bcrypt)最容易出环境差异,换平台、换主版本要重装 - 验证别只看
node -v:装一遍依赖、跑一遍测试才算环境搭好
搭 Node 环境的关键不是把 node 装上,而是让每台机器跑的版本和依赖一模一样。 我一般会在项目里放一个 .nvmrc 写上主版本,package.json 里用 engines 声明范围,大家进目录 nvm use 就对齐了。依赖这边,lockfile 必须提交,CI 和部署用 npm ci,它严格按 lockfile 装,和 package.json 对不上就直接报错,不会偷偷升级。很多「我电脑上能跑」的问题,根子都是 Node 主版本不一样或者没锁依赖。
理解「环境搭建」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
环境搭建这件事,真正难的不是装上 node,而是保证「同一份代码在任何地方跑出同样的结果」。可以按三层来对齐。
第一层是 Node 版本。项目根目录放一个 .nvmrc:
echo "22" > .nvmrc
nvm use # 读取 .nvmrc,切到 22.x
node -v # v22.x.x
package.json 里再声明一次:
{
"engines": { "node": ">=22 <23" }
}
配合 .npmrc 里的 engine-strict=true,版本不对时 npm install 会直接失败,而不是只打一行没人看的警告。
第二层是依赖。package-lock.json(或 yarn.lock、pnpm-lock.yaml)一定要提交,它记录了每个包的精确版本和下载地址。开发时用 npm install 加依赖,CI 和部署统一用 npm ci,严格按 lockfile 安装。
第三层是系统环境。node-gyp 编译原生模块需要 Python 和 C++ 编译工具,Windows 上还要装 Visual Studio Build Tools;环境变量也要列成清单,别让某个只在你机器上存在的变量决定程序能不能跑。
最后验证别只敲一句 node -v。一个比较靠谱的检查是:干净目录克隆代码,nvm use、npm ci、npm test 三步全过,才算这台机器的环境真正搭好了。
面试官追问
追问 1本地好好的,CI 上 npm install 之后测试挂了,同一份代码,你先查什么?
先比两边的 node -v 和实际装出来的依赖版本。npm install 会在 semver 范围内装新版,CI 换成 npm ci 严格按 lockfile,大部分这类问题就消失了。
追问 2npm ci 和 npm install 有什么区别?
npm ci 先删掉 node_modules,完全按 package-lock.json 装,lockfile 和 package.json 不一致直接失败,也不会改写 lockfile。npm install 会解析范围、可能更新 lockfile,适合开发时加依赖。
追问 3package.json 写了 "engines": { "node": ">=20" },有人用 Node 18 装依赖为什么没报错?
npm 默认对 engines 只给警告。要强制拦住就在 .npmrc 里加 engine-strict=true,版本不对直接安装失败。
追问 4从 Node 18 升到 22 之后,启动报 was compiled against a different Node.js version,怎么回事?
原生模块是按 Node 的 ABI 编译的,主版本一换就不兼容。删掉 node_modules 重新装,或者 npm rebuild;Docker 镜像里也要确保构建和运行用同一个基础镜像版本。
追问 5Dockerfile 里写 FROM node:latest,有什么问题?
latest 会跟着 Node 发版漂移,今天构建是 24,过几个月可能就是 26,构建结果不可复现。写死主版本,比如 node:22-alpine,升级当成一次显式变更来做。
# 认识 Node.js
30 秒速记
Node.js不是语言也不是框架,是让JS跑在浏览器外的运行时:V8+libuv+ 内置模块- 事件驱动 + 非阻塞
I/O:读文件、发请求交给底层,主线程不傻等 - 擅长
I/O密集(接口层、BFF、SSR、工具链),不擅长CPU密集(大计算会卡住事件循环) - 和浏览器比:没有
DOM/window,多了fs、http、process这些系统能力 - 生态靠
npm,前端工具链(Webpack、Vite、ESLint)几乎全跑在Node上
Node.js 是一个让 JavaScript 跑在服务端的运行时,核心是 Chrome 的 V8 引擎加上 libuv 提供的事件循环和异步 I/O。 它不是新语言,语法还是 JS,只是把浏览器里的 DOM、window 换成了 fs、http、process 这些系统能力。它快的地方在 I/O:读文件、查数据库都交给底层异步去做,主线程接着处理下一个请求,所以单线程也能扛很多并发连接。反过来,图片压缩、大循环这类 CPU 密集的活会把唯一的主线程卡住,这时候要上 worker_threads 或者拆出去。
const fs = require('fs');
fs.readFile(__filename, () => console.log('2. 文件读完了'));
console.log('1. 不等文件,先往下走');
// 输出顺序:1 → 2
- Node 是一个服务器端 JavaScript 解释器
- Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境
- Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效
- Node.js 的包管理器 npm,是全球最大的开源库生态系统
- Node.js 是一门动态语言,运行在服务端的 Javascript
面试官追问
追问 1都说 Node 是单线程,那它怎么同时处理几千个请求?
单线程指的是执行 JS 的主线程只有一个。网络 I/O 交给操作系统的 epoll / kqueue,文件 I/O 交给 libuv 线程池,主线程只负责在结果回来时执行回调,所以不会因为等 I/O 而堵住。
追问 2接口里要对一张 20MB 的图片做压缩,直接写在请求处理函数里会怎样?
压缩期间主线程被占满,其他所有请求都排队,表现是整个服务卡顿、健康检查超时。要么用 sharp 这类底层多线程的库,要么丢给 worker_threads 或者单独的任务队列。
追问 3浏览器里写的代码,拿到 Node 里跑报 window is not defined,为什么?
Node 没有 window 和 DOM,全局对象是 global,通用写法是 globalThis。做 SSR 时这种报错特别常见,访问浏览器 API 的代码要放到只在客户端执行的地方。
追问 4前端为什么要懂 Node?
前端工具链本身就是 Node 程序,Vite 插件、Webpack loader、ESLint 规则都要用 Node 写;SSR 和 BFF 层也跑在 Node 上,出了内存泄漏、事件循环阻塞这类问题得自己能查。
# 版本介绍
30 秒速记
- 看版本:
node -v;看各组件版本:node -p process.versions - 发版规律:偶数主版本会进
LTS长期维护,奇数版本只是Current,生产别用奇数版 ES6支持粗略分代:4.x有了class/ 箭头函数,6.x覆盖绝大部分ES6,7.6+原生async/await- 历史版本
0.x→io.js分裂 → 合并后直接跳到4.0,所以没有1.x ~ 3.x的Node - 查某个语法哪个版本开始支持,去
node.green对照
查版本直接 node -v,选版本记住一条:生产用偶数号的 LTS 版本。 Node 每年发两个主版本,偶数版本会进入大约 30 个月的长期维护,奇数版本半年左右就停止维护,只适合尝鲜。早年各版本对 ES6 的支持差别很大,4.x 开始有 class 和箭头函数,6.x 基本覆盖了 ES6,7.6 之后 async/await 才能原生用。现在这些都不是问题了,更要关心的是你用的版本是否还在维护期,过了 EOL 就没有安全补丁。
node -v # v22.11.0
node -p process.versions.v8 # 查 V8 版本
node -p "process.release.lts" # LTS 版本会打印代号,比如 'Jod'
