前言
大家好!今天我们要一起开发一个前端项目脚手架工具 - Tohru-CLI。这个工具可以帮助我们快速创建基于React的管理系统模板。通过这个教程,你将学习到如何从零开始构建一个完整的CLI工具,包括命令行交互、Git操作、进度提示等实用功能。
技术栈选择
- 🔧 TypeScript:提供类型安全和更好的开发体验
- 📦 核心依赖:
- commander:命令行解析工具
- prompts:交互式命令行工具
- shelljs:Shell命令执行
- ora:终端加载动画
- fs-extra:增强的文件系统操作
详细实现步骤
1. 项目初始化
首先创建项目并安装依赖:
# 创建项目目录
mkdir tohru-cli
cd tohru-cli
# 初始化package.json
pnpm init
# 安装核心依赖
pnpm add commander prompts shelljs ora fs-extra
pnpm add -D typescript @types/node esbuild nodemon ts-node
2. 项目结构设计
tohru-cli/
├── src/
│ ├── interactiveTools/ # 交互式工具
│ │ ├── config.ts # 交互配置
│ │ └── index.ts # 交互逻辑
│ ├── utils/ # 工具函数
│ │ ├── cmd.ts # 命令行工具
│ │ └── prompt.ts # 提示工具
│ └── test/ # 入口文件
│ └── index.ts # CLI入口
├── bin/ # 编译输出目录
├── package.json
└── tsconfig.json
3. 核心功能实现
3.1 命令行入口(src/test/index.ts)
const { Command } = require("commander");
const interactiveTools = require("../interactiveTools");
const program = new Command();
program
.name("tohru")
.description("🐉 小林家的龙女仆前端项目脚手架")
.version("1.0.0");
program
.command("create")
.argument("[project-name]", "项目名")
.description("创建一个新项目")
.action((projectName: string) => {
interactiveTools.createProject(projectName);
});
program.parse();
这是CLI的入口文件,使用commander设置了:
- 工具名称:tohru
- 版本号:1.0.0
- create命令:用于创建新项目
- 可选参数:project-name(项目名称)
3.2 交互配置(src/interactiveTools/config.ts)
const projectNameConfig = {
type: "text",
name: "projectName",
message: "项目名是?",
};
const repoConfig = {
type: "select",
name: "projectType",
message: "选择拉取的远程仓库",
choices: [
{
title: "github-优先即使更新",
value: "https://github.com/zhuxin0/react-admin-template.git",
},
{
title: "gitee-国内速度快",
value: "https://gitee.com/king_zhu/react-admin-template.git",
},
],
initial: 1,
};
配置文件定义了两个交互提示:
- projectNameConfig:项目名称输入提示
- repoConfig:模板仓库选择提示,提供GitHub和Gitee两个源
3.3 交互工具实现(src/interactiveTools/index.ts)
const fs = require("fs-extra");
const path = require("path");
const shellFn = require("../utils/cmd");
const ora = require("ora");
const promptUtils = require("../utils/prompt");
const promptConfig = require("./config");
// 获取项目名称
const getProjectName = async (name: string) => {
const projectName = await promptUtils.getValue(
name,
promptConfig.projectNameConfig
);
return projectName;
};
// 获取仓库地址
const getRepo = async (repo: string) => {
const projectType = await promptUtils.getValue(repo, promptConfig.repoConfig);
return projectType;
};
// 创建项目主函数
const createProject = async (name: string, repo: string) => {
// 1. 获取用户输入
const projectName = await getProjectName(name);
const projectType = await getRepo(repo);
const projectPath = path.resolve(process.cwd(), projectName);
// 2. 检查目录是否存在
if (fs.existsSync(projectPath)) {
console.error(`❌ 目录 ${projectName} 已存在`);
process.exit(1);
}
// 3. 克隆模板
const spinner = ora(`正在从 ${projectType} 拉取模板...`).start();
try {
await shellFn.exitTimeout(`git clone ${projectType} "${projectPath}"`);
spinner.succeed("拉取成功");
// 4. 清理.git文件夹
fs.removeSync(path.resolve(projectPath, ".git"));
// 5. 提示后续步骤
console.log(`👉 cd ${projectName}`);
console.log(`👉 npm install && npm run dev`);
} catch (error) {
spinner.fail("拉取失败");
console.log(error);
}
};
这是核心实现文件,主要功能包括:
- 收集用户输入(项目名和模板源)
- 验证项目目录是否已存在
- 显示进度动画并克隆模板
- 清理git信息
- 提供后续操作提示
3.4 提示工具(src/utils/prompt.ts)
const prompts = require('prompts');
interface PromptConfig {
type: string;
name: string;
message: string;
initial?: string;
[key: string]: any;
}
async function getValue<T>(
value: T | undefined,
config: PromptConfig
): Promise<T> {
// 如果已经有值,直接返回
if (value !== undefined) {
return value;
}
// 否则显示交互式提示
const response = await prompts(config);
return response[config.name];
}
提示工具的主要功能:
- 定义了提示配置的类型接口
- 实现了getValue函数,支持默认值和交互式输入
- 使用泛型保证类型安全
3.5 命令执行工具(src/utils/cmd.ts)
const shell = require("shelljs");
const exitTimeout = (cmd: string, timeoutMs = 15000): Promise<void> => {
return new Promise((resolve, reject) => {
const child = shell.exec(cmd, { async: true, silent: false });
const timeout = setTimeout(() => {
child.kill();
reject(new Error(`命令超时(${timeoutMs}ms):${cmd}`));
}, timeoutMs);
child.on("exit", (code: number) => {
clearTimeout(timeout);
if (code === 0) resolve();
else reject(new Error(`命令执行失败(exit code: ${code})`));
});
});
};
命令执行工具的特点:
- 支持异步执行shell命令
- 实现了超时控制(默认15秒)
- 提供了错误处理机制
4. 打包配置
在package.json中添加必要的配置:
{
"name": "tohru-cli",
"version": "1.0.1",
"bin": {
"tohru": "./bin/index.js"
},
"scripts": {
"dev": "nodemon --config nodemon.json",
"build": "esbuild src/test/index.ts --bundle --platform=node --format=cjs --outfile=bin/index.js --banner:js='#!/usr/bin/env node'",
"test": "node bin/index.js",
"cli": "node bin/index.js"
}
}
关键配置说明:
-
bin字段:
- 定义全局命令名称
- 安装包后,npm会根据这个配置创建命令软链接
-
build脚本参数说明:
--bundle:将所有依赖打包到一个文件--platform=node:指定运行环境为Node.js--format=cjs:使用CommonJS模块格式--outfile:指定输出文件路径
-
--banner:js='#!/usr/bin/env node'的重要性:- 这行代码被称为 "shebang" 或 "hashbang"
- 它告诉Unix/Linux系统用什么程序来执行这个文件
#!/usr/bin/env node的工作原理:#!是 shebang 标记/usr/bin/env是一个程序,用于在环境变量 PATH 中查找后面指定的命令node指定使用 node 来执行这个文件
- 为什么要用
/usr/bin/env?- 不同系统中 node 可能安装在不同位置
/usr/bin/env会自动在 PATH 中查找 node- 这样可以提高脚本的可移植性
- 如果不加这个 banner:
- Unix/Linux 系统将无法直接执行该文件
- 用户必须显式使用 node 来执行:
node tohru.js - 全局命令将无法正常工作
-
dev脚本:
- 使用 nodemon 实现开发时热重载
- 修改代码后自动重启服务
-
test/cli脚本:
- 用于本地测试命令行工具
- 直接执行编译后的文件
💡 Tips:
- 在 Windows 系统中,shebang 行不起作用,但 npm 会自动创建一个 CMD 文件来执行你的脚本
- 确保生成的文件具有可执行权限:
chmod +x bin/index.js - 如果使用其他打包工具(如 webpack),也需要确保添加这个 banner
- 可以通过检查生成的文件来确认 banner 是否正确添加:
head -n 1
5. 本地测试
# 链接到全局
pnpm link --global
# 测试命令
tohru create my-project
6. 发布到npm
# 登录npm
npm login
# 发布包
npm publish
💡 Tips:
- 发布到npmjs上必须切换到npmjs官方镜像源,否则会发布失败
- 发布成功后,等待约5分钟,包会自动同步到淘宝镜像源(registry.npmmirror.com/)
- 发布完成后,可以切换回淘宝镜像源加速安装:
npm config set registry https://registry.npmmirror.com/ - 如果不想每次发布都手动切换镜像源,可以使用
nrm工具管理镜像源:# 安装nrm npm install -g nrm # 查看可用镜像源 nrm ls # 切换镜像源 nrm use npm # 发布包时切换到npm官方源 nrm use taobao # 日常开发切换到淘宝源 - 发布前检查项:
- package.json 中的 name 是否唯一(可以在 npmjs.com 搜索确认)
- version 版本号是否已更新
- files 字段是否包含了所有需要发布的文件
- README.md 是否包含了完整的使用说明
使用示例
安装CLI工具:
npm install -g tohru-cli
创建新项目:
tohru create my-project
最佳实践与优化建议
1. 错误处理
- 添加更多的错误检查
- 实现优雅的错误提示
- 添加debug模式
// 示例:添加debug模式
const debug = require('debug')('tohru:cli');
if (process.env.DEBUG) {
debug('错误详情:', error);
} else {
console.error('创建失败,使用 DEBUG=tohru:cli 查看详细信息');
}
2. 配置管理
- 支持.tohrurc配置文件
- 允许自定义模板源
- 记住用户偏好设置
// 示例:读取配置文件
const config = require('rc')('tohru', {
defaultTemplate: 'github',
timeout: 15000
});
3. 进度提示优化
- 添加更详细的进度信息
- 支持多步骤进度展示
- 添加颜色区分
// 示例:多步骤进度
const steps = ['检查环境', '下载模板', '安装依赖', '初始化git'];
const multiSpinner = ora().start();
for (const step of steps) {
multiSpinner.text = `[${step}] 进行中...`;
await executeStep(step);
multiSpinner.succeed(`[${step}] 完成`);
}
常见问题解决
- 模板下载失败
- 检查网络连接
- 尝试切换模板源
- 增加超时时间
- 权限问题
- 检查目录权限
- 使用sudo(不推荐)
- 修改npm配置
- 依赖冲突
- 清理node_modules
- 更新依赖版本
- 检查peerDependencies
结语
通过这个详细的教程,你应该已经掌握了如何开发一个功能完整的CLI工具。记住,好的CLI工具应该:
- 使用简单直观
- 提供清晰的反馈
- 具有良好的容错性
- 支持个性化配置
希望这个教程能帮助你开发出自己的CLI工具!如果遇到问题,欢迎查看完整源码或提出issue。
祝你开发愉快!🎉