在当今的开发工作流中,命令行工具(CLI)扮演着至关重要的角色。无论是前端构建工具、后端脚手架,还是 DevOps 自动化脚本,一个设计良好的 CLI 工具能极大提升开发效率。本文将带你从零开始,构建一个功能完整、用户体验优秀的现代化 Node.js 命令行工具。
为什么需要现代化的 CLI 工具?
传统的命令行工具往往存在以下问题:
- 参数解析混乱,缺乏一致性
- 错误处理不完善,用户反馈不友好
- 缺乏自动补全和文档生成
- 测试覆盖率低,维护困难
现代化的 CLI 工具应该具备:
- 直观的命令结构
- 丰富的交互体验
- 完善的错误处理
- 完整的测试覆盖
- 良好的文档支持
项目初始化与架构设计
首先创建我们的项目,我们将构建一个名为 modern-cli 的工具:
mkdir modern-cli && cd modern-cli
npm init -y
安装核心依赖:
npm install commander inquirer chalk ora figlet boxen
npm install -D typescript @types/node ts-node jest @types/jest
创建基础目录结构:
modern-cli/
├── src/
│ ├── commands/ # 命令实现
│ ├── utils/ # 工具函数
│ ├── types/ # TypeScript 类型定义
│ └── index.ts # 入口文件
├── tests/ # 测试文件
├── bin/ # 可执行文件
└── package.json
核心实现:命令解析与执行
1. 使用 Commander.js 构建命令框架
Commander.js 是目前最流行的 Node.js 命令行框架,提供了完整的参数解析、帮助文档生成等功能。
// src/index.ts
import { Command } from 'commander';
import { version } from '../package.json';
import { initCommand } from './commands/init';
import { generateCommand } from './commands/generate';
const program = new Command();
program
.name('modern-cli')
.description('一个现代化的 Node.js 命令行工具')
.version(version)
.option('-d, --debug', '启用调试模式')
.hook('preAction', (thisCommand, actionCommand) => {
if (thisCommand.opts().debug) {
process.env.DEBUG = 'true';
console.log('调试模式已启用');
}
});
// 注册子命令
program.addCommand(initCommand);
program.addCommand(generateCommand);
// 全局错误处理
program.configureOutput({
writeErr: (str) => process.stderr.write(chalk.red(str)),
outputError: (str, write) => write(chalk.red.bold(str))
});
program.parse();
2. 实现交互式命令
使用 Inquirer.js 创建丰富的交互体验:
// src/commands/init.ts
import { Command } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
import fs from 'fs-extra';
import path from 'path';
export const initCommand = new Command('init')
.description('初始化项目配置')
.option('-y, --yes', '使用默认配置')
.action(async (options) => {
const spinner = ora('正在初始化项目...').start();
try {
let config = {};
if (options.yes) {
// 使用默认配置
config = getDefaultConfig();
} else {
// 交互式配置
const answers = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: '选择项目模板:',
choices: [
{ name: 'React + TypeScript', value: 'react-ts' },
{ name: 'Vue 3 + TypeScript', value: 'vue3-ts' },
{ name: 'Node.js API', value: 'node-api' }
]
},
{
type: 'checkbox',
name: 'features',
message: '选择需要集成的功能:',
choices: [
{ name: 'ESLint', value: 'eslint' },
{ name: 'Prettier', value: 'prettier' },
{ name: 'Husky', value: 'husky' },
{ name: '单元测试', value: 'tests' }
]
},
{
type: 'confirm',
name: 'useGit',
message: '是否初始化 Git 仓库?',
default: true
}
]);
config = answers;
}
// 生成配置文件
await generateConfigFiles(config);
spinner.succeed(chalk.green('项目初始化完成!'));
// 显示后续步骤
console.log('\n' + chalk.bold('下一步:'));
console.log(chalk.cyan('1. cd your-project'));
console.log(chalk.cyan('2. npm install'));
console.log(chalk.cyan('3. npm run dev'));
} catch (error) {
spinner.fail(chalk.red('初始化失败'));
console.error(chalk.red(error.message));
process.exit(1);
}
});
async function generateConfigFiles(config: any) {
const configDir = path.join(process.cwd(), '.modern-cli');
await fs.ensureDir(configDir);
await fs.writeJson(
path.join(configDir, 'config.json'),
config,
{ spaces: 2 }
);
// 根据配置生成其他文件
if (config.template === 'react-ts') {
await generateReactTemplate(config);
}
// ... 其他模板生成逻辑
}
3. 实现进度显示与动画
使用 Ora 和 Chalk 增强用户体验:
// src/utils/progress.ts
import ora, { Ora } from 'ora';
import chalk from 'chalk';
export class ProgressManager {
private spinner: Ora;
private steps: string[] = [];
private currentStep = 0;
constructor(message: string) {
this.spinner = ora(message);
}
start() {
this.spinner.start();
return this;
}
step(message: string) {
this.currentStep++;
const progress = `[${this.currentStep}/${this.steps.length}]`;
this.spinner.text = `${chalk.cyan(progress)} ${message}`;
return this;
}
succeed(message?: string) {
this.spinner.succeed(message);
return this;
}
fail(message?: string) {
this.spinner.fail(message);
return this;
}
setSteps(steps: string[]) {
this.steps = steps;
return this;
}
}
// 使用示例
export async function runWithProgress<T>(
task: () => Promise<T>,
message: string
): Promise<T> {
const progress = new ProgressManager(message).start();
try {
const result = await task();
progress.succeed('任务完成');
return result;
} catch (error) {
progress.fail('任务失败');
throw error;
}
}
高级特性实现
1. 插件系统设计
// src/core/plugin-manager.ts
import fs from 'fs-extra';
import path from 'path';
export interface Plugin {
name: string;
version: string;
register: (program: Command) => void;
}
export class PluginManager {
private plugins: Map<string, Plugin> = new Map();
private pluginDir: string;
constructor(pluginDir: string) {
this.pluginDir = pluginDir;
}
async loadPlugins() {
if (!await fs.pathExists(this.pluginDir)) {
return;
}
const pluginFiles = await fs.readdir(this.pluginDir);
for (const file of pluginFiles) {
if (file.endsWith('.js') || file.endsWith('.ts')) {
try {
const pluginPath = path.join(this.pluginDir, file);
const pluginModule = await import(pluginPath);
if (pluginModule.default && this.validatePlugin(pluginModule.default)) {
this.registerPlugin(pluginModule.default);
}
} catch (error) {
console.warn(`加载插件 ${file} 失败:`, error.message);
}
}
}
}
private validatePlugin(plugin: any): plugin is Plugin {
return (
typeof plugin.name === 'string' &&
typeof plugin.version === 'string' &&
typeof plugin.register === 'function'
);
}
registerPlugin(plugin: Plugin) {
this.plugins.set(plugin.name, plugin);
}
getPlugin(name: string): Plugin | undefined {
return this.plugins.get(name);
}
getAllPlugins(): Plugin[] {
return Array.from(this.plugins.values());
}
}
2. 配置管理系统
// src/core/config-manager.ts
import fs from