头图

前言

最近在看脚手架的一些源码实现,想着自己做个简易版的脚手架。但是一直没有出发点,搁置了很久。后面在公司拉取代码的时候想到,我们有时候会去github或gitee下载源码到本地运行调试等。所以这给我了一个启发点,搞一个源码下载器!

下载及使用

npm install dl-repo-cli -g
在命令行输入dl-repo-cli直接回车 下一步即可

或可通过配置options手动配置platform和token的缓存配置
dl-repo-cli -p <github|gitee> -t <your token>
请确保你的platform和token是正确的,否则请求无效。
github token可在settings/Developer settings/Personal access tokens下创建,gitee类似

项目预览

preview.gif

项目地址
github api地址
gitee api地址

技术前瞻

  • axios:发请求的
  • chalk:给控制台点颜色瞧瞧
  • commander:轻松地定义命令和选项
  • fs-extra:提供了比原生fs模块更多的功能和实用的API
  • shelljs:可以让我们在Node.js中轻松地执行Shell命令和脚本
  • inquirer:帮助我们构建交互式命令行界面(CLI),并且提供了丰富的用户输入和选择方式
  • ora:在命令行界面中显示一个动态的加载指示器

下面对这些做个简单的理解,熟悉者可直接跳过😘

chalk

chalk用法比简单、且支持链式调用,主要用来给控制台输出区分颜色

console.log(chalk.blue('Hello') + ' World' + chalk.red('!'));
链式
console.log(chalk.blue.bgRed.bold('Hello world!'));

长这样

c.png

commander

基本配置

program
  .name('dl-repo-cli')
  .description('一个在终端运行的源码下载器cli,支持github、gitee')
  .version('1.0.0')
  program.parse()

长这样

Snipaste\_2023-06-06\_18-18-30.png

接下来配置options,可以通过option或addOption添加

  program
  .name('dl-repo-cli')
  .description('一个在终端运行的源码下载器cli,支持github、gitee')
  .version('1.0.0')
  .addOption(
    new Option(
      "-p, --platform <platform>",
      "代码托管平台(github、gitee)"
    ).choices([GITHUB, GITEE])
  )
  .option("-t, --token <token>", "代码托管平台的token")
  program.parse()

这里分别使用option和addOption,因为我希望-p这个参数为github或gitee使用new Option可以对选项进行更详尽的配置

配置完长这样
sn.png

当我们使用-p参数时只能为github或gitee,输入其他时会提示,就像这样

sna.png

最后可以通过action获取到options参数并书写后续相关逻辑,如下

ex.png

另外commande可以注册命令,这里我们用不到,详细用法参考这里

inquirer

inquirer可以用来创建交互式命令行,用过vue和react的脚手架都知道,创建项目时会各种询问你要装什么。最后生成一个你想要的项目,下面是个基本用法。

inquirer.prompt([
      {
        type: "list",
        name: "platform",
        message: "请选择平台",
        choices: [
          { name: "github", value: "github" },
          { name: "gitee", value: "gitee" },
        ],
      },
      {
        type: "password",
        name: "token",
        message: "请输入token",
        validate: function (value) {
          if (value.trim() === "") {
            return "请输入token";
          }
          return true;
        },
      },
    ])

通过配置type可以实现你想要的询问方式,有input、password、confirm、checkbox等等,此处validate参数用来做校验token不为空。

ora

这个可以用来在控制台加载loading,有时候请求时间过长、或者代码执行过久给一个loading是一个比较好的交互方式,代码如下。也是非常好理解

const spinner = ora('加载中......');
spinner.start();
setTimeout(() => {
        spinner.stop()
}, 3000);

fs-extra

fs-extra其实就是对node的fs模块做了封装处理,也是比较简单这里不做过多赘述

shelljs

可以用它执行shell脚本,我们源码下载器最后一个步骤就是使用shell.exec('git clone xxx')来执行下载,
也可以使用execa这个库实现相同功能。

正文

首先通过npm init创建package.json并创建bin文件夹以及index.js入口文件。然后修改package.json的main、type、bin属性。其中bin字段用于注册运行脚本的命令,全局安装后,可在全局执行该命令。这也是为什么安装vue和react脚手架后通过cli可以创建项目的原因。然后我们新建bin和src文件,文件结构如下👇

其中,constant.js是常量文件,utils.js是一些辅助函数,而serve下面则是两个平台的api接口文件

入口

一般入口文件放置bin目录下,当然你随便起什么名字都可以。入口文件只有三行代码

#! /usr/bin/env node
import entry from '../src/index.js';
entry()

! /usr/bin/env node,这行代码,它的作用是告诉系统这个脚本文件需要使用node来执行。

请求

请求是基于axios,利用工厂模式,针对github及gitee分别做不同的处理。后续如需支持新的平台可直接继承Serve。

image.png

核心

  1. 检查并更新缓存文件
  2. 输入仓库名称、语言等并搜索
  3. clone 下载
    对应代码中这三个函数 👇
    Snipaste_2023-06-19_18-12-38.png
checkCache
async function checkCache(t, p) {
  if (checkFileIsExist(TEMPFILEPATH)) {
    const { token, platform } = fs.readJsonSync(TEMPFILEPATH);
    fs.writeJsonSync(TEMPFILEPATH, {
      token: t ? t : token,
      platform: p ? p : platform,
    });
  } else if (t && p) {
    fs.writeJsonSync(TEMPFILEPATH, {
      token: t,
      platform: p,
    });
  } else {
    await creatCache();
  }
}

async function creatCache() {
  const answers = await getAnswers([
    {
      type: "list",
      name: "platform",
      message: "请选择平台",
      choices: [
        { name: "github", value: GITHUB },
        { name: "gitee", value: GITEE },
      ],
    },
    {
      type: "password",
      name: "token",
      message: "请输入token",
      validate: function (value) {
        if (value.trim() === "") {
          return "请输入token";
        }
        return true;
      },
    },
  ]);
  fs.writeJsonSync(TEMPFILEPATH, answers);
}
searchRepos
async function searchRepos() {
  const answers = await getAnswers([
    {
      type: "input",
      name: "repoName",
      message: "请输入仓库名称",
      validate: function (value) {
        if (value.trim() === "") {
          return "请输入仓库名称";
        }
        return true;
      },
    },
    {
      type: "list",
      name: "language",
      message: "请选择语言",
      choices: LANGUAGE.map((lan) => ({ name: lan, value: lan })),
    },
    {
      type: "input",
      name: "author",
      message: "请输入作者",
    },
  ]);
  await searchRepoByParams(answers);
}

async function searchRepoByParams({ repoName, language, author }) {
  const { token, platform } = fs.readJsonSync(TEMPFILEPATH);
  const api = platform === GITHUB ? new githubApi(token) : new giteeApi();
  const params =
    platform === GITHUB
      ? {
          q: author
            ? `repo:${author}/${repoName}`
            : `${repoName}+language:${language}`,
          per_page: 30,
          page: 1,
        }
      : {
          q: repoName,
          owner: author ? author : undefined,
          language: language ? language : undefined,
          per_page: 30,
          page: 1,
          access_token: token,
        };
  const result = await wrapperLoading(
    api.searchRepositories.bind(api, params),
    {
      loadingInfo: "搜索中......",
    }
  );
  if (
    (result?.total_count === 0 && platform === GITHUB) ||
    (result?.length === 0 && platform === GITEE) ||
    !result
  ) {
    console.log(chalk.red("搜索结果为空,请检查搜索条件是否有误后重新输入"));
    await searchRepos();
    return;
  }
  console.log(
    `共${chalk.green(result?.total_count || result?.length)}条搜索结果`
  );
  const data = platform === GITHUB ? result?.items : result;
  const choicesRepo = data.map((item) => {
    return {
      name: `${chalk.green(item.full_name)}(${item.description})`,
      value: item.full_name,
    };
  });
  // 选择仓库
  const { full_name } = await getAnswers([
    {
      type: "list",
      name: "full_name",
      message: "请选择仓库",
      choices: choicesRepo,
    },
  ]);
  repoFileName = full_name?.split("/")[1];
  const tagResult = await wrapperLoading(api.searchTags.bind(api, full_name), {
    loadingInfo: "搜索中......"
  });
  // 处理tag为空的情况
  if (tagResult?.length) {
    const tagChoices = tagResult?.map((item) => {
      return {
        name: `${chalk.green(item.name)}`,
        value: item.name,
      };
    });
    const { tag } = await getAnswers([
      {
        type: "list",
        name: "tag",
        message: "请选择tag",
        choices: tagChoices,
      },
    ]);
    downLoadCommand = `git clone --branch ${tag} git@${platform}.com:${full_name}.git`;
  } else {
    downLoadCommand = `git clone git@${platform}.com:${full_name}.git`;
  }
}
downLoadRepo
async function downLoadRepo() {
  if (checkFileIsExist(path.join(process.cwd(), repoFileName))) {
    const { confirm } = await getAnswers([
      {
        type: "confirm",
        name: "confirm",
        message: "当前执行目录下已存在该项目,是否强制更新?",
      },
    ]);
    if (confirm) {
      execaCommand(true)
    } else {
      shell.exit(1);
    }
  } else {
    execaCommand(false)
  }
}

async function execaCommand(rm) {
  if (rm) shell.rm("-rf", repoFileName);
  const res = shell.exec(downLoadCommand);
  if (res?.code === 0) {
    shell.echo(chalk.green("下载成功 √√√"));
  }
}

不足

仓库目前默认搜索30条,不支持分页。可能还有一些别的问题,后续再完善吧(等个有缘人😀)

最后

项目地址,如果该文章对你有一点帮助的话,可以帮忙点个star⭐️。thanks💕


潘潘潘呀
1 声望0 粉丝