实现自定义脚手架mfy-cli(一)

写在前面:2020年已经到了第四个季度了,时间真的过的稍微有点快呀~ 

目录链接

实现自定义脚手架mfy-cli(一) 实现自定义脚手架mfy-cli(二)

进入正题:

     搭建自己的一个cli,首先是要知道自己想要实现什么功能,简称需求分析(入坑太深),然后在确定子功能模块下的交互形式,简言之就是先实现思路,在进行搭建

功能介绍:

  • 创建项目
  • 配置项目模版
  • 添加文件、创建文件模版
  • 删除文件 

 

npm 包地址 github地址

npm install mfy-cli //安装即可~

本节主要内容 基本文件配置+创建项目 

 前置介绍

需求分析

对于用户而言操作

对于cli本身操作流程而言

开章介绍

  这里主要是介绍在构建自己的cli项目的时候所使用的第三方插件,后续就不会介绍了,构建cli中,基本都是这些包给予我们很大的帮助,基本上所有的交互都在这些包里面了

 commander:解析用户输入的命令

 

 当用户执行mfy-cli create projectName 就会进入到action中

 inquirer 命令行交互的功能

  • 提供给我们单选项目
 const Inquirer = require('inquirer') 
 //Inquirer 为异步函数 需要await进行等待操作处理结果
 let {repo}= await Inquirer.prompt({
     name:'repo',
     type:'list',
     choices:repos,
     message:"please choose a template to create project"
   })

  •  多选项目
let {action}  =  await inquirer.prompt([
  {
    name:'action',
    type:'checkbox',//类型比较丰富
    message:"Target directory already exits,please select new action",
    choices:[
     {name:'Overwrite',value:'overwrite'},
     {name:'Cancel',value:false,}, 
    ]
  }, 
])

 axios 获取git上的请求数据信息,具体的方法和日常使用基本一样

fs-extra 封装了node的内置fs模块,增加了一些函数

 ora  等待loading标志

//创建loading
  const spiner = ora(message+' ---'+ args?args:'');
  spiner.start(); //开启加载 
  spiner.succeed(); //
  spiner.fail("request failed , refetching...",args)

chalk 文字颜色输出控制

console.log(`Run ${chalk.red(`mfy-cli <command> --help`)} show details`)

开始进入脚手架编写环节

基本配置准备

项目目录 ,目录是经过几番折腾,改来改去才弄的,不一定是最好的分配方式,这里提出仅仅是为了后面介绍更加方便

配置脚手架名称 在package.json中进行

{
  "name": "mfy-cli", //脚手架名称
  "version": "2.0.3", //当前包的版本
  "description": "mfy-cli ", 
  "bin": "./bin/mfy", //入口文件
  "gitOwner": "mfy-template", //配置的的git模版
  "defaultOwner": "mfy-template",//默认的git模版 "keywords": [
    "cli",
    "mfy-cli"
  ],
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  } "devDependencies": {}
}

创建可执行文件

我们的可执行文件放置在bin的目录的mfy文件中,

此时我们要配置当前脚手架的执行环境,因此需要在mfy文件的头部添加上

#!/usr/bin/env node

该信息必须在头部,不能在其顶部添加任何其他的信息,否则会导致报错,

将我们的cli链接到全局 以便全局可以使用

  • 进入mfy-cli 目录
  • 执行npm link
  • 链接成功后就可以在全局进行访问

配置基本命令

配置基本命令时候,其实先思考下我们都想要什么命令,怎样输入等,此时我们借助vue-cli的命令行交互方式,将我们的自己的脚手架的创建项目命令定义为以下几个;

首先配置mfy-cli的基础选项 比如当我们输入mfy-cli help的时候能够提示我们一些信息;

  ./bin/mfy

配置基本命令

#!/usr/bin/env node
 

const { chalk, program } = require('../libs/tools/module')
const packageData = require('../package.json')
const log = console.log; 
//当前cli的名称 
const cliName = packageData.name; console.log(chalk.yellowBright.bold(`🌟---------------------------------------🌟\n    👏 welcome to use ${cliName}👏    \n🌟---------------------------------------🌟`));

//credit-cli 的版本信息
program
  .version(`${cliName}@${packageData.version}`)
  .usage('<command> [option]')

//在 --help 的时候进行调整
program.on('--help', () => {
  log(`Run ${chalk.red(`${cliName} <command> --help`)} show details`)
})
//解析用户执行命令时候传入的参数 根据参数进行配置 
program.parse(process.argv)
if (!program.args.length) {
  program.help()
}

解析命令

配置完基本命令后,在界面中输入命令

  • 用户输入mfy-cli create projectName
  • 此时获取输入的projectName和后缀参数等信息
//创建create命令 并进行操作  f
program.command('create <app-name>')
  .description("create a new project")
  .option("-f,--force", 'overwrite target if it exists')
  .action((name, cmd) => {
//我们输入的name = app-name
// cmd中包含了一些参数信息 比如输入的-f、--force等
if (!name) { log(chalk.red("please write project name")) return; } require('../libs/command/create.js')(name, clearArgs(cmd)) })

用户输入的结果展示 从中提取我们需要的信息

Command {
  commands: [],
  options:
   [ Option {
       flags: '-f,--force',
       required: false,
       optional: false,
       bool: true,
       short: '-f',//短操作
       long: '--force', //命令行书输入的命令
       description: 'overwrite target if it exists' } ],
  _execs: {},
  .....
  _events: [Object: null prototype] { 'option:force': [Function] },
  _eventsCount: 1 }

参数的提取方法其实也非常简单

/**
 * 参数的格式化插件
 * @param cmd 当前命令行中的命令数据 
 */
const clearArgs = (cmd) => {
  const args = {};
  cmd.options.forEach(o => {
    const key = o.long.slice(2)
    //如果当前命令通过key能取到这个值,则存在这个值
    if (cmd[key])
      args[key] = cmd[key];
  });
  return args;//{force:true}的格式
}  

进行前置验证

当我们创建项目的时候,需要我们自己进行交互设置如,整个基本流程如下;

 校验:判断当前的文件夹下是否包含同名的文件夹,采用fse.existsSync(targetDir)判断,如果存在,则进行提示,借助inquirer进行交互的选项

const path =require('path')
const fs =require('fs-extra')
const inquirer = require('inquirer');
const chalk =require('chalk')
const Creator = require('./Creator')
module.exports = async function(projectName,options){ 

  //获取当前命令执行时候的工作目录 
  const cwd = process.cwd() ;

  //获取当前target的目录
  const targetDir = path.join(cwd,projectName)

  //1.首先判断当前文件下是否存在当前操作的项目目录名字 
  
  //后续持续优化 大小写问题
  
  if(fs.existsSync(targetDir)){
  
    //如果命令中存在强制安装,则删除已经存在的目录
    if(options && options.force){
       await fs.remove(targetDir);
    }else{
    
      //配置询问的方式 让用户选择是重写还是取消当前的操作
     let {action}  =  await inquirer.prompt([
        {
          name:'action',
          type:'list',//类型比较丰富
          message:"Target directory already exits,please select new action",
          choices:[
           {name:'Overwrite',value:'overwrite'},
           {name :'Cancel',value:false,}, 
          ]
        }, 
      ])
     if(!action) {
       return
     }else if(action =='overwrite'){
       console.log(chalk.green(`\r\Removing.....`))
       await fs.remove(targetDir);
       console.log(chalk.green(`\r 删除成功`))
     }
    }
  }
  //创建新的 inquirer 选择功能
  const creator = new Creator(projectName,targetDir)
  //创建项目
  creator.create();  
}

进入重要的创建项目环节 

 

  1. 拉取当前组织下的模版
  2. 通过模版找到版本号信息
  3. 下载当前的模版
  4. 下载安装依赖信息
拉取当前组织下的模版
Creator.js中构造函数架子
const {fetchRepoList,fetchTagList} = require('./request.js') 
 const Inquirer = require('inquirer') 
 const { wrapLoading} = require('./util')
 const downloadGitRepo = require('download-git-repo')
 //downloadGitRepo 为普通方法,不支持promise
const util = require('util');
const path = require('path'
class  Creator{
  constructor(projectName,targetDir) { 
    //new 的时候会调用构造函数  
    this.name = projectName;
    this.target=targetDir
    this.downloadGit= util.promisify(downloadGitRepo)
  }
  //真实开始创建了
  async create(){
    console.log(this.name,this.target)
    //采用远程拉取的方式  github的api 
    // 1.先去拉去当前组织下的模版
    let repo = await this.fetchRepo();

    // 2.通过模版找到版本号
    let tag = await this.fetchTag(repo);
     
    // 3.下载当前的模版 依靠api
    await this.download(repo,tag) 
  } 
}
module.exports = Creator
1.获取当前的模版内容

放在Creator.js

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

请求repo 可参考官网

异步获取

async function fetchRepoList(){
  //可以通过配置文件拉取不同的仓库对应下载的文件
   let result = await axios.get('https://api.github.com/orgs/yourName/repos')
   return result;
}

命令行中就会出现了该选择了,选择其中的某一个项目进行下载

2.获取选择模版的tag的信息 
 async fetchTag(repo){
    let tags =await wrapLoading(fetchTagList,'waiting fetch tagList',repo)
    if(!tags) return  
    //仍然是获取tag的名称
    tags = tags.map(item=>item.name);  //[2.1,2.3,3.0]
    let {tag}= await Inquirer.prompt({
      name:'tag',
      type:'list',
      choices:tags,
      message:"please choose a tags to create project"
    }) 
    return tag;
  }
//获取当前的模版tag的信息 repo是我们选择的模版的名称 async
function fetchTagList(repo){ //可以通过配置文件拉取不同的仓库对应下载的文件 console.log(repo) if(!repo) return ; let result = await axios.get(`https://api.github.com/repos/yourName/${repo}/tags`) return result; }

选择当前的版本信息

 

⚠️ 在github上拉取代码有的时候可能网络请求失败、拉取失败的情况,因此需要进行间断自动请求,这个过程为了更好的交互使用了ora的loading.

wrapLoading 是一个启动显示在项目中loading的内容 放置在util.js中

//引入可以loading的插件
const ora = require('ora')

//请求失败的时候进行睡眠在请求
 async function sleep(n){
   var timer = null;
    return new Promise((resolve,reject)=>{
      timer= setTimeout(() => {
       //执行请求
        resolve(); 
        clearTimeout(timer)
      }, n);
    })
 }
//页面的loading效果
async function wrapLoading(fn,message,args){
//开始展示loading
  const spiner = ora(message);
  spiner.start(); //开启加载 
  //需要进行捕获异常操作,存在首次获取失败情况
  try{
    let repos = await fn(args);
    spiner.succeed();
    return repos;
  }catch(e){
    spiner.fail("request failed , refetching...",args)
    // 等待1s再去请求
    await sleep(1000)
    //重复执行这个请求
    return wrapLoading(fn,message,args) 
  }  
}
module.exports={
  sleep,
  wrapLoading
}
3.开始下载我们的路径

此时我们借助一个git-download-repo的插件包,将路径直接拼接上进行操作即可

  async download(repo,tag){
    console.log(`----begin to download---`)
   //1.先拼接出下载路径
   let requestUrl = `yourName/${repo}${tag?'#'+tag:''}`
   //2.把路径资源下载到某个路径上(后续可以增加缓存功能) 
    //应该下载下载到系统目录中,后续可以使用ejs handlerbar 进行渲染,最后生成结果并写入`${repo}@${tag}`
   let result =  await wrapLoading (()=>{this.downloadGit(requestUrl,path.resolve(process.cwd(),this.target))},'waiting download...');
   return result;
  }

先拉当前的模版代码,后续会进行下载安装依赖模版 

自动下载npm包的时候,需要借助node的exec的执行模块

  async downloadNodeModules(downLoadUrl) {
    let that = this;
    log.success('\n √ Generation completed!')
    const execProcess = `cd ${downLoadUrl} && npm install`;
    loading.show("Downloading node_modules")
    //执行安装node_modules的以来
    exec(execProcess, function (error, stdout, stderr) {
      //如果下载不成功 则提示进入目录重新安装
      if (error) {
        loading.fail(error)
        log.warning(`\rplease enter file《 ${that.name} 》 to install dependencies`)
        log.success(`\n cd ${that.name} \n npm install \n`)
        process.exit()
      } else {
        //如果成功则直接提示进入目录 执行即可
        log.success(`\n cd ${that.name} \n npm run server \n`)
      }
      process.exit()
    });
    return true;
  }

 

 

 下载也完成啦~ 

接2呀~ 

主要用于创建文件/文件夹、删除文件、配置自定义的模版下载路径等(期待 搓手手)

总结

简单完成了下载功能,除此之外,还缺少对模版文件进行配置功能,比如项目中的

  • package中的依赖模块选择
  • 自动下载依赖安装模块
  • 根据用户选择定制可视化下载目录结构

 

 

 

 

  

 

进入下一篇实现自定义脚手架mfy-cli(二)

posted @ 2020-10-23 19:45  MFYNGUFD  阅读(1789)  评论(0编辑  收藏  举报