實現自定義腳手架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(二)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM