Node.js CLI
Node.js CLI 是一种用 Node.js 编写的命令行程序,它可以被安装成系统命令,像 mkdir、git 一样在终端直接使用。它的作用是把 JavaScript/TypeScript 的程序能力封装成命令,用于自动化任务、项目脚手架、开发工具、运维脚本和批处理操作,具有跨平台、易发布、生态丰富、适合前端和全栈开发者的特点。
CLI 的 Hello World 项目
检查是否安装了 Node 环境,如果没有安装,请查看:https://blog.csdn.net/a1053765496/article/details/145473576
Windows PowerShell 中输入如下命令:
bash
node -v
npm -v

新建一个项目文件夹 test

初始化项目
进入到 test 目录中,打开 Windows PowerShell
输入如下命令:
bash
npm init -y
你会得到一个 package.json

新建一个 bin 目录

bin 目录下新建一个 js 文件, hello.js (如果是 Linux / MacOS 环境,需要给 hello.js 文件权限 chmod +x bin/hello.js )

hello.js 中的代码如下:
javascript
#!/usr/bin/env node // 第一行必须是这个代码, 意思是用 node 来执行这个文件
console.log('Hello CLI'); // 输出 Hello CLI
修改 package.json 文件,添加如下内容
javascript
"bin": {
"hello": "./bin/hello.js"
}
完整 package.json 文件内容如下
javascript
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"bin": {
"hello": "./bin/hello.js"
}
}
在本地注册这个命令,执行如下命令:
bash
# 把 hello 命令链接到系统 PATH 环境
npm link

执行自定义的 hello 命令,输出 Hello CLI

常见参数形式
| 写法 | 示例 | 在 argv 里的样子 |
|---|---|---|
| 位置参数 (命令后面要带一个参数叫位置参数) | hello zhangsan |
['zhangsan'] |
| 长参数 | --name zhangsan |
['--name', 'zhangsan'] |
| 短参数 | -n zhangsan |
['-n', 'zhangsan'] |
| 布尔参数 | --force |
['--force'] |
| 等号形式 | --port=3000 |
['--port=3000'] |
让命令接收参数
process.argv 介绍
process.argv 是 Node.js 提供的"命令行参数数组"
当你在终端输入命令时:
bash
node app.js a b c
Node 会把整条命令拆成一个数组,放进 process.argv
无论你传不传参数,它永远至少有 2 个元素:
bash
process.argv = [
'node 的绝对路径',
'当前执行脚本的绝对路径',
...你传的参数
]
打印 console.log(process.argv); 看效果
bash
PS D:\project\test> hello
[
'C:\\Users\\aoc\\AppData\\Local\\fnm_multishells\\31380_1767578153606\\node.exe',
'C:\\Users\\aoc\\AppData\\Local\\fnm_multishells\\31380_1767578153606\\node_modules\\test\\bin\\hello.js'
]

process.argv.slice(2) 说明
slice(2) 因为前两个参数不是你关心的业务参数。
了解了一下 process.argv 后,我们来让 命令接收参数
让命令接收参数
修改 bin/hello.js
bash
#!/usr/bin/env node
const args = process.argv.slice(2);
const param = args[0] || 'World';
console.log(`Hello ${param}`);

接收 长参数 示例
怎么接收如下格式的参数
bash
hello --age 18 --city bj
代码如下:
javascript
#!/usr/bin/env node
const args = process.argv.slice(2);
const result = {};
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith("--")) {
const key = args[i].slice(2);
const value = args[i + 1];
result[key] = value;
i++;
}
}
console.log(result);

接收 等号参数 示例
怎么接收如下格式的参数
javascript
hello port=3000
代码如下:
javascript
#!/usr/bin/env node
const args = process.argv.slice(2);
args.forEach(arg => {
if (arg.includes('=')) {
const [key, value] = arg.split('=');
console.log(key, value);
}
});

process.argv 的问题
process.argv
- 不解析 -abc
- 不解析 --no-cache
- 不支持 help
- 不支持校验
- 不支持子命令
所以真实项目中都是 process.argv + 参数解析库 的方式
主流的 参数解析库有 commander、yargs、minimist,当然底层都是 process.argv
Node.js commander
commander 是 Node.js 中最常用的命令行框架,用于快速构建专业的 CLI 工具,它可以基于 process.argv 自动解析命令和参数,支持子命令、选项(如 --port)、必填校验、默认值以及自动生成 --help 和 --version,让开发者用简洁、可维护的方式把 Node.js 程序封装成像 git、npm 一样的命令行工具。
安装 commander
javascript
npm i commander
配置成 ESM 语法(TS/Vite 常见)
修改 package.json 的 type 为 module
javascript
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"bin": {
"hello": "./bin/hello.js"
},
"dependencies": {
"commander": "^14.0.2"
}
}
然后 js/ts 文件中,就可以使用 import { Command } from 'commander'; 这样的语法,而不是 const { Command } = require('commander'); 语法。
const program = new Command() 方法 详解
program 是 CLI 的根命令对象
负责:
定义命令名、版本、描述
注册子命令
解析参数
生成
--help分发执行到对应
action一句话:program 是整个 CLI 的"指挥中心"。
方法详解
.name(name): 设置命令名(help 中显示)
示例:
javascript
program.name('hello 程序');
.description(desc):命令描述
示例:
javascript
program.description('hello 工具集');
.version(version, flags?, description?):自动提供版本号功能,等于内置了一个 --version 命令
示例:
javascript
program.version('1.0.0');
.helpOption(flags, description):自定义 help 选项
示例:
javascript
program.helpOption('-h, --help', '查看帮助');
.addHelpText(position, text):给 help 增加说明(非常常用)
javascript
program.addHelpText('after', `
Examples:
hello init
hello build -m dev
`);
位置参数
.argument():<> 必填,[] 可选
javascript
program.argument('<name>', '名字');
.arguments()
javascript
program.arguments('<src> <dest>');
选项参数(flags)
.option(flags, description, defaultValue?)
javascript
program.option('-p, --port <number>', '端口', '3000');
// 这样获取 program.opts().port // '3000'
.requiredOption():未提供会报错 + help
javascript
program.requiredOption('-t, --token <string>', '必须提供');
选项值转换(parser)
javascript
program.option(
'-p, --port <number>',
'端口',
v => Number(v),
3000
);
子命令(像 git 一样)
.command(name):创建子命令
示例:
javascript
program
.command('init')
.description('初始化项目')
.action(() => {
console.log('init');
});
子命令 + 参数 + 选项
示例:
javascript
program
.command('build')
.argument('[env]', '环境', 'prod')
.option('-m, --mode <mode>', '模式')
.action((env, opts) => {
console.log(env, opts.mode);
});
执行相关方法(决定什么时候跑)
.action(fn):真正执行的地方
语法: // 子命令和根命令都可以有 action
javascript
program.action((...args) => {
// args = [arguments..., options, command]
});
.parse(argv?):触发解析(必须)不调用 CLI 什么都不干,通常放在文件最后
javascript
program.parse(process.argv);
.parseAsync():异步 action
javascript
// 如果 action 里有 async/await
await program.parseAsync();
解析结果读取
.opts():获取选项对象
javascript
const opts = program.opts();
.args:剩余的参数数组(不常用)
javascript
program.args;
.processedArgs:解析后的参数(调试用)
错误与退出控制
.exitOverride():阻止 commander 自动 process.exit()
javascript
program.exitOverride();
try {
program.parse();
} catch (err) {
console.error(err.message);
process.exit(1);
}
.allowUnknownOption():允许未知参数(一般不推荐)
javascript
program.allowUnknownOption();
行为控制
.showHelpAfterError():错误后显示 help
javascript
program.showHelpAfterError();
.showSuggestionAfterError():拼写错误时给建议
javascript
program.showSuggestionAfterError();
完整语法流程:
javascript
program
.name('zkar')
.description('工具集')
.version('1.0.0')
.option('--debug', '调试')
.command('init')
.description('初始化')
.action(() => {})
program.parse();
执行流程如下:
输入命令
→ parse()
→ 匹配子命令
→ 解析参数/选项
→ 校验
→ 调用 action
commander 示例
示例一:help + version
javascript
#!/usr/bin/env node
import { Command } from 'commander';
const program = new Command();
program
.name('hello')
.description('我的第一个 Node CLI')
.version('1.0.0');
program.parse(process.argv);

如果想把 output the version number 和 display help for command 改成自己指定的内容,可以如下这样写:
javascript
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program
.name("hello")
.description("我的第一个 Node CLI")
.version("1.0.0", "-v, --version", "查看 hello 版本号")
.helpOption("-h, --help", "查看使用说明");
program.parse(process.argv);

示例二:位置参数,如果有多个位置参数就写多个 argument ,不推荐 arguments
javascript
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program.argument("<name>", "必填名字").action((name) => {
console.log("Hello", name);
});
program.parse(process.argv);

示例:选项参数
javascript
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program
.option("-p, --port <number>", "端口号", "3000")
.option("--host <string>", "主机", "127.0.0.1")
.requiredOption("--dry-run", "只演练不执行") // 必填 requiredOption
.action((opts) => {
console.log(opts);
});
program.parse(process.argv);

示例:类型转换与校验:.argParser() / 自定义 parser
javascript
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
const toInt = (v) => {
const n = Number(v);
if (!Number.isInteger(n)) throw new Error("port 必须是整数");
return n;
};
program.option("-p, --port <number>", "端口", toInt, 3000);
program.parse(process.argv);

示例:子命令
javascript
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program
.command("init")
.description("初始化项目")
.option("--force", "强制覆盖")
.action((opts) => {
console.log("init", opts);
});
program
.command("build")
.description("构建项目")
.option("-m, --mode <mode>", "模式", "prod")
.action((opts) => {
console.log("build", opts.mode);
});
program.parse(process.argv);
javascript
hello init --force
hello build -m dev
示例:帮助信息定制
javascript
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program.helpOption("-h, --help", "查看帮助").addHelpText(
"after",
`
Examples:
hello init --force
hello build -m dev
`
);
program.parse(process.argv);

示例:获取解析结果:.opts() / .args / .processedArgs
javascript
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program
.name("hello")
.description("parse / opts / args 示例")
.option("-f, --force", "强制执行")
.option("-m, --mode <mode>", "运行模式")
.argument("<src>", "源路径")
.argument("[dest]", "目标路径(可选)");
program.parse(process.argv);
// 所有 option(--xxx)
const opts = program.opts();
// 所有位置参数
const args = program.args;
console.log("opts =", opts);
console.log("args =", args);

示例:错误处理与退出码(做成"像系统命令一样")
javascript
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program
.name("hello")
.description("exitOverride 示例")
.exitOverride() // 关键:阻止自动 exit
.argument("<src>", "源路径")
.option("-f, --force", "强制执行");
try {
program.parse(process.argv);
// 只有 parse 成功,才会走到这里
const opts = program.opts();
const args = program.args;
console.log("解析成功");
console.log("opts:", opts);
console.log("args:", args);
} catch (err) {
// 所有 Commander 的退出行为都会到这里
console.error("参数解析失败");
process.exit(1);
}
缺少必填参数

非法参数

参数正确

commander 项目最佳实践
项目目录结构
javascript
mycli/
bin/
hello.js # 入口:注册命令
commands/
init.js
build.js
utils
logger.js
fs.js
hello.js
javascript
#!/usr/bin/env node
import { Command, CommanderError } from "commander";
import initCommand from "../commands/init.js";
const program = new Command();
program
.name("hello")
.description("My CLI Tool")
.version("1.0.0", "-v, --version", "查看版本")
.exitOverride()
.configureOutput({
writeErr: () => {}, // 禁止 commander 自动打印错误
});
// 注册子命令
initCommand(program);
try {
program.parse(process.argv);
} catch (err) {
if (err instanceof CommanderError) {
console.error("参数错误");
console.error(err.message);
process.exit(err.exitCode);
}
// 其他非 CLI 错误
console.error("程序异常");
console.error(err);
process.exit(1);
}
init.js
javascript
export default function initCommand(program) {
program
.command("init <projectName>")
.description("初始化项目")
.option("-f, --force", "强制覆盖")
.action((projectName, options) => {
console.log("init 命令执行");
console.log("projectName:", projectName);
console.log("options:", options);
});
}
搭建 CLI 真实生产项目
在你想放项目的目录下执行
bash
# npm create vite@latest <项目名称>
npm create vite@latest crawler-cmd
创建好的项目目录结构如下:

改造项目,删除 index.html文件、tsconfig.app.json、src下的所有文件、public文件夹
删除后项目目录结构如下:

改造 vite.config.ts 文件
TypeScript
// 给 Vite 配置提供 类型提示
import { defineConfig } from "vite";
// 把相对路径转换成 绝对路径
import { resolve } from "node:path";
// 导出 Vite 的配置对象
export default defineConfig({
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
build: {
// 告诉 Vite,这是一个"库 / 工具"的构建,而不是前端应用。如果不用,vite 会默认找 index.html 构建成浏览器产物
lib: {
// 指定 CLI 的入口文件,使用 resolve 保证路径稳定,不依赖执行命令的目录
entry: resolve(__dirname, "src/bin/dog.ts"),
// 输出 ES Module 格式, package.json 写了 "type": "module",用 ESM 解析 不接受 CJS
formats: ["es"],
// 指定输出文件名
fileName: () => "dog.js",
},
// 构建产物输出目录
outDir: "dist",
// 每次 build 前清空 dist
emptyOutDir: true,
// target: "node18",
// 这些模块不要打进最终文件
rollupOptions: {
external: [
"commander",
"node:fs",
"node:path",
"node:os",
"node:child_process",
],
},
},
});
改造 package.json,只留下 node、typescript、vite 相关的依赖,没有commander就安装。如果报如下错误:《加载引用"https://www.schemastore.org/package"时出现问题: 无法从"https://www.schemastore.org/package"加载架构: read ECONNRESET。》在 settings.json 中添加 "json.schemaDownload.enable": false 配置
TypeScript
{
"name": "crawler-cmd",
"private": true,
"version": "1.0.0",
"type": "module",
"bin": {
"dog": "dist/dog.js"
},
"scripts": {
"dev": "vite build && node dist/dog.js",
"build": "vite build",
"build:link": "vite build && npm link"
},
"devDependencies": {
"@types/node": "^24.10.1",
"typescript": "~5.9.3",
"vite": "^7.2.4"
},
"dependencies": {
"commander": "^14.0.2"
}
}
然后就可以在,src 下面写 bin、commands 等内容了
如果想要配置 @ 别名,配置如下:
tsconfig.node.json 中新增
bash
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}
vite.config.ts 中新增
TypeScript
import { defineConfig } from "vite";
import { resolve } from "node:path";
export default defineConfig({
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
build: {
。。。省略
},
});
部署到 Linux 服务器
当我们把 CLI 代码写好后,就要发布到线上环境了。发布步骤如下
- 需要把 package.json 文件一起打包到 dist 目录中(因为Node CLI是运行时解析(运行时再去找依赖),运行时需要node_modules中的依赖,和web不同web运行时在"浏览器",依赖必须在构建期全部打包),所以需要在编写一个打包脚本,代码如下:
src/scripts/build-dist.js
javascript
import fs from "fs";
const rootPkg = JSON.parse(fs.readFileSync("package.json", "utf-8"));
const distPkg = {
name: rootPkg.name,
version: rootPkg.version,
private: true,
type: "commonjs",
bin: {
dog: "./dog.js",
},
dependencies: {
commander: rootPkg.dependencies.commander,
},
};
fs.writeFileSync("dist/package.json", JSON.stringify(distPkg, null, 2));
if (fs.existsSync("package-lock.json")) {
fs.copyFileSync("package-lock.json", "dist/package-lock.json");
}
修改 package.json 的 scripts
javascript
"scripts": {
"build": "vite build && node src/scripts/build-dist.js",
},
- 把 打包后的 dist 文件夹中的文件上传到 linux 的 /usr/local/lib/cli 目录


执行安装命令
javascript
npm install
- 赋予可执行权限
javascript
chmod +x /usr/local/lib/cli/dog.js
- 创建命令入口(wrapper)
不要直接把 dog.js 放 PATH 里
新建一个入口文件
bash
vim /usr/local/bin/dog
bash
#!/bin/bash
node /usr/local/lib/cli/dog.js "$@"
赋予执行权限
bash
chmod +x /usr/local/bin/dog
- 验证 CLI 是否注册成功
bash
which dog
// 输出 /usr/local/bin/dog
- 使用命令
bash
dog --help
