为什么要搭建cli
在前端开发中,在搭建好一套自己满意的代码架子后,希望以后可以在别的开发中也一直沿用。之前或许把这份代码的结构再手动码一份,但这样的重复劳动其实搭建好自己的cli后,就可以通过几个命令来完成了
必备模块
"dependencies": {
"axios": "^0.19.2",
"chalk": "^4.1.0",
"commander": "^6.0.0",
"download-git-repo": "^3.0.2",
"fs-extra": "^9.0.1",
"inquirer": "^7.3.3",
"ora": "^5.0.0"
}
步骤
-
npm link
使用npm link将yh-cli链接到全局
-
commander
安装及使用
// 安装
npm i commander
// 使用
const program = require('commander');
program.parse(process.argv);
命令配置
program
.command(create'') //配置命令的名字
.alias('c') // 配置命令的参数
.description('TyrionJYQ personal CLI') // 命令描述
.action(() => {
console.log('TyrionJYQ') // 命令动作
})
在创建多个命令时,可以先将需要设置的命令用对象来描述,然后遍历对象,调用program生成命令
module.exports = {
create: {
alias: 'c',
description: 'create a new project',
examples: [
'tj-cli create <project-name>',
],
},
config: {
alias: 'conf',
description: 'config project varible',
examples: [
'tj-cli config set <key> <value>',
'tj-cli config get <key>',
],
},
'*': {
alias: '',
description: 'command not found',
examples: [],
},
}
-
下载模板
使用axios下载模板
// 通过axios来获取结果
const axios = require('axios');
axios.interceptors.response.use(res => res.data);
async function fetchRepoList () {
return axios.get('组织仓库地址');
}
async function fetchTagList (repo) {
return axios.get(`版本号地址`);
}
module.exports = {
fetchRepoList,
fetchTagList
}
加载动画
使用ora显示加载动画
安装
npm install ora
使用
const spinner = ora('download...')
spinner.start() //开启动画
spinner.succeed() // 关闭动画
问询(交互)
安装
npm install inquirer
使用
const { repo } = await Inquirer({
name: 'repo',
type: 'list',
message: 'choice a repo',
choices: repos
})
问询是一个等待用户选择,所以这是一个异步事件
实现效果

实现效果
目录结构

目录结构
代码实现
- yh
#! /usr/bin/env node
// 创建可执行命令
const program = require('commander');
// 修改命令在控制台颜色
const chalk = require('chalk');
// 提取cmd中的属性
const cleanArgs = cmd => {
const args = {};
cmd.options.forEach(o => {
const key = o.long.slice(2);
cmd[key] && (args[key] = cmd[key]);
})
return args;
}
// 创建项目
program
.command('create <app-name>')
.description('create a new project')
.option('-f, --force', 'overwrite target directory if it exists')
.action((name, cmd) => {
// 调用create模块去创建
require('../lib/create')(name, cleanArgs(cmd));
})
// 监听 "--help命令输入"
program.on('--help', function () {
console.log();
console.log(`Run ${chalk.cyan(`yahang-cli <command> --help`)} show details`)
console.log();
})
program.parse(process.argv);
- create.js
const path = require('path');
// node 自带的 fs并不能返回promise 所以此处使用 fs-extrafs-extra
const fs = require('fs-extra');
// 创建交互式命令
const Inquirer = require('inquirer');
const Creator = require('./Ctreator');
// 创建项目
module.exports = async function (projectName, options) {
// 获取当前命令执行时的工作目录
const cwd = process.cwd();
// 目标目录
const targetDir = path.join(cwd, projectName);
if (fs.existsSync(targetDir)) {
// 如果强制创建 ,删除已有的
if (options.force) await fs.remove(targetDir);
else {
// 提示用户是否确定要覆盖
let { action } = await Inquirer.prompt([ // 配置询问的方式
{
name: 'action',
type: 'list', // 类型非常丰富
message: `Target directory already exists Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Cancel', value: false }
]
}
]);
if (!action) return
else if (action === 'overwrite') {
console.log(`\r\nRemoving....`);
await fs.remove(targetDir)
}
}
}
// 创建项目
const creator = new Creator(projectName, targetDir);
creator.create(); // 开始创建项目
}
- Ctreator.js
const { fetchRepoList, fetchTagList } = require("./request");
const Inquirer = require('inquirer');
const { wrapLoading } = require('./util');
const downloadGitRepo = require('download-git-repo'); // 不支持promise
const util = require('util');
const path = require('path');
class Creator {
constructor(projectName, targetDir) {
this.name = projectName;
this.target = targetDir;
// 此时这个方法就是一个promise方法了
this.downloadGitRepo = util.promisify(downloadGitRepo);
}
async fetchRepo () {
// 失败重新拉取
let repos = await wrapLoading(fetchRepoList, 'waiting fetch template');
if (!repos) return;
repos = repos.map(item => item.name);
let { repo } = await Inquirer.prompt({
name: 'repo',
type: 'list',
choices: repos,
message: 'please choose a template to create project'
});
return repo
}
async fetchTag (repo) {
let tags = await wrapLoading(fetchTagList, 'waiting fetch tag', repo);
if (!tags) return;
tags = tags.map(item => item.name);
let { tag } = await Inquirer.prompt({
name: 'tag',
type: 'list',
choices: tags,
message: 'please choose a tag to create project'
});
return tag;
}
async download (repo, tag) {
// 1.需要拼接处下载路径来
let requestUrl = `项目名/${repo}${tag ? '#' + tag : ''}`
// 2.把资源下载到某个路径上
await wrapLoading(this.downloadGitRepo, 'waiting donwload', requestUrl, path.resolve(process.cwd(), `${repo}@${tag}`));
return this.target;
}
async create () {
// 真实开始创建了
// 1) 先去拉取当前组织下的模板
let repo = await this.fetchRepo();
// 2) 在通过模板找到版本号
let tag = await this.fetchTag(repo);
// 3) 下载
await this.download(repo, tag);
}
}
module.exports = Creator;
- request.js
// 通过axios来获取结果
const axios = require('axios');
axios.interceptors.response.use(res => res.data);
async function fetchRepoList () {
return axios.get('组织仓库地址');
}
async function fetchTagList (repo) {
return axios.get(`代码地址`);
}
module.exports = {
fetchRepoList,
fetchTagList
}
- util.js
// 命令行加载
const ora = require('ora');
async function sleep (n) {
return new Promise(resolve => setTimeout(resolve, n));
}
// 制作了一个等待的loading
async function wrapLoading (fn, message, ...args) {
const spinner = ora(message);
//开启加载
spinner.start();
try {
let repos = await fn(...args);
spinner.succeed();
return repos;
} catch (e) {
spinner.fail('request failed , refetch...');
await sleep(1000);
return wrapLoading(fn, message, ...args);
}
}
module.exports = {
sleep,
wrapLoading
}
- package.json
{
"author": "yh",
"bin": {
"yh": "./bin/yh",
"yahang": "./bin/yh"
},
"dependencies": {
"axios": "^0.19.2",
"chalk": "^4.1.0",
"commander": "^6.0.0",
"download-git-repo": "^3.0.2",
"fs-extra": "^9.0.1",
"inquirer": "^7.3.3",
"ora": "^5.0.0"
},
"keywords": [],
"license": "ISC",
"main": "index.js",
"name": "yahang-cli",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"version": "1.0.0"
}
运行
# yh create xxx