在当今的开发世界中,命令行界面(CLI)工具仍然是开发者日常工作中不可或缺的一部分。无论是脚手架工具、构建工具还是开发辅助工具,一个设计良好的CLI可以显著提升开发效率。本文将带你从零开始,使用现代Node.js技术栈构建一个功能完整、用户体验优秀的CLI工具。
为什么需要现代化的CLI工具?
传统的CLI工具往往存在以下问题:
- 安装复杂,依赖管理混乱
- 用户体验差,错误提示不友好
- 缺乏交互性,配置过程繁琐
- 跨平台兼容性问题
现代CLI工具应该具备:
- 一键安装,零配置启动
- 直观的交互界面
- 智能的自动补全
- 清晰的文档和错误提示
- 良好的性能表现
项目初始化与架构设计
1. 创建项目结构
mkdir modern-cli-tool && cd modern-cli-tool
npm init -y
创建基础目录结构:
modern-cli-tool/
├── src/
│ ├── commands/ # 命令模块
│ ├── utils/ # 工具函数
│ ├── templates/ # 模板文件
│ └── index.js # 入口文件
├── bin/ # CLI入口
├── tests/ # 测试文件
└── package.json
2. 配置package.json
{
"name": "modern-cli",
"version": "1.0.0",
"description": "A modern CLI tool built with Node.js",
"main": "src/index.js",
"bin": {
"modern": "./bin/cli.js"
},
"type": "module",
"engines": {
"node": ">=14.0.0"
},
"scripts": {
"start": "node ./bin/cli.js",
"dev": "nodemon ./bin/cli.js",
"test": "jest",
"build": "esbuild src/index.js --bundle --platform=node --outfile=dist/index.js",
"lint": "eslint src/**/*.js"
},
"files": ["bin", "dist", "src"],
"keywords": ["cli", "tool", "nodejs"],
"author": "Your Name",
"license": "MIT"
}
核心依赖选择
现代CLI工具需要以下关键依赖:
{
"dependencies": {
"commander": "^11.0.0", // 命令行参数解析
"inquirer": "^9.2.10", // 交互式提示
"chalk": "^5.2.0", // 终端样式
"ora": "^7.0.1", // 加载动画
"boxen": "^7.0.2", // 终端框
"figlet": "^1.6.0", // ASCII艺术字
"update-notifier": "^6.0.2", // 更新提示
"fs-extra": "^11.1.1", // 增强的文件操作
"axios": "^1.4.0" // HTTP请求
},
"devDependencies": {
"esbuild": "^0.18.0",
"jest": "^29.5.0",
"eslint": "^8.42.0",
"nodemon": "^3.0.1"
}
}
实现核心功能
1. 创建CLI入口文件
// bin/cli.js
#!/usr/bin/env node
import { program } from 'commander';
import chalk from 'chalk';
import figlet from 'figlet';
import updateNotifier from 'update-notifier';
import { readPackageUp } from 'read-pkg-up';
// 检查更新
const pkg = await readPackageUp();
updateNotifier({ pkg: pkg.packageJson }).notify();
// 显示欢迎信息
console.log(
chalk.cyan(
figlet.textSync('Modern CLI', {
font: 'Standard',
horizontalLayout: 'full'
})
)
);
program
.name('modern')
.description('A modern CLI tool for developers')
.version(pkg.packageJson.version);
// 注册命令
import initCommand from '../src/commands/init.js';
import generateCommand from '../src/commands/generate.js';
import configCommand from '../src/commands/config.js';
initCommand(program);
generateCommand(program);
configCommand(program);
program.parse(process.argv);
2. 实现初始化命令
// src/commands/init.js
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default function initCommand(program) {
program
.command('init [project-name]')
.description('Initialize a new project')
.option('-t, --template <template>', 'Specify template type')
.option('-y, --yes', 'Skip prompts and use defaults')
.action(async (projectName, options) => {
try {
// 交互式问答
const answers = options.yes
? getDefaultAnswers()
: await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: 'Project name:',
default: projectName || 'my-project',
validate: (input) => {
if (!input.trim()) return 'Project name is required';
if (!/^[a-z0-9-]+$/.test(input)) {
return 'Project name can only contain lowercase letters, numbers, and hyphens';
}
return true;
}
},
{
type: 'list',
name: 'template',
message: 'Select template:',
choices: [
{ name: 'React + TypeScript', value: 'react-ts' },
{ name: 'Vue 3 + TypeScript', value: 'vue-ts' },
{ name: 'Node.js API', value: 'node-api' },
{ name: 'Library', value: 'library' }
],
default: options.template || 'react-ts'
},
{
type: 'checkbox',
name: 'features',
message: 'Select additional features:',
choices: [
{ name: 'ESLint', value: 'eslint', checked: true },
{ name: 'Prettier', value: 'prettier', checked: true },
{ name: 'Husky', value: 'husky' },
{ name: 'Testing (Jest)', value: 'testing' },
{ name: 'Docker', value: 'docker' }
]
}
]);
// 创建项目
const spinner = ora('Creating project...').start();
const projectPath = path.resolve(process.cwd(), answers.projectName);
// 检查目录是否存在
if (await fs.pathExists(projectPath)) {
spinner.fail(`Directory ${answers.projectName} already exists`);
process.exit(1);
}
// 创建目录
await fs.ensureDir(projectPath);
// 复制模板文件
const templateDir = path.join(__dirname, '../../templates', answers.template);
await fs.copy(templateDir, projectPath);
// 生成package.json
const packageJson = {
name: answers.projectName,
version: '1.0.0',
private: true,
scripts: {
dev: getDevScript(answers.template),
build: getBuildScript(answers.template),
test: answers.features.includes('testing') ? 'jest' : undefined
},
dependencies: getDependencies(answers.template),
devDependencies: getDevDependencies(answers.template, answers.features)
};
await fs.writeJson(
path.join(projectPath, 'package.json'),
packageJson,
{ spaces: 2 }
);
// 生成配置文件
if (answers.features.includes('eslint')) {
await generateEslintConfig(projectPath);
}
spinner.succeed('Project created successfully!');
// 显示后续步骤
console.log('\n' + chalk.bold('Next steps:'));
console.log(chalk.cyan(` cd ${answers.projectName}`));
console.log(chalk.cyan(' npm install'));
console.log(chalk.cyan(' npm run dev\n'));
} catch (error) {
console.error(chalk.red('Error:'), error.message);
process.exit(1);
}
});
}
function getDefaultAnswers() {
return {
projectName: 'my-project',
template: 'react-ts',
features: ['eslint', 'prettier']
};
}
// 辅助函数省略...