04-CLI 工具开发
CLI 工具开发涉及命令行交互、终端美化、文件操作和模板生成等核心功能。
📑 目录
快速参考
工具选型速查表
| 工具类型 | 推荐工具 | 适用场景 | 备选方案 |
|---|---|---|---|
| 命令行解析 | commander | 功能丰富、API 友好 | yargs(灵活)、meow(轻量) |
| 交互式输入 | inquirer | 功能全面、生态丰富 | prompts(轻量)、enquirer |
| 终端美化 | chalk | 功能丰富、API 友好 | picocolors(极轻量)、kleur |
| 加载动画 | ora | 简单易用 | - |
| 进度条 | cli-progress | 文件上传/下载进度 | - |
| 文件操作 | fs-extra | Promise API、功能增强 | - |
| 文件匹配 | glob | 通配符匹配 | - |
| 模板引擎 | handlebars | 轻量、逻辑少 | ejs、mustache |
快速开始
bash
# 1. 安装核心工具
pnpm add commander inquirer chalk ora fs-extra glob handlebars
# 2. 创建 CLI 入口文件
# src/cli.ts
# 3. 配置 package.json bin 字段
命令行交互
commander(推荐)
commander 是一款 Node.js 命令行解析工具,核心用途是解析命令行参数,让 CLI 工具的命令行交互更友好、专业。
优势:
- ✅ API 友好,链式调用
- ✅ 功能丰富,支持子命令、选项、帮助信息
- ✅ 生态完善,文档详细
- ✅ 自动生成帮助信息
劣势:
- ❌ 体积较大(相比 meow)
安装
bash
pnpm add commander
pnpm add @types/commander -D
基础用法
ts
// src/cli.ts
import { program } from 'commander';
program
.version('1.0.0', '-v, --version')
.description('一个基于 commander + inquirer 的 CLI 工具示例');
// 定义无参数命令
program
.command('init')
.description('初始化项目')
.action(() => {
console.log('开始初始化项目...');
});
// 定义带选项的命令
program
.command('build')
.description('打包项目')
.option('-e, --env <env>', '打包环境', 'development')
.option('-o, --outDir <dir>', '输出目录', 'dist')
.action((options) => {
console.log('开始打包...');
console.log('打包环境:', options.env);
console.log('输出目录:', options.outDir);
});
program.parse(process.argv);
选项配置
| 选项格式 | 说明 | 示例 |
|---|---|---|
.option('-s, --single') |
布尔型选项(无参数,存在即 true) | your-cli --single → { single: true } |
.option('-n, --name <name>') |
必填参数选项 | your-cli --name test → { name: 'test' } |
.option('-a, --age [age]') |
可选参数选项 | your-cli --age 25 → { age: 25 };不传则为 undefined |
.option('--env <env>', '描述', 'dev') |
带默认值的选项 | 不传 --env 时,默认 { env: 'dev' } |
高级用法
ts
// 子命令
program
.command('create <name>')
.description('创建新项目')
.option('-t, --template <template>', '模板类型', 'default')
.action((name, options) => {
console.log(`创建项目 ${name},使用模板 ${options.template}`);
});
// 必需选项
program.requiredOption('-c, --config <path>', '配置文件路径').parse();
// 自定义帮助信息
program.addHelpText('after', '\n示例:\n $ my-cli init\n $ my-cli build --env production');
yargs
yargs 是功能强大的命令行解析工具,支持位置参数、命令补全等高级功能。
优势:
- ✅ 功能强大,支持位置参数
- ✅ 灵活的配置方式
- ✅ 支持命令补全
劣势:
- ❌ API 相对复杂
- ❌ 学习曲线较陡
安装
bash
pnpm add yargs
pnpm add @types/yargs -D
基础用法
ts
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const argv = yargs(hideBin(process.argv))
.option('name', {
alias: 'n',
type: 'string',
description: '项目名称',
demandOption: true,
})
.option('template', {
alias: 't',
type: 'string',
default: 'default',
description: '模板类型',
})
.command('init <name>', '初始化项目', (yargs) => {
return yargs.positional('name', {
describe: '项目名称',
type: 'string',
});
})
.parseSync();
console.log(argv);
meow
meow 是轻量级的命令行解析工具,适合简单场景。
优势:
- ✅ 轻量级,体积小
- ✅ 配置简单
- ✅ 自动处理帮助信息
劣势:
- ❌ 功能相对简单
- ❌ 不支持复杂命令结构
安装
bash
pnpm add meow
基础用法
ts
import meow from 'meow';
const cli = meow(
`
用法
$ my-cli <input>
选项
--name, -n 项目名称
--template, -t 模板类型
示例
$ my-cli init --name my-project
`,
{
importMeta: import.meta,
flags: {
name: {
type: 'string',
alias: 'n',
},
template: {
type: 'string',
alias: 't',
default: 'default',
},
},
},
);
console.log(cli.input[0]); // 命令参数
console.log(cli.flags); // 选项
命令行解析工具对比
| 工具 | 体积 | 配置复杂度 | 功能丰富度 | 适用场景 |
|---|---|---|---|---|
| commander | 较大 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 功能丰富的 CLI 工具 |
| yargs | 较大 | ⭐⭐ | ⭐⭐⭐⭐⭐ | 需要位置参数的场景 |
| meow | 轻量级 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 简单 CLI 工具 |
选型建议:
- 功能丰富的 CLI 工具:commander(推荐)
- 需要位置参数:yargs
- 简单工具:meow
交互式输入
inquirer(推荐)
当命令行参数无法满足需求(如让用户选择框架、输入密码、确认操作)时,inquirer 提供「交互式输入」。
优势:
- ✅ 功能全面,支持多种交互类型
- ✅ 生态丰富,插件多
- ✅ 文档完善
劣势:
- ❌ 体积较大
- ❌ 配置相对复杂
安装
bash
pnpm add inquirer
pnpm add @types/inquirer -D
基础用法
ts
import { program } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
program
.command('init')
.description('初始化项目')
.action(async () => {
console.log(chalk.blue('📦 开始初始化项目...'));
const answers = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: '请输入项目名称:',
default: 'my-project',
validate: (value) => {
if (!value.trim()) return '项目名称不能为空!';
return true;
},
},
{
type: 'list',
name: 'framework',
message: '请选择项目框架:',
choices: [
{ name: 'React + TypeScript', value: 'react-ts' },
{ name: 'Vue + TypeScript', value: 'vue-ts' },
{ name: 'Vanilla JS', value: 'vanilla' },
],
default: 'react-ts',
},
{
type: 'checkbox',
name: 'modules',
message: '请选择需要的功能模块:',
choices: ['路由', '状态管理', 'UI 组件库', 'ESLint/Prettier'],
default: ['路由', 'ESLint/Prettier'],
},
{
type: 'confirm',
name: 'initGit',
message: '是否初始化 Git 仓库?',
default: true,
},
]);
console.log(chalk.green('\n✅ 项目配置如下:'));
console.log('项目名称:', answers.projectName);
console.log('框架:', answers.framework);
console.log('功能模块:', answers.modules.join(', '));
console.log('初始化 Git:', answers.initGit ? '是' : '否');
});
program.parse(process.argv);
核心交互类型
| 类型 | 用途 | 关键配置 |
|---|---|---|
input |
普通文本输入(如项目名称、邮箱) | message、default、validate |
password |
密码输入(输入内容隐藏) | 同 input,自动隐藏输入 |
list |
单选(如框架选择、环境选择) | choices(选项数组)、default |
checkbox |
多选(如功能模块、依赖选择) | choices、default(默认选中项) |
confirm |
二选一确认(是 / 否) | message、default(true/false) |
rawlist |
带编号的单选(按数字选择) | 同 list,选项前显示编号 |
autocomplete |
带自动补全的输入(如文件路径) | 需配合 inquirer-autocomplete-prompt 插件 |
prompts
prompts 是轻量级的交互式输入工具,API 简洁。
优势:
- ✅ 轻量级,体积小
- ✅ API 简洁
- ✅ 支持取消操作(Ctrl+C)
劣势:
- ❌ 功能相对简单
- ❌ 生态较小
安装
bash
pnpm add prompts
基础用法
ts
import prompts from 'prompts';
const response = await prompts([
{
type: 'text',
name: 'projectName',
message: '项目名称',
initial: 'my-project',
validate: (value) => (value.trim() ? true : '项目名称不能为空'),
},
{
type: 'select',
name: 'framework',
message: '选择框架',
choices: [
{ title: 'React', value: 'react' },
{ title: 'Vue', value: 'vue' },
],
},
]);
console.log(response);
enquirer
enquirer 是现代化的交互式输入工具,支持自定义提示符。
优势:
- ✅ 现代化设计
- ✅ 支持自定义提示符
- ✅ API 灵活
劣势:
- ❌ 文档相对较少
- ❌ 生态较小
安装
bash
pnpm add enquirer
基础用法
ts
import { prompt } from 'enquirer';
const response = await prompt({
type: 'input',
name: 'projectName',
message: '项目名称',
initial: 'my-project',
});
console.log(response);
交互式输入工具对比
| 工具 | 体积 | 配置复杂度 | 功能丰富度 | 生态 | 适用场景 |
|---|---|---|---|---|---|
| inquirer | 较大 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 功能全面的 CLI 工具 |
| prompts | 轻量级 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 简单交互场景 |
| enquirer | 中等 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 需要自定义提示符 |
选型建议:
- 功能全面的 CLI 工具:inquirer(推荐)
- 简单交互场景:prompts
- 需要自定义提示符:enquirer
终端美化
chalk(推荐)
chalk 是 Node.js 终端日志彩色打印工具,核心用途是给终端输出的文字添加颜色、背景色、加粗 / 下划线等样式。
优势:
- ✅ 功能丰富,API 友好
- ✅ 支持链式调用
- ✅ 生态完善
劣势:
- ❌ 体积较大(相比 picocolors)
安装
bash
pnpm add chalk
封装日志函数
ts
// src/utils/logger.ts
import chalk from 'chalk';
export enum LogType {
SUCCESS = 'success',
ERROR = 'error',
WARN = 'warn',
INFO = 'info',
}
export const log = (message: string, type: LogType = LogType.INFO) => {
const prefixMap = {
[LogType.SUCCESS]: chalk.green('✅'),
[LogType.ERROR]: chalk.bold.red('❌'),
[LogType.WARN]: chalk.yellow('⚠️'),
[LogType.INFO]: chalk.blue('ℹ️'),
};
const colorMap = {
[LogType.SUCCESS]: chalk.green,
[LogType.ERROR]: chalk.red,
[LogType.WARN]: chalk.yellow,
[LogType.INFO]: chalk.blue,
};
const prefix = prefixMap[type];
const color = colorMap[type];
console.log(`${prefix} ${color(message)}`);
};
export const logSuccess = (message: string) => log(message, LogType.SUCCESS);
export const logError = (message: string) => log(message, LogType.ERROR);
export const logWarn = (message: string) => log(message, LogType.WARN);
export const logInfo = (message: string) => log(message, LogType.INFO);
使用
ts
import { logSuccess, logError, logWarn, logInfo } from './utils/logger';
logInfo('正在初始化项目...');
logWarn('当前 Node 版本低于推荐版本');
logSuccess('项目初始化完成!');
logError('配置文件缺失,请检查 config.json');
picocolors
picocolors 是极轻量的终端颜色库,体积仅 0.5KB。
优势:
- ✅ 极轻量(0.5KB)
- ✅ API 简洁
- ✅ 性能好
劣势:
- ❌ 功能相对简单
- ❌ 不支持链式调用
安装
bash
pnpm add picocolors
基础用法
ts
import pc from 'picocolors';
console.log(pc.green('成功'));
console.log(pc.red('错误'));
console.log(pc.bold(pc.blue('加粗蓝色')));
kleur
kleur 是轻量级的终端颜色库,API 类似 chalk。
优势:
- ✅ 轻量级(体积小)
- ✅ API 类似 chalk,迁移成本低
- ✅ 支持链式调用
劣势:
- ❌ 功能相对简单
安装
bash
pnpm add kleur
基础用法
ts
import kleur from 'kleur';
console.log(kleur.green('成功'));
console.log(kleur.red('错误'));
console.log(kleur.bold().blue('加粗蓝色'));
ora(展示加载动画)
ora 是一款 Node.js 终端加载动画工具,核心用途是在耗时操作时显示「加载中」动画 + 提示文字。
安装
bash
pnpm add ora
封装加载动画函数
ts
// src/utils/loader.ts
import ora from 'ora';
import chalk from 'chalk';
export const withLoader = async <T>(message: string, asyncFn: () => Promise<T>): Promise<T> => {
const spinner = ora(chalk.bold.blue(message)).start();
try {
const result = await asyncFn();
spinner.succeed(chalk.green('✅ 操作完成!'));
return result;
} catch (error) {
spinner.fail(chalk.bold.red(`❌ 操作失败:${(error as Error).message}`));
throw error;
}
};
使用示例
ts
import { withLoader } from './utils/loader';
await withLoader('正在请求接口数据...', async () => {
await new Promise((resolve) => setTimeout(resolve, 1500));
});
高级用法
ts
import ora from 'ora';
const spinner = ora('加载中...').start();
// 更新文本
spinner.text = '处理中...';
// 成功
spinner.succeed('完成!');
// 失败
spinner.fail('失败!');
// 警告
spinner.warn('警告!');
// 信息
spinner.info('信息');
cli-progress(进度条)
cli-progress 用于显示文件上传/下载、批量处理等操作的进度。
安装
bash
pnpm add cli-progress
基础用法
ts
import cliProgress from 'cli-progress';
const bar = new cliProgress.SingleBar({
format: '进度 |{bar}| {percentage}% | {value}/{total}',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
hideCursor: true,
});
bar.start(100, 0);
// 模拟进度
for (let i = 0; i <= 100; i++) {
await new Promise((resolve) => setTimeout(resolve, 50));
bar.update(i);
}
bar.stop();
boxen(边框框)
boxen 用于在终端中创建带边框的文本框,适合显示重要信息。
安装
bash
pnpm add boxen
基础用法
ts
import boxen from 'boxen';
import chalk from 'chalk';
const message = boxen(chalk.green('✅ 项目初始化完成!'), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'green',
});
console.log(message);
cli-table3(表格展示)
cli-table3 用于在终端中展示表格数据,适合显示配置信息、对比数据等。
安装
bash
pnpm add cli-table3
pnpm add @types/cli-table3 -D
基础用法
ts
import Table from 'cli-table3';
const table = new Table({
head: ['工具', '用途', '推荐度'],
colWidths: [20, 30, 10],
});
table.push(
['commander', '命令行解析', '⭐⭐⭐⭐⭐'],
['inquirer', '交互式输入', '⭐⭐⭐⭐⭐'],
['chalk', '终端美化', '⭐⭐⭐⭐⭐'],
);
console.log(table.toString());
终端美化工具对比
| 工具 | 体积 | 功能 | 适用场景 |
|---|---|---|---|
| chalk | 较大 | 颜色、样式 | 功能丰富的 CLI 工具 |
| picocolors | 极轻量 | 基础颜色 | 对体积敏感的项目 |
| kleur | 轻量级 | 颜色、样式 | 轻量级 CLI 工具 |
| ora | 中等 | 加载动画 | 耗时操作提示 |
| cli-progress | 中等 | 进度条 | 文件处理进度 |
| boxen | 轻量级 | 边框框 | 重要信息展示 |
| cli-table3 | 中等 | 表格 | 数据展示 |
选型建议:
- 功能丰富的 CLI 工具:chalk(推荐)
- 对体积敏感:picocolors
- 需要进度条:cli-progress
- 需要表格展示:cli-table3
文件操作
fs-extra(操作文件系统)
fs-extra 在 Node.js 原生 fs 模块基础上做了增强,核心优势:
- 完全兼容原生 fs 模块(可直接替换 fs 使用)
- 所有 API 支持 Promise(无需手动封装 util.promisify)
- 新增高频实用功能(递归创建目录、递归删除目录、复制文件 / 目录等)
安装
bash
pnpm add fs-extra
pnpm add @types/fs-extra -D
核心用法
ts
import fs from 'fs-extra';
import path from 'path';
// 递归创建目录
await fs.ensureDir(path.resolve(__dirname, 'a/b/c'));
// 递归删除目录
await fs.remove(path.resolve(__dirname, 'a'));
// 复制文件/目录
await fs.copy(src, dest);
// 写入 JSON 文件
await fs.writeJson(jsonPath, { name: 'test', version: '1.0.0' }, { spaces: 2 });
// 读取 JSON 文件
const config = await fs.readJson(jsonPath);
// 判断文件/目录是否存在
const exists = await fs.pathExists(path);
常用 API
| API 名称 | 核心用途 | 优势对比(vs 原生 fs) |
|---|---|---|
fs.ensureDir(path) |
递归创建目录(不存在则创建,存在则忽略) | 原生需手动递归,fs-extra 一键实现 |
fs.remove(path) |
递归删除文件 / 目录(支持任意层级) | 原生需先遍历目录,fs-extra 一键删除 |
fs.copy(src, dest) |
复制文件 / 目录(自动创建目标目录) | 原生需区分文件 / 目录,fs-extra 自动适配 |
fs.writeJson(path, data) |
写入 JSON 文件(自动 stringify) | 原生需手动 JSON.stringify,fs-extra 简化步骤 |
fs.readJson(path) |
读取 JSON 文件(自动 parse) | 原生需手动 JSON.parse,fs-extra 简化步骤 |
fs.pathExists(path) |
判断文件 / 目录是否存在(返回 boolean) | 原生需用 fs.access 捕获错误,fs-extra 直接返回 |
glob(匹配文件)
glob 解决「按规则批量查找文件」的需求,支持用通配符(如 *、**、?)匹配文件路径。
安装
bash
pnpm add glob
pnpm add @types/glob -D
核心用法
ts
import glob from 'glob';
import path from 'path';
// 同步匹配
const files = glob.sync('src/**/*.ts', {
cwd: process.cwd(),
ignore: ['src/test/**/*'],
});
// 异步匹配(推荐)
const files = await glob.promise('src/**/*.{ts,js}', {
cwd: process.cwd(),
dot: true,
});
// 流式匹配(适合大量文件)
const stream = glob.stream('src/**/*.ts');
stream.on('data', (filePath) => {
console.log('匹配到文件:', filePath);
});
常见通配符规则
| 通配符 | 含义 | 示例 | 匹配结果 |
|---|---|---|---|
* |
匹配当前目录下的任意字符(不含子目录) | src/*.ts |
src/index.ts、src/utils.ts |
** |
匹配任意层级的目录(递归) | src/**/*.ts |
src/index.ts、src/a/b/utils.ts |
? |
匹配单个字符 | src/file?.ts |
src/file1.ts、src/file2.ts |
[] |
匹配括号内的任意一个字符 | src/[ab].ts |
src/a.ts、src/b.ts |
! |
排除匹配的文件 | src/**/*.ts + !src/test.ts |
所有 .ts 文件,排除 src/test.ts |
模板生成
handlebars(生成模板文件)
handlebars 是一款逻辑少、轻量型的模板引擎,核心用途是「将数据与模板结合,动态生成文本内容」。
安装
bash
pnpm add handlebars
pnpm add @types/handlebars -D
基础用法
ts
import handlebars from 'handlebars';
// 1. 定义模板
const template = `{
"name": "{{ projectName }}",
"version": "{{ version }}",
"description": "{{ description }}"
}`;
// 2. 编译模板
const compiledTemplate = handlebars.compile(template);
// 3. 传入数据渲染
const data = {
projectName: 'my-ts-pkg',
version: '1.0.0',
description: '基于 TS + 双模式的 npm 包',
};
const result = compiledTemplate(data);
console.log(result);
核心语法
- 变量占位符 :
{{ 变量名 }}(支持嵌套对象) - 条件判断 :
{{#if 条件}}...{{else}}...{{/if}} - 循环遍历 :
{{#each 数组}}...{{/each}} - 注释 :
{{! 注释内容 }}
高级用法
ts
// 注册辅助函数
handlebars.registerHelper('uppercase', (str) => {
return str.toUpperCase();
});
// 使用辅助函数
const template = `{{ uppercase name }}`;
完整示例
结合所有工具,实现一个完整的 CLI 工具:
ts
import { program } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
import fs from 'fs-extra';
import glob from 'glob';
import handlebars from 'handlebars';
import path from 'path';
import boxen from 'boxen';
program.version('1.0.0', '-v, --version').description('CLI 工具示例');
program
.command('init [projectName]')
.description('初始化项目')
.option('-t, --template <template>', '模板类型', 'default')
.option('-y, --yes', '跳过交互式询问', false)
.action(async (projectName, options) => {
console.log(chalk.blue('📦 开始初始化项目...'));
let answers: any = {};
// 如果提供了项目名称且使用了 --yes,跳过交互
if (projectName && options.yes) {
answers = {
projectName,
framework: 'react-ts',
modules: ['路由', 'ESLint/Prettier'],
initGit: true,
};
} else {
// 交互式询问
answers = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: '项目名称',
default: projectName || 'my-project',
validate: (value) => {
if (!value.trim()) return '项目名称不能为空!';
if (!/^[a-z0-9-]+$/.test(value)) {
return '项目名称只能包含小写字母、数字和连字符!';
}
return true;
},
},
{
type: 'list',
name: 'framework',
message: '选择框架',
choices: [
{ name: 'React + TypeScript', value: 'react-ts' },
{ name: 'Vue + TypeScript', value: 'vue-ts' },
{ name: 'Vanilla JS', value: 'vanilla' },
],
default: 'react-ts',
},
{
type: 'checkbox',
name: 'modules',
message: '选择功能模块',
choices: ['路由', '状态管理', 'UI 组件库', 'ESLint/Prettier'],
default: ['路由', 'ESLint/Prettier'],
},
{
type: 'confirm',
name: 'initGit',
message: '是否初始化 Git 仓库?',
default: true,
},
]);
}
const spinner = ora(chalk.blue('正在生成项目文件...')).start();
try {
// 1. 检查目录是否存在
const targetDir = path.resolve(process.cwd(), answers.projectName);
if (await fs.pathExists(targetDir)) {
spinner.fail(chalk.red(`目录 ${answers.projectName} 已存在!`));
process.exit(1);
}
// 2. 创建项目目录
await fs.ensureDir(targetDir);
// 3. 读取模板文件
const templateDir = path.resolve(__dirname, '../templates', options.template);
const templateFiles = await glob.promise('**/*.hbs', {
cwd: templateDir,
dot: true,
});
// 4. 渲染模板并写入文件
for (const templateFile of templateFiles) {
const templatePath = path.resolve(templateDir, templateFile);
const templateContent = await fs.readFile(templatePath, 'utf8');
const compiled = handlebars.compile(templateContent);
const renderedContent = compiled(answers);
const targetFile = templateFile.replace(/\.hbs$/, '');
const targetPath = path.resolve(targetDir, targetFile);
await fs.ensureDir(path.dirname(targetPath));
await fs.writeFile(targetPath, renderedContent, 'utf8');
}
// 5. 初始化 Git(如果选择)
if (answers.initGit) {
spinner.text = '正在初始化 Git 仓库...';
// 这里可以调用 git 命令
}
spinner.succeed(chalk.green('项目初始化完成!'));
// 6. 显示成功信息
const successMessage = boxen(
chalk.green(`✅ 项目 ${answers.projectName} 创建成功!\n\n`) +
chalk.cyan(`cd ${answers.projectName}\n`) +
chalk.cyan('npm install\n') +
chalk.cyan('npm run dev'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'green',
},
);
console.log(successMessage);
} catch (error) {
spinner.fail(chalk.red(`初始化失败:${(error as Error).message}`));
process.exit(1);
}
});
program
.command('build')
.description('构建项目')
.option('-e, --env <env>', '构建环境', 'production')
.option('-o, --outDir <dir>', '输出目录', 'dist')
.action(async (options) => {
const spinner = ora(chalk.blue('正在构建项目...')).start();
try {
// 模拟构建过程
await new Promise((resolve) => setTimeout(resolve, 2000));
spinner.succeed(chalk.green(`构建完成!输出目录:${options.outDir}`));
} catch (error) {
spinner.fail(chalk.red(`构建失败:${(error as Error).message}`));
process.exit(1);
}
});
program.parse(process.argv);
最佳实践
- 错误处理:使用 try-catch 捕获错误,提供友好的错误提示
- 用户体验:使用 ora 显示加载状态,使用 chalk 美化输出
- 参数验证:在 inquirer 中使用 validate 验证用户输入
- 文件操作:使用 fs-extra 的 Promise API,避免回调地狱
- 模板管理:将模板文件放在独立目录,使用 glob 批量处理
- 命令结构:使用 commander 组织命令,保持清晰的命令层次
- 帮助信息:为每个命令添加清晰的描述和示例
常见问题
命令行解析相关问题
Q: commander 和 yargs 如何选择?
A:
- commander:API 友好,适合大多数场景(推荐)
- yargs:需要位置参数或复杂参数解析时使用
Q: 如何获取未定义的选项?
A:
ts
// commander
program.parse();
const unknownOptions = program.opts();
// yargs
const argv = yargs.parse();
const unknown = argv._; // 未定义的参数
交互式输入相关问题
Q: inquirer 和 prompts 如何选择?
A:
- inquirer:功能全面,生态丰富(推荐)
- prompts:轻量级,简单场景使用
Q: 如何中断交互式输入?
A:
ts
// inquirer 会自动处理 Ctrl+C
// prompts 需要手动处理
const response = await prompts({
type: 'text',
name: 'value',
message: '输入值',
onCancel: () => {
console.log('已取消');
process.exit(0);
},
});
终端美化相关问题
Q: chalk 和 picocolors 如何选择?
A:
- chalk:功能丰富,适合大多数场景(推荐)
- picocolors:对体积敏感的项目使用
Q: 如何检测终端是否支持颜色?
A:
ts
import chalk from 'chalk';
// chalk 会自动检测,不支持时自动禁用颜色
// 手动检测
const supportsColor = chalk.supportsColor;
文件操作相关问题
Q: fs-extra 和原生 fs 的区别?
A:
- fs-extra:Promise API、递归操作、JSON 操作更便捷
- 原生 fs:需要手动封装 Promise、手动递归
Q: glob 如何排除多个文件?
A:
ts
const files = await glob.promise('src/**/*.ts', {
ignore: ['src/test/**/*', 'src/**/*.test.ts'],
});
模板生成相关问题
Q: handlebars 和其他模板引擎的区别?
A:
- handlebars:逻辑少、轻量级(推荐)
- ejs:支持 JavaScript 代码,功能强大但体积大
- mustache:无逻辑模板,但功能较少
Q: 如何在模板中使用辅助函数?
A:
ts
handlebars.registerHelper('eq', (a, b) => a === b);
// 模板中使用
// {{#if (eq value "test")}}...{{/if}}