Appearance
Node.js 是什么与安装
Node.js 不是一门新语言。它做的事情是把 JavaScript 从浏览器里拿出来,放到操作系统上执行。浏览器里的 JavaScript 操作 DOM、处理用户交互;Node.js 里的 JavaScript 读写文件、监听端口、调用系统命令、操作进程——这些能力来自 libuv 和系统调用绑定,不是 JavaScript 语言本身。
运行时 = 引擎 + 系统能力
| 组成部分 | 做了什么 |
|---|---|
| JavaScript(ECMAScript) | 语言本身:变量、函数、类型、原型链、异步模型 |
| V8 | Google 开发的 JavaScript 引擎,负责把 JS 代码编译成机器码执行 |
| libuv | 跨平台的异步 I/O 库,提供事件循环、线程池、文件系统、网络、子进程等能力 |
| Node.js 内置模块 | fs、http、path、child_process 等,是 C++ 层面对 libuv 和系统调用的封装 |
| npm | Node.js 生态的包管理工具,随 Node.js 一起分发 |
把这个关系理解清楚,后面排查"为什么同一个 JS 代码浏览器里能跑 Node.js 里报错"会容易很多——报错通常是因为浏览器有 DOM API,Node.js 有 fs/process,它们的全局对象不同。反过来 Node.js 代码在浏览器跑不了,也一样。
JavaScript 语言层面由 TC39 委员会推动年度新版本(ES2015 ~ ES2025),V8 引擎跟进实现,Node.js 再按照发行节奏把新版 V8 整合进各版本。一个语法特性从提案到能在 Node.js LTS 里稳定使用,中间隔了 TC39 推动、V8 实现、Node.js 整合三道环节。
运维场景里的适用边界
Node.js 在运维里更适合这些场景:
| 合适 | 具体例子 |
|---|---|
| CLI 工具 | 批量服务检查、配置生成、调用发布 API、跑巡检 |
| 轻量 API 和 webhook | 内部回调接收、运维接口、状态查询 |
| 构建和自动化脚本 | 前端项目构建、文档站点生成、静态资源处理 |
| JSON/YAML/API 数据处理 | 读配置、调接口、生成报告——JS 原生支持 JSON |
| 简单 Web 面板 | 只读状态页、内部管理页 |
不太合适的情况:
| 场景 | 原因 |
|---|---|
| 大量 CPU 密集计算 | Node.js 主线程只有一个,CPU 密集型任务会把事件循环堵住,接口延迟跟着抖动 |
| 需要多线程共享内存的复杂计算 | worker_threads 能做但不顺手,不如 Go/Rust/Java |
| 几行 shell 就能完成的一次性任务 | 为少量逻辑拉 npm 项目、写 package.json 的维护成本偏高 |
| 对启动体积极度敏感的嵌入式场景 | Node.js 运行时本身比可执行文件大很多 |
不是不能做,而是运维里选择一个工具,首先看的是:维护这堆代码的人以后还愿不愿意看。一个 systemctl 管道就能查的事情,变成 10 个 npm 依赖 + commander + axios + chalk,长期维护成本会累积。
LTS 和版本策略
Node.js 版本分为 LTS(Long Term Support)和 Current,每半年发布一个 Current,偶数大版本进入 LTS:
- Current:偶数号进入 LTS,奇数号只在 Current 存活。试新特性用 Current,但过了窗口就不再维护
- LTS:维护 30 个月,持续接收安全补丁和 critical bugfix。生产服务必须跟 LTS
- EOL:不再维护,建议升级
实际运维中固定大版本号很重要。服务器上和开发机上一个 LTS 20,一个 Current 22——过了两个月重新部署时可能发现 npm 包需要的新语法 Current 有而 LTS 没有,或者原生依赖在新版本 V8 上编译不过。
在 package.json 里声明:
json
{
"engines": {
"node": ">=20"
}
}这个字段只是声明意图,不是运行时强制校验。真正部署前还是要跑 node -v。
bash
node -v # 看版本
npm -v # npm 版本随 Node.js 变化
which node # 确认执行的到底是哪个路径下的 node
node -p "process.platform" # 看系统平台
node -p "process.arch" # 看 CPU 架构二进制安装(服务器首选)
服务器上用官方二进制包安装,比用系统源更可控:版本明确、路径清楚、切换和回滚都只需要改软链接。
bash
# 1. 从镜像站拉指定版本的二进制包
curl -L https://mirrors.aliyun.com/nodejs-release/v20.18.0/node-v20.18.0-linux-x64.tar.xz \
-o node-v20.18.0-linux-x64.tar.xz
# 2. 解压到 /usr/local,版本号直接写进目录名
tar -xf node-v20.18.0-linux-x64.tar.xz -C /usr/local/
# 3. 建一个不带版本号的软链接——以后切版本只改这个链接
ln -snf /usr/local/node-v20.18.0-linux-x64 /usr/local/node
# 4. PATH 写进 profile.d
cat > /etc/profile.d/node.sh <<'EOF'
export PATH=/usr/local/node/bin:$PATH
EOF
source /etc/profile.d/node.sh验证:
bash
node -v
npm -v
which node
node -p "process.execPath"process.execPath 能看到 Node.js 二进制文件的完整路径。机器上同时装了系统源的旧 node 和 /usr/local 的新 node,PATH 顺序错了就执行了错的版本——which node 和 process.execPath 一起看能定位。
系统包管理器安装
bash
# Ubuntu / Debian
apt install nodejs npm -y
# RHEL / Rocky / CentOS
yum install nodejs npm -y最省事但版本可能偏旧。适合临时工具和一次性脚本,长期服务不太适合。系统仓库的 Node.js 优先级低于系统厂商需求,通常不是最新 LTS。
开发机版本管理
开发机一台机器上经常同时维护多个项目,不同项目要求不同 Node.js 版本。用版本管理工具可以随时切换:
- fnm(Fast Node Manager):Rust 实现,跨平台,速度快,Windows 支持好
- volta:Rust 实现,项目级固定版本,自动切换
- nvm:最老的方案,bash 脚本,启动时可能影响终端性能
bash
fnm install 20
fnm use 20
fnm list
node -v服务器上不需要版本管理工具。systemd service 里 ExecStart 写绝对路径 /usr/local/node/bin/node,不依赖用户 shell 的 PATH。
容器里使用
dockerfile
FROM node:20-bookworm-slim
WORKDIR /app
# 先拷 package 文件,利用 Docker 层缓存——代码改了依赖没变时不用重装
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]关于镜像选择:
node:20-slim:基于 Debian,体积比完整版小,多数场景合适node:20-alpine:基于 Alpine(musl libc),更小,但用sharp、bcrypt、sqlite3这类原生 C++ 依赖时可能因为 musl/glibc 差异编译不过。出现这类问题时不是 Node.js 的问题,是原生模块和 libc 的兼容问题
一个最小 HTTP 服务——验证安装
装好以后用一个内置 http 模块的服务确认整个链路通——不只是 node 命令能执行,而是网络监听、端口绑定、HTTP 响应都正常:
js
// server.js
import http from "node:http";
const server = http.createServer((req, res) => {
// 运维场景里健康检查端点几乎每个服务都要有
if (req.url === "/healthz") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
res.end("hello from node\n");
});
server.listen(3000, "0.0.0.0", () => {
// 0.0.0.0 绑定所有网卡——容器里 localhost 只绑容器内部,外部访问不到
console.log("listening on port 3000");
});bash
node server.js &
curl http://127.0.0.1:3000/
curl http://127.0.0.1:3000/healthz先确认运行时时和最简单的 HTTP 都正常,再装框架和拆模块。出问题时退回这个最小版本验证,能排除框架、依赖和业务逻辑的干扰。