从零开始打造你的前端CLI工具:Tohru-CLI开发指南 🚀

315 阅读6分钟

前言

大家好!今天我们要一起开发一个前端项目脚手架工具 - 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);
  }
};

这是核心实现文件,主要功能包括:

  1. 收集用户输入(项目名和模板源)
  2. 验证项目目录是否已存在
  3. 显示进度动画并克隆模板
  4. 清理git信息
  5. 提供后续操作提示

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"
  }
}

关键配置说明:

  1. bin字段

    • 定义全局命令名称
    • 安装包后,npm会根据这个配置创建命令软链接
  2. build脚本参数说明

    • --bundle:将所有依赖打包到一个文件
    • --platform=node:指定运行环境为Node.js
    • --format=cjs:使用CommonJS模块格式
    • --outfile:指定输出文件路径
  3. --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
      • 全局命令将无法正常工作
  4. dev脚本

    • 使用 nodemon 实现开发时热重载
    • 修改代码后自动重启服务
  5. test/cli脚本

    • 用于本地测试命令行工具
    • 直接执行编译后的文件

💡 Tips:

  1. 在 Windows 系统中,shebang 行不起作用,但 npm 会自动创建一个 CMD 文件来执行你的脚本
  2. 确保生成的文件具有可执行权限:
    chmod +x bin/index.js
    
  3. 如果使用其他打包工具(如 webpack),也需要确保添加这个 banner
  4. 可以通过检查生成的文件来确认 banner 是否正确添加:
    head -n 1 
    

5. 本地测试

# 链接到全局
pnpm link --global

# 测试命令
tohru create my-project

6. 发布到npm

# 登录npm
npm login

# 发布包
npm publish

💡 Tips:

  1. 发布到npmjs上必须切换到npmjs官方镜像源,否则会发布失败
  2. 发布成功后,等待约5分钟,包会自动同步到淘宝镜像源(registry.npmmirror.com/)
  3. 发布完成后,可以切换回淘宝镜像源加速安装:
    npm config set registry https://registry.npmmirror.com/
    
  4. 如果不想每次发布都手动切换镜像源,可以使用nrm工具管理镜像源:
    # 安装nrm
    npm install -g nrm
    
    # 查看可用镜像源
    nrm ls
    
    # 切换镜像源
    nrm use npm     # 发布包时切换到npm官方源
    nrm use taobao  # 日常开发切换到淘宝源
    
  5. 发布前检查项:
    • 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}] 完成`);
}

常见问题解决

  1. 模板下载失败
  • 检查网络连接
  • 尝试切换模板源
  • 增加超时时间
  1. 权限问题
  • 检查目录权限
  • 使用sudo(不推荐)
  • 修改npm配置
  1. 依赖冲突
  • 清理node_modules
  • 更新依赖版本
  • 检查peerDependencies

结语

通过这个详细的教程,你应该已经掌握了如何开发一个功能完整的CLI工具。记住,好的CLI工具应该:

  • 使用简单直观
  • 提供清晰的反馈
  • 具有良好的容错性
  • 支持个性化配置

希望这个教程能帮助你开发出自己的CLI工具!如果遇到问题,欢迎查看完整源码或提出issue。

祝你开发愉快!🎉