前端如何从0构建自己的cli脚手架

问题描述

前端开发的过程中通常不可避免的会使用到脚手架来构建项目,其中最常见的就是vue-cli、create-react-app、angular-cli 等等,脚手架工具的使用可以大幅度的提升项目的构建速度,通过简单的命令行的交互,可以快速的集成依赖,完成项目结构的创建。

准备工作

在开始构建脚手架前首先需要知道脚手架具备的最基本的功能

  1. 可以通过命令行交互询问用户问题
  2. 根据用户回答的结果生成文件

比如说我们在使用vue-cli 创建项目的时候在终端键入命令👇

vue create my_project

这里会有询问用户问题的操作

在这里插入图片描述
在这里插入图片描述

选择了需要的依赖后就会去集成依赖生成相应的目录结构

创建自己的脚手架

知道了脚手架的运行流程后我们来根据流程一步一步来创建自己的脚手架,此次我们要做的只是利用脚手架去拉取固定的模板,自定义创建依赖模板的流程留到后面再讲。

1、首先创建一个文件夹生成package.json文件

mkdir yd_cli_vue

cd yd_cli_vue/

npm init

2、创建文件夹 “bin” 以及文件夹 “lib” ,“bin”文件夹用来存放程序的入口文件,“lib”文件夹用来存放一些工具函数等文件。文件夹结构如下 👇

yd_cli_vue      
├─ bin
├─ lib        
└─ package.json  

3、在文件夹“bin”下创建程序的入口文件“cli.js”,文件目录结构如下👇

yd_cli_vue      
├─ bin
│  ├─ cli.js   
├─ lib        
└─ package.json  

4、在package.json里面指定程序的入口文件为bin下面的cli.js👇

{
  "name": "yd_cli_vue",
  "version": "1.0.0",
  "description": "simple_cli",
  "main": "index.js",
  "bin": {
    "yd": "./bin/cli.js"  // 手动添加入口文件为 cli.js yd为程序的快速启动命令
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "youngdan",
  "license": "ISC"
}

5、在入口文件cli.js中进行编辑。

#! /usr/bin/env node

console.log('hello cli')

在上面的代码中,我们可以看到第一行出现了比较奇怪的命令,那么这句命令是什么意思呢,首先我们先来认识一下 #!这个符号,这个符号在linux或者unix中叫做shebang,在unix类的操作系统里,文件开头如果带有 #!的话,那么这个文件就会被当做一个执行文件来执行,执行文件也即是可以由操作系统进行加载执行的文件。如果带有这个符号,说明这个文件可以当做脚本来运行。
那么后面的 /usr/bin/env node 是什么意思呢,他的意思是这个文件要用node来执行,但是node是怎么来的呢?所以会去用户的安装根目录下的env环境变量里面去寻找node(/usr/bin/env node)然后用node 来执行整个的脚本文件。

6、检查入口文件是否可以正常的执行,在终端键入以下命令

npm link// npm 链接到全局

当出现以下情况的时候说明执行成功了
在这里插入图片描述

此时我们输入程序的快速启动命令 yd (在package.json里面进行配置的),可以看到入口文件成功执行了

在这里插入图片描述

入口文件执行成功了,我们就可以正式开始脚手架的构建了

完成脚手架最基本的功能

首先在构建脚手架前,需要引入一些脚手架构建中必须用到的工具

commander
commander 可以自定义一些命令行指令,在输入自定义的命令行的时候,会去执行相应的操作

inquirer
inquirer 可以在命令行询问用户问题,并且可以记录用户回答选择的结果

fs-extra
fs-extra 是fs的一个扩展,提供了非常多的便利API,并且继承了fs所有方法和为fs方法添加了promise的支持。

chalk
chalk 可以美化终端的输出

figlet
figlet 可以在终端输出logo

ora
ora 控制台的loading样式

download-git-repo
download-git-repo 下载远程模板

利用commander自定义命令行指令

在入口文件中引入下载的commander

const program = require('commander')

program
.version('0.1.0')
.command('create <project_name>')
.description('create a new project')
.action(res => { 
    // 打印命令行输入的值
    console.log(res)
})

program.parse()

执行程序快捷运行命令 yd

在这里插入图片描述

可以看到出现了cli使用的命令信息,其中有一条命令是 create <project_name> 现在我们试着输入一下这个命令,可以看到出现了以下的信息,也就是说,程序拿到了我们输入的项目的名称,那么之后就可以用我们输入的名称作为我们要创建的项目的名字。

在这里插入图片描述
我们经常可以看到别人的npm包,输入了命令后终端上会出现彩色的字体,以及logo。是怎么做的呢,其实就是利用了chalk和figlet,示例代码👇🏻

program
  // 监听 --help 执行
  .on('--help', () => {
    console.log('\r\n' + figlet.textSync('YangDan', {
      font: 'Ghost',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 100,
      whitespaceBreak: true
    }));
    // 新增说明信息
    console.log(`\r\nRun ${chalk.cyan(`yd <command> --help`)} for detailed usage of given command\r\n`)
  })

我们再次输入脚手架快速启动命令可以看到出现了logo和彩色的字体

在这里插入图片描述

询问用户是否需要强行覆盖已有文件

首先我们在项目的根目录下找到lib文件夹,在lib文件夹的下面创建 create.js文件,这个文件的作用就是用来创建项目文件的,我们在create.js 文件下面键入以下代码👇🏻

// lib/create.js

module.exports = async function (name, options) {
  console.log('项目名称以及配置项:', name, options)
}

然后在cli.js中引入 create.js 将 cli.js 中的部分代码修改为👇🏻

program
  .command('create <project_name>')
  .description('create a new project')
  .option('-f, --force', 'overwrite target directory if it exist') // 文件夹存在的时候强制创建文件夹
  .action((name, options) => {
    // 在 create.js 中执行创建任务
    require('../lib/create.js')(name, options)
  })

接着我们来执行和一下我们的脚手架的命令,看一下create.js有没有成功的引入成功,以及在create.js里面能不能成功的拿到输入的项目的名称以及options。👇🏻

在这里插入图片描述

可以看到成功的打印出了create.js以及配置项,其中的配置项-f的意思是,当我创建一个文件夹的时候,这个文件夹的名字可能已经存在了,这个时候我们是否需要强行覆盖掉之前的文件夹,创建一个新的文件夹。

然后我们需要完善一下create.js的逻辑

// lib/create.js

const path = require('path')
const fs = require('fs-extra')

module.exports = async function (name, options) {
  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetCwd  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetCwd)) {

    // 是否为强制创建?
    if (options.force) {
      console.log('进入了强制创建')
    } else {
      console.log('需要询问用户是否强行创建')
      // 询问用户是否需要强行创建文件夹
    }
  } else {
    console.log('目录尚不存在,去创建');
  }
}

接着我们执行一下这块的逻辑,分别看在存在目录,和不存在目录的情况下分别会输出什么

不存在的时候
在这里插入图片描述

当存在目录的时候,但是在创建的时候不输入 -f (强制覆盖)
在这里插入图片描述

当存在目录的时候,但是在创建的时候直接输入 -f (强制覆盖)
在这里插入图片描述

可以看到,确实是可以在我们输入不同的命令的情况下,根据有没有已经创建的目录,进入不同的逻辑里面。那么我们就可以分别根据三种不同的情况,来做不同的处理。

如果说想创建一些自定义的指令,根据自定义的指令来做不同的处理,那么也可以仿照强行覆盖的这个逻辑来进行自定义,此处就不再赘述。

询问用户拿到用户的创建信息

要做到终端和用户进行交互,并且拿到用户选择的信息,那么就需要前面介绍过的工具 inquirer 来帮助我们,inquirer 可以在命令行询问用户问题,并且可以记录用户回答选择的结果。便于我们拿到终端与用户的交互信息。

npm install inquirer

接下来我们就需要解决询问用户是否覆盖已经存在的文件夹,然后用户选择模板,选择版本,进行模板的下载

首先,如果目录已经存在,就看用户输入的指令里面存不存在 -f 或者 --force ,假如有的话,那么直接覆盖掉之前存在的目录,然后创建。假如没有,那么久需要询问用户是否需要强行覆盖掉原本存在的文件,用户选择是的情况下覆盖,用户选择否的情况下不覆盖。 如果用户不存在,创建的时候,直接走新建的逻辑。根据这个逻辑,我们可以完善create.js的逻辑如下。👇🏻

// lib/create.js

const path = require('path')
const fs = require('fs-extra')

module.exports = async function (name, options) {
  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetCwd  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetCwd)) {
    // 是否为强制创建?
    if (options.force) {
      console.log('进入了强制创建')
      //移除掉原本存在的文件
      await fs.remove(targetCwd)
    } else {
      console.log('需要询问用户是否强行创建')
      // 询问用户是否需要强行创建文件夹
      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(`移除`)
        await fs.remove(targetCwd)
      }
    }
  }
}

我们来测试一下,当已经存在文件的情况下,输入指令创建一个同名的文件夹,可以拿到这样的输出结果

在这里插入图片描述

当我选择了覆盖的时候,可以看到👇🏻,这个时候再去看之前的同名文件夹,已经被删除掉了,既然文件已经被删除掉了,接下来的事情自然而然就是去创建文件,并且拉取模板。

在这里插入图片描述

创建文件

首先在lib目录下创建factory.js文件,创建好了后,目前为止的文件的目录结构是👇🏻

yd_cli_vue      
├─ bin
│  ├─ cli.js   
├─ lib
│  ├─ create.js
│  ├─ factory.js 
└─ package.json 

在 factory.js 中,主要负责的东西就是创建目录,以及拉取模板进行下载,我们在文件里面创建 factory类来处理项目的创建逻辑。👇🏻

// lib/factory.js

class Factory {
  constructor (name, targetCwd){
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetCwd = targetCwd;
    
    console.log(targetCwd)
    console.log(name)
  }

  // 核心创建逻辑
  create(){

  }
}

module.exports = Factory;

然后我们在create.js里面引入 factory.js 并且传入项目的名字以及创建目录👇🏻

const Factory = require("./factory");

module.exports = async function (name, options) {
  ...
  // 创建项目
  const factory = new Factory(name, targetCwd);
  // 开始创建项目
  factory.create()
}

然后我们执行一下脚手架的命令看一下能不能在factory.js里面拿到项目的名字以及创建目录👇🏻

在这里插入图片描述

可以看到已经拿到的创建目录以及,项目的名称

接着来写询问用户选择模版的逻辑,github提供了接口可以获取模板,我事先已经准备好了两个模板发布到了github因此可以直接使用,如果想下载自己的模板的话也可以自己在github上传,然后替换为自己的模板接口就行了。所以下面需要先准备好接口,先在lib下面创建文件http.js专门用来管理接口,创建好后整个目录结构👇🏻

yd_cli_vue      
├─ bin
│  ├─ cli.js   
├─ lib
│  ├─ create.js
│  ├─ factory.js
│  ├─ http.js 
└─ package.json  

在http.js里面配置接口信息

// 通过 axios 处理请求
const axios = require('axios')

axios.interceptors.response.use(res => {
  return res.data;
})

/**
 * 获取模板列表
 * @returns Promise
 */

async function getRepoList() {
  return axios.get('https://api.github.com/orgs/vue3-0-cli-yd/repos')
}

/**
 * 获取版本信息
 * @param {string} repo 模板名称
 * @returns Promise
 */

async function getTagList(repo) {
  return axios.get(`https://api.github.com/repos/vue3-0-cli-yd/${repo}/tags`)
}


module.exports = {
  getRepoList,
  getTagList
}

准备好接口后我们回到factory.js里面,正式开始创建前,我们需要准备一个loading函数,用来展示下载模板时的loading效果,并且将下载模板的方法进行promise化

const ora = require("ora");
const downloadGitRepo = require("download-git-repo"); // 不支持 Promise
const util = require("util");

// 添加加载动画
async function Loading(fn, message, ...args) {
  // 使用 ora 初始化,传入提示信息 message
  const spinner = ora(message);
  // 开始加载动画
  spinner.start();

  try {
    // 执行传入方法 fn
    const result = await fn(...args);
    // 状态为修改为成功
    spinner.succeed();
    return result;
  } catch (error) {
    // 状态为修改为失败
    spinner.fail("Request failed, refetch ...");
  }
}


......

  constructor (name, targetCwd){
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetCwd = targetCwd;
    
    // 对 download-git-repo 进行 promise 化改造
    this.downloadGitRepo = util.promisify(downloadGitRepo);
    
    console.log(targetCwd)
    console.log(name)
  }

获取模板的核心逻辑

async getRepo() {
    // 1)从远程拉取模板数据
    const repoList = await Loading(getRepoList, "waiting fetch template");
    if (!repoList) return;

    // 过滤我们需要的模板名称
    const repos = repoList.map((item) => item.name);

    // 2)用户选择自己新下载的模板名称
    const { repo } = await inquirer.prompt({
      name: "repo",
      type: "list",
      choices: repos,
      message: "Please choose a template to create project",
    });

    // 3)return 用户选择的名称
    return repo;
  }

获取模板的tag列表

async getTag(repo) {
    // 1)基于 repo 结果,远程拉取对应的 tag 列表
    const tags = await Loading(getTagList, "waiting fetch tag", repo);
    if (!tags) return;

    // 过滤我们需要的 tag 名称
    const tagsList = tags.map((item) => item.name);

    // 2)用户选择自己需要下载的 tag
    const { tag } = await inquirer.prompt({
      name: "tag",
      type: "list",
      choices: tagsList,
      message: "Place choose a tag to create project",
    });

    // 3)return 用户选择的 tag
    return tag;
  }

下载远程模板的逻辑

 async download(repo, tag) {
    // 1)拼接下载地址
    const requestUrl = `vue3-0-cli-yd/${repo}${tag ? "#" + tag : ""}`;

    // 2)调用下载方法
    await wrapLoading(
      this.downloadGitRepo, // 远程下载方法
      "waiting download template", // 加载提示信息
      requestUrl, // 参数1: 下载地址
      path.resolve(process.cwd(), this.targetCwd)
    ); // 参数2: 创建位置
  }

创建项目的逻辑

 async create() {
    try {
      // 1)获取模板名称
      const repo = await this.getRepo();

      // 2) 获取 tag 名称
      const tag = await this.getTag(repo);

      // 3)下载模板到模板目录
      await this.download(repo, tag);

      // 4)模板使用提示
      console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`);
      console.log(`\r\n  cd ${chalk.cyan(this.name)}`);
      console.log(`\r\n  npm install`);
      console.log("\r\n  npm run dev\r\n");
    } catch (error) {
      console.log(error);
    }
  }

然后我们将这些函数组装到类里面,完整的factory.js文件如下

const ora = require("ora");
const util = require("util");
const downloadGitRepo = require("download-git-repo"); // 不支持 Promise
const path = require("path");
const chalk = require("chalk");

const { getRepoList, getTagList } = require("./http");
const inquirer = require("inquirer");

// 添加加载动画
async function Loading(fn, message, ...args) {
  // 使用 ora 初始化,传入提示信息 message
  const spinner = ora(message);
  // 开始加载动画
  spinner.start();

  try {
    // 执行传入方法 fn
    const result = await fn(...args);
    // 状态为修改为成功
    spinner.succeed();
    return result;
  } catch (error) {
    // 状态为修改为失败
    spinner.fail("Request failed, refetch ...");
  }
}

class Factory {
  constructor(name, targetCwd) {
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetCwd = targetCwd;

    // 对 download-git-repo 进行 promise 化改造
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }

  // 获取用户选择的模板
  // 1)从远程拉取模板数据
  // 2)用户选择自己新下载的模板名称
  // 3)return 用户选择的名称
  async getRepo() {
    // 1)从远程拉取模板数据
    const repoList = await Loading(getRepoList, "waiting fetch template");
    if (!repoList) return;

    // 过滤我们需要的模板名称
    const repos = repoList.map((item) => item.name);

    // 2)用户选择自己新下载的模板名称
    const { repo } = await inquirer.prompt({
      name: "repo",
      type: "list",
      choices: repos,
      message: "Please choose a template to create project",
    });

    // 3)return 用户选择的名称
    return repo;
  }

  async getTag(repo) {
    // 1)基于 repo 结果,远程拉取对应的 tag 列表
    const tags = await Loading(getTagList, "waiting fetch tag", repo);
    if (!tags) return;

    // 过滤我们需要的 tag 名称
    const tagsList = tags.map((item) => item.name);

    // 2)用户选择自己需要下载的 tag
    const { tag } = await inquirer.prompt({
      name: "tag",
      type: "list",
      choices: tagsList,
      message: "Place choose a tag to create project",
    });

    // 3)return 用户选择的 tag
    return tag;
  }

  // 下载远程模板
  // 1)拼接下载地址
  // 2)调用下载方法
  async download(repo, tag) {
    // 1)拼接下载地址
    const requestUrl = `vue3-0-cli-yd/${repo}${tag ? "#" + tag : ""}`;

    // 2)调用下载方法
    await Loading(
      this.downloadGitRepo, // 远程下载方法
      "waiting download template", // 加载提示信息
      requestUrl, // 参数1: 下载地址
      path.resolve(process.cwd(), this.targetCwd)
    ); // 参数2: 创建位置
  }

  // 核心创建逻辑
  // 1)获取模板名称
  // 2)获取 tag 名称
  // 3)下载模板到模板目录
  async create() {
    try {
      // 1)获取模板名称
      const repo = await this.getRepo();

      // 2) 获取 tag 名称
      const tag = await this.getTag(repo);

      // 3)下载模板到模板目录
      await this.download(repo, tag);

      // 4)模板使用提示
      console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`);
      console.log(`\r\n  cd ${chalk.cyan(this.name)}`);
      console.log(`\r\n  npm install`);
      console.log("\r\n  npm run dev\r\n");
    } catch (error) {
      console.log(error);
    }
  }
}

module.exports = Factory;

测试

接下来,我们使用快捷命令创建一个项目,看一下效果是怎样的👇🏻
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

如此一来,一个最简单的脚手架就搭建成功了

怎么发布到npm

如果你想将这个包发布到npm的话,首先需要有一个npm的账号,然后你需要完善脚手架的package.json的信息👇🏻

{
  "name": "yd_cli_kiko",
  "version": "1.0.0",
  "description": "simple_cli",
  "main": "index.js",
  "bin": {
    "yd": "./bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git@gitee.com:yangdan1028/yd_cli_vue.git"
  },
  "files": [
    "bin",
    "lib"
  ],
  "keywords": [
    "yangdan-cli",
    "yd",
    "脚手架"
  ],
  "author": {
    "name": "kiKo-YD",
    "email": "1292867089@qq.com"
  },
  "license": "ISC",
  "dependencies": {
    "axios": "^0.26.1",
    "chalk": "^4.0.0",
    "commander": "^9.2.0",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.5.0",
    "fs-extra": "^10.1.0",
    "inquirer": "^8.2.2",
    "ora": "^5.4.0",
    "util": "^0.12.4"
  }
}

然后再终端输入 npm publish 发布就好了。注意,发布前如果你的npm镜像是淘宝或非官方npm,需要将npm的镜像设置为官方,才能在发布后在npm官网里面搜到你自己的npm包

npm config set registry https://registry.npmjs.org

在这里插入图片描述

脚手架完整代码

cli.js

#! /usr/bin/env node

// #! 符号的名称叫 Shebang,用于指定脚本的解释程序 Node CLI 应用入口文件必须要有这样的文件头 

const program = require('commander')
const chalk = require('chalk')
const figlet = require('figlet')
program
  // 定义命令和参数
  .command('create <app-name>')
  .description('create a new project')
  // -f or --force 为强制创建,如果创建的目录存在则直接覆盖
  .option('-f, --force', 'overwrite target directory if it exist')
  .action((name, options) => {
    // 打印执行结果
    require('../lib/create.js')(name, options)
  })

program
  // 配置版本号信息
  .version(`v${require('../package.json').version}`)
  .usage('<command> [option]')

program
  // 监听 --help 执行
  .on('--help', () => {
    console.log('\r\n' + figlet.textSync('YangDan', {
      font: 'Ghost',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 100,
      whitespaceBreak: true
    }));
    // 新增说明信息
    console.log(`\r\nRun ${chalk.cyan(`yd <command> --help`)} for detailed usage of given command\r\n`)
  })

// 解析用户执行命令传入参数
program.parse(process.argv);

create.js

const path = require("path");
const fs = require("fs-extra");
const inquirer = require("inquirer");
const Factory = require("./Factory");

module.exports = async function (name, options) {
  // 当前命令行选择的目录
  const cwd = process.cwd();
  // 需要创建的目录地址
  const targetCwd = path.join(cwd, name);
  // 判断在目录的位置是否存在相同的目录了
  // 如果存在相同的目录
  if (fs.existsSync(targetCwd)) {
    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetCwd);
    } else {
      console.log("是否需要覆盖");
      // 询问用户是否确定要覆盖
      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(targetCwd);
      }
    }
  }

  const factory = new Factory(name, targetCwd);
  // 开始创建项目
  factory.create();
};

Factory.js

const ora = require("ora");
const util = require("util");
const downloadGitRepo = require("download-git-repo"); // 不支持 Promise
const path = require("path");
const chalk = require("chalk");

const { getRepoList, getTagList } = require("./http");
const inquirer = require("inquirer");

// 添加加载动画
async function Loading(fn, message, ...args) {
  // 使用 ora 初始化,传入提示信息 message
  const spinner = ora(message);
  // 开始加载动画
  spinner.start();

  try {
    // 执行传入方法 fn
    const result = await fn(...args);
    // 状态为修改为成功
    spinner.succeed();
    return result;
  } catch (error) {
    // 状态为修改为失败
    spinner.fail("Request failed, refetch ...");
  }
}

class Factory {
  constructor(name, targetCwd) {
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetCwd = targetCwd;

    // 对 download-git-repo 进行 promise 化改造
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }

  // 获取用户选择的模板
  // 1)从远程拉取模板数据
  // 2)用户选择自己新下载的模板名称
  // 3)return 用户选择的名称
  async getRepo() {
    // 1)从远程拉取模板数据
    const repoList = await Loading(getRepoList, "waiting fetch template");
    if (!repoList) return;

    // 过滤我们需要的模板名称
    const repos = repoList.map((item) => item.name);

    // 2)用户选择自己新下载的模板名称
    const { repo } = await inquirer.prompt({
      name: "repo",
      type: "list",
      choices: repos,
      message: "Please choose a template to create project",
    });

    // 3)return 用户选择的名称
    return repo;
  }

  async getTag(repo) {
    // 1)基于 repo 结果,远程拉取对应的 tag 列表
    const tags = await Loading(getTagList, "waiting fetch tag", repo);
    if (!tags) return;

    // 过滤我们需要的 tag 名称
    const tagsList = tags.map((item) => item.name);

    // 2)用户选择自己需要下载的 tag
    const { tag } = await inquirer.prompt({
      name: "tag",
      type: "list",
      choices: tagsList,
      message: "Place choose a tag to create project",
    });

    // 3)return 用户选择的 tag
    return tag;
  }

  // 下载远程模板
  // 1)拼接下载地址
  // 2)调用下载方法
  async download(repo, tag) {
    // 1)拼接下载地址
    const requestUrl = `vue3-0-cli-yd/${repo}${tag ? "#" + tag : ""}`;

    // 2)调用下载方法
    await Loading(
      this.downloadGitRepo, // 远程下载方法
      "waiting download template", // 加载提示信息
      requestUrl, // 参数1: 下载地址
      path.resolve(process.cwd(), this.targetCwd)
    ); // 参数2: 创建位置
  }

  // 核心创建逻辑
  // 1)获取模板名称
  // 2)获取 tag 名称
  // 3)下载模板到模板目录
  async create() {
    try {
      // 1)获取模板名称
      const repo = await this.getRepo();

      // 2) 获取 tag 名称
      const tag = await this.getTag(repo);

      // 3)下载模板到模板目录
      await this.download(repo, tag);

      // 4)模板使用提示
      console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`);
      console.log(`\r\n  cd ${chalk.cyan(this.name)}`);
      console.log(`\r\n  npm install`);
      console.log("\r\n  npm run dev\r\n");
    } catch (error) {
      console.log(error);
    }
  }
}

module.exports = Factory;

http.js

// 通过 axios 处理请求
const axios = require('axios')

axios.interceptors.response.use(res => {
  return res.data;
})

/**
 * 获取模板列表
 * @returns Promise
 */

async function getRepoList() {
  return axios.get('https://api.github.com/orgs/vue3-0-cli-yd/repos')
}

/**
 * 获取版本信息
 * @param {string} repo 模板名称
 * @returns Promise
 */

async function getTagList(repo) {
  return axios.get(`https://api.github.com/repos/vue3-0-cli-yd/${repo}/tags`)
}


module.exports = {
  getRepoList,
  getTagList
}

如果需要源码的话可以访问我的:gitee仓库

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值