Commander.js:Node.js 命令行工具开发

FreeGuideOnline 最新 2026-06-18

什么是 Commander.js?

Commander.js 是 Node.js 生态中最流行的命令行界面(CLI)开发框架。它以声明式的方式让你快速定义命令、选项(options)、参数(arguments),并自动生成美观的帮助信息。无论是构建简单的脚本工具,还是带有子命令的复杂应用,Commander.js 都能让你的代码结构清晰、易于维护。

核心优势:

  • 零依赖,轻量高效
  • 链式 API,可读性强
  • 自动生成 --help 文档
  • 支持子命令(git 风格)
  • 完善的 TypeScript 类型支持

快速开始

安装 Commander.js

确保你已经安装了 Node.js(建议 v14 以上)。在项目根目录执行:

npm init -y            # 初始化 package.json
npm install commander # 安装 commander

建议在 package.json 中添加 "type": "module" 以便使用 ES 模块语法,本教程将统一采用 ESM 写法。

第一个命令行工具

创建一个文件 index.mjs,输入以下代码:

#!/usr/bin/env node
import { Command } from 'commander';
const program = new Command();

program
  .name('mycli')
  .description('我的第一个 CLI 工具')
  .version('1.0.0');

program.parse(process.argv);

赋予文件执行权限(Linux/macOS):

chmod +x index.mjs

现在运行 ./index.mjs --help,你将看到自动生成的帮助信息。

定义命令与选项

创建独立命令

使用 .command() 定义子命令,.action() 绑定回调函数:

program
  .command('greet <name>')
  .description('向指定用户打招呼')
  .action((name) => {
    console.log(`你好,${name}!`);
  });

<name> 表示必填参数,如果希望参数可选,使用 [name]。在 action 回调中可直接获取传入的参数值。

添加选项 (option)

选项用于控制命令的行为,支持多种类型:

program
  .option('-d, --debug', '启用调试模式')
  .option('-p, --port <number>', '指定端口号', '3000') // 默认值
  .option('-c, --config <path>', '配置文件路径')
  .action((options) => {
    console.log(options.debug);   // 布尔值
    console.log(options.port);    // 字符串(默认 '3000')
    console.log(options.config);  // 用户传递的值或 undefined
  });

Commander 会自动解析选项,并将结果注入到 action 回调函数的最后一个参数中(通常命名为 options)。布尔型选项只需指定标志,带值选项需要使用 < > 标记。

设置必选选项与默认值

让选项强制必填:

.requiredOption('-t, --token <string>', 'API 令牌(必填)')

提供默认值并限制可选值:

.option('-e, --env <environment>', '运行环境', 'development')
.addOption(new Option('-e, --env <environment>').choices(['dev', 'test', 'prod']))

使用 new Option 创建选项对象可以更精细地控制行为。

变长参数与处理函数

通过 ... 语法接收多个参数:

program
  .command('build <source...>')
  .description('构建指定文件')
  .action((sources) => {
    console.log('构建文件列表:', sources); // 数组
  });

你还可以在 action 中获得选项和命令本身:

action((sources, options, command) => { ... })

处理参数与交互

解析命令行参数

调用 program.parse(process.argv) 是启动解析的标准方式。你也可以手动传递参数数组:

program.parse(['node', 'mycli.js', 'build', 'src/index.js'], { from: 'user' });

parse 方法会自动匹配命令、验证必填选项并执行对应的 action。

交互式提示

虽然 Commander.js 本身不提供交互式输入功能,但它可以很好地与 Inquirer.js 配合。你可以在 action 中发起提问:

import inquirer from 'inquirer';
program
  .command('init')
  .action(async () => {
    const answers = await inquirer.prompt([
      { type: 'input', name: 'projectName', message: '项目名称:' }
    ]);
    console.log(`正在创建项目:${answers.projectName}`);
  });

这样便能构建出既有命令行参数,又有优雅交互的工具。

高级特性

子命令

子命令允许你构建像 git commitnpm install 这样的多层命令结构。可以将子命令定义在独立的文件中:

program
  .command('service <action>')
  .description('服务管理')
  .command('start')
  .argument('<serviceName>')
  .action((serviceName) => { /* ... */ })
  .command('stop')
  .argument('<serviceName>')
  .action((serviceName) => { /* ... */ });

为大型项目推荐使用 .command() 的第二个参数传入 Command 实例,实现命令分离。

自动化帮助信息

只要定义了命令和选项,Commander 就会自动处理 --help。你可以自定义帮助页脚:

program.addHelpText('after', `
示例:
  $ mycli greet World
  $ mycli build ./src
`);

beforeafter 参数控制文本添加位置。

版本和自定义帮助

.version('1.0.0', '-v, --version', '输出当前版本号') 如果你的 CLI 需要显示自定义信息,可以覆写 .helpInformation() 方法。

生命周期钩子

在命令执行前后插入逻辑:

program
  .command('deploy')
  .hook('preAction', (thisCommand, actionCommand) => {
    console.log('部署前检查...');
  })
  .hook('postAction', () => {
    console.log('部署完成');
  });

适用于全局配置校验、日志记录等场景。

项目实战:构建一个文件生成器 CLI

需求分析

我们将创建一个名为 maker 的 CLI 工具,它包含两个子命令:

  • maker component <name> :生成一个 Vue 组件模板文件
  • maker page <name> --route :生成页面文件,并可选是否添加路由配置

实现代码

新建 maker.mjs

#!/usr/bin/env node
import { Command } from 'commander';
import fs from 'node:fs';
import path from 'node:path';

const program = new Command();
program.name('maker').description('前端代码生成器').version('0.1.0');

const componentTemplate = (name) =>
`<template>
  <div class="${name.toLowerCase()}-container">
    <h2>${name}</h2>
  </div>
</template>
<script setup>
// ${name} component logic
</script>
<style scoped>
.${name.toLowerCase()}-container {
  padding: 1rem;
}
</style>`;

const pageTemplate = (name, hasRoute) =>
`<template>
  <div>Page: ${name}</div>
</template>
<script setup>
definePageMeta({
  ${hasRoute ? `path: '/${name.toLowerCase()}'` : '// no route'}
})
</script>`;

program
  .command('component <name>')
  .description('创建 Vue 组件')
  .action((name) => {
    const content = componentTemplate(name);
    const dir = path.join(process.cwd(), 'components');
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(path.join(dir, `${name}.vue`), content);
    console.log(`组件 ${name}.vue 已生成在 components/ 目录`);
  });

program
  .command('page <name>')
  .option('-r, --route', '添加路由配置')
  .description('创建页面')
  .action((name, options) => {
    const content = pageTemplate(name, options.route);
    const dir = path.join(process.cwd(), 'pages');
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(path.join(dir, `${name}.vue`), content);
    console.log(`页面 ${name}.vue 已生成${options.route ? '并包含路由' : ''}`);
  });

program.parse(process.argv);

测试与使用

运行:

node maker.mjs component HelloWorld
node maker.mjs page About --route

检查项目目录下的 components/pages/ 文件夹,对应的模板文件已经创建。

总结与最佳实践

  • 命名规范:使用 .name() 显式设置工具名,便于帮助信息展示。
  • 错误处理:在 action 中使用 try...catch 并调用 process.exit(1) 优雅退出。
  • 命令拆分:当项目变大时,将子命令抽离为独立模块,通过 .addCommand() 加载。
  • TypeScript:Commander 提供完整的类型定义,推荐结合 TS 开发以获得更好的智能提示。
  • 发布到 npm:在 package.json 中配置 "bin" 字段,即可通过 npm link 全局使用你的 CLI。

Commander.js 让 Node.js 命令行开发变得前所未有的简单。从一个小脚本到大型工具链,它都能提供严谨且富有表现力的 API,帮助你专注于业务逻辑本身。立即动手,打造你自己的命令行工具箱吧!