前言
最近在看脚手架的一些源码实现,想着自己做个简易版的脚手架。但是一直没有出发点,搁置了很久。后面在公司拉取代码的时候想到,我们有时候会去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类似
项目预览
技术前瞻
- 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!'));
长这样
commander
基本配置
program
.name('dl-repo-cli')
.description('一个在终端运行的源码下载器cli,支持github、gitee')
.version('1.0.0')
program.parse()
长这样
接下来配置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可以对选项进行更详尽的配置
配置完长这样
当我们使用-p参数时只能为github或gitee
,输入其他时会提示,就像这样
最后可以通过action获取到options参数并书写后续相关逻辑,如下
另外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。
核心
- 检查并更新缓存文件
- 输入仓库名称、语言等并搜索
- clone 下载
对应代码中这三个函数 👇
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💕
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。