运行时与 process
这一课你将能做到
- 熟练使用
process.argv、process.cwd()、process.env 编写接收参数的命令行脚本
- 用正确的退出码结束脚本(
process.exit),让脚本可以被 CI 和其他程序判断成败
- 使用
process.stdout / process.stderr / process.stdin 与管道、重定向配合工作
为什么要学
process 是 Node 给每个进程准备的「全局控制台」:命令行参数、环境变量、标准输入输出、退出码全在这里。日常最有感的三类场景都靠它:
- 写脚本:内容批处理、数据导入,第一件事就是解析命令行参数;
- 接配置:数据库地址、密钥都从
process.env 读(第 8 课展开);
- 接 CI/CD:脚本退出码非 0,流水线立刻标红——「成功静默、失败喊叫」就靠它。
人话:process 就是脚本和操作系统之间唯一的对话窗口,这课学会它,你写的就不只是「能跑的代码」,而是「能被指挥、能报状态」的工具。
先建立图像
你在终端输入:node greet.js --name Java
│
▼
┌────────────────── process ──────────────────┐
│ argv = [node路径, greet.js路径, --name, Java]
│ env = { PATH: ..., HOME: ..., ... } ← 系统环境变量
│ cwd() = h:\work\java-nav ← 终端当前目录
│ platform = win32
│ stdout ──► 终端(可被 | 管道接走)
│ stderr ──► 终端(错误专用通道)
│ exit(code)──► 0 成功 / 非 0 失败
└─────────────────────────────────────────────┘
记住一条主线:argv 管「指挥」,env 管「配置」,stdout/stderr 管「汇报」,exit 码管「成败」。
跟着做
步骤 1:解析命令行参数
新建文件夹 node-process,npm init -y 后新建 greet.js:
// greet.js
// node greet.js Java
const args = process.argv.slice(2); // 去掉前两项,只留用户参数
const name = args[0] ?? 'stranger';
console.log(`你好,${name}!`);
运行:
node greet.js
# 你好,stranger!
node greet.js Java
# 你好,Java!
slice(2) 是最常见的写法:argv 第 0 项是 node 可执行文件路径,第 1 项是脚本路径,真正传给你的从下标 2 开始。
步骤 2:支持 --key=value 风格参数
裸数组在参数一多就难维护。写一个极简解析器(不装任何依赖):
// args.js —— 解析 --key=value 和 --flag 形式
function parseArgs(argv) {
const parsed = {};
for (const arg of argv) {
if (arg.startsWith('--')) {
const eq = arg.indexOf('=');
if (eq === -1) {
parsed[arg.slice(2)] = true; // --verbose 之类开关
} else {
parsed[arg.slice(2, eq)] = arg.slice(eq + 1); // --name=Java
}
}
}
return parsed;
}
module.exports = { parseArgs };
// greet.js(升级版)
const { parseArgs } = require('./args');
const args = parseArgs(process.argv.slice(2));
const name = args.name ?? 'stranger';
if (args.verbose) {
console.error(`[verbose] 收到参数:`, args);
}
console.log(`你好,${name}!`);
node greet.js --name=Java --verbose
# [verbose] 收到参数: { name: 'Java', verbose: true }
# 你好,Java!
参数一多,这种 --key=value 风格比按下标取值健壮得多。需求更复杂时再换官方推荐的 util.parseArgs()。
步骤 3:cwd 与脚本目录——新手第一大坑
// tools/where.js
const path = require('node:path');
console.log('process.cwd() =', process.cwd());
console.log('__dirname =', __dirname);
console.log('两者相同吗 =', process.cwd() === __dirname);
node tools/where.js
# process.cwd() = h:\work\node-process
# __dirname = h:\work\node-process\tools
# 两者相同吗 = false
process.cwd():终端当前所在目录,相对路径都相对它;
__dirname:脚本文件所在目录,永远稳定。
所以「读脚本旁边的配置文件」必须用 __dirname 拼路径,用相对路径等于把脚本命运交给了调用者的终端位置。ESM 里没有 __dirname,用 path.dirname(fileURLToPath(import.meta.url)) 等价替代(第 3 课讲 ESM)。
步骤 4:退出码——成功静默,失败喊叫
// check.js
const fs = require('node:fs');
const file = process.argv[2];
if (!file) {
console.error('用法: node check.js <文件路径>');
process.exit(1); // 参数缺失:退出码 1
}
if (!fs.existsSync(file)) {
console.error(`文件不存在: ${file}`);
process.exit(2); // 找不到文件:退出码 2
}
console.log(`校验通过: ${file}`);
// 不调用 process.exit 时,事件循环空了自然退出,退出码 0
node check.js note.txt
# 校验通过: note.txt
echo $LASTEXITCODE # PowerShell 查看上一条命令退出码,输出 0
node check.js
# 用法: node check.js <文件路径>
echo $LASTEXITCODE # 输出 1
约定俗成:0 成功,1 一般失败,其他数字自定义错误类别。CI 流水线、&& 串联命令、进程管理器都靠退出码判断脚本死活。
步骤 5:stdout / stderr 分流
console.log 其实写到 process.stdout,console.error 写到 process.stderr。两条通道分开后,错误信息才不会被「正常输出」淹没:
node check.js note.txt 2> err.log
# 正常输出仍在屏幕,错误进了 err.log
node greet.js --name=Java | node args-demo.js # stdout 可以接进下一个程序
先看一个 stdin 的小交互,新建 ask.js:
// ask.js
process.stdout.write('你的名字?');
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
const name = chunk.trim() || 'stranger';
console.log(`你好,${name}!`);
process.stdin.pause(); // 停止监听,让事件循环空下来从而退出
});
node ask.js
# 你的名字?Java
# 你好,Java!
日常 90% 场景用 console.log/error 就够了,但知道底层是两条流,排障时(比如「日志怎么进文件了」)就有抓手。
步骤 6:几个常用的 process 信息
// info.js
console.log('Node 版本 :', process.version);
console.log('平台 :', process.platform); // win32 / linux / darwin
console.log('CPU 架构 :', process.arch); // x64 / arm64
console.log('运行时长(s) :', Math.round(process.uptime()));
console.log('内存占用 :', Math.round(process.memoryUsage().heapUsed / 1024 / 1024), 'MB');
node info.js
# Node 版本 : v20.11.0
# 平台 : win32
# CPU 架构 : x64
# 运行时长(s) : 0
# 内存占用 : 4 MB
写跨平台脚本时用 process.platform 分支(比如 Windows 换行符是 \r\n),比硬编码可靠。
讲清楚要点
- argv 前 2 项不是你的参数:
[node路径, 脚本路径, ...用户参数],处理前先 slice(2)。
- 相对路径参照 cwd,不参照脚本:
fs.readFileSync('a.txt') 读的是终端当前目录下的 a.txt。要锚定脚本位置用 __dirname(CJS)或 import.meta.url(ESM)。
- 退出码是脚本的「状态汇报协议」:0 成功、非 0 失败。被 CI/流水线调用的脚本必须认真设计退出码,否则「静默失败」比崩溃更危险。
- stdout 走数据、stderr 走错误:
console.log → stdout,console.error → stderr。分流后才能 2> 单独收集错误日志。
- process.env 里的值全是字符串:
PORT=0 读出来是 "0",做数字比较前要 Number() 转换——这是经典 bug 源。
你可能会懵的地方
-
process.exit(1) 和脚本自然结束有什么区别?
自然结束是事件循环里的任务跑完了,退出码 0。process.exit() 立刻终止进程——未写完的异步 I/O 会被打断(比如刚发起的文件写入可能没落盘)。只在明确要报错时用它;正常收尾用 process.exitCode = 1 更安全,它等事件循环自然排空后再退出。
-
环境变量和命令行参数都用,怎么选?
参数表达「这次运行做什么」(输入文件、目标环境),环境变量表达「这个环境里有什么」(密钥、地址)。频繁变的走参数,按部署环境固定的走 env。
-
--verbose 打出来的日志为什么用 console.error?
调试信息本质是「旁路信息」,进 stderr 才不会污染 stdout 的数据流,管道下游拿到的才是纯数据。
-
Windows 和 Linux 的 PATH 分隔符一样吗?
不一样:Windows 是 ;,Linux/macOS 是 :。需要解析时用 path.delimiter,别硬编码。
-
怎么看上一条命令的退出码?
PowerShell 用 echo $LASTEXITCODE,cmd 用 echo %errorlevel%,bash 用 echo $?。
错法对照
| 错法 | 问题 | 正解 |
|---|
process.argv[2] 直接当文件名,不判空 | 没传参时 undefined 引发下游诡异报错 | 先判空并输出用法、process.exit(1) |
| 用相对路径读脚本同目录文件 | 换个目录运行就 ENOENT | path.join(__dirname, 'a.txt') |
失败时只 console.log 不设置退出码 | CI 认为 0 成功,坏数据悄悄上线 | console.error + process.exit(1) 或 process.exitCode = 1 |
process.exit() 后还指望异步任务完成 | 进程已死,未落盘 | 用 process.exitCode,或把退出放在流 finish 回调后 |
Number(process.env.PORT) === 0 写成 process.env.PORT === 0 | env 是字符串 "0",永不相等 | 显式 Number() 转换 |
卡点急救
- 脚本在 A 目录正常、在 B 目录报 ENOENT:相对路径踩了 cwd 坑,改用
__dirname 拼绝对路径。
process.exit() 后日志没打出来:stdout 是异步的,exit 截断了输出。排查场景改用 process.exitCode,或对 stdout 监听 drain/用同步写。
- PowerShell 里
node a.js && node b.js 报错:旧版 PowerShell 不支持 &&,用 node a.js; if ($LASTEXITCODE -eq 0) { node b.js },或换 PowerShell 7+。
- stdin 交互脚本不退出:stdin 监听器让事件循环一直有活干,处理完记得
process.stdin.pause() 或 process.exit()。
面试常问
-
process.argv 是什么结构?
数组:[node 可执行文件路径, 当前脚本路径, ...命令行参数]。实际开发用 slice(2) 取业务参数,复杂场景用 util.parseArgs 或 commander 这类库。
-
process.cwd() 和 __dirname 的区别?
cwd 是进程启动时终端所在目录,随调用位置变化;__dirname 是脚本文件自身所在目录,恒定。读「脚本旁边」的文件必须用 __dirname。
-
process.exit() 的坑?
立即终止进程,stdout/stderr 的异步写入和进行中的 I/O 可能被截断。推荐非致命场景用 process.exitCode 设置退出码让进程自然结束,致命错误才 exit()。
-
Node 进程退出码 0 和非 0 意味着什么?
0 表示成功;非 0 表示失败,具体数值可自定义错误类别。CI、进程管理器、shell 的 && 都依据退出码决定后续行为。
自测三问
node greet.js --name=Java 时 process.argv 数组有几个元素?分别是什么?
- 在
h:\work 目录执行 node tools/where.js,此时 process.cwd() 和 __dirname 各是什么?
- 为什么错误信息要打给 stderr 而不是 stdout?
小结一句话
process 是脚本与操作系统之间的控制台:argv 接指挥、env 读配置、stdout/stderr 分流汇报、退出码宣告成败——掌握它,脚本才算「可被指挥、可报状态」。
延伸