寫在前面:2020年已經到了第四個季度了,時間真的過的稍微有點快呀~
目錄鏈接
實現自定義腳手架mfy-cli(一) 實現自定義腳手架mfy-cli(二)
進入正題:
搭建自己的一個cli,首先是要知道自己想要實現什么功能,簡稱需求分析(入坑太深),然后在確定子功能模塊下的交互形式,簡言之就是先實現思路,在進行搭建
功能介紹:
- 創建項目
- 配置項目模版
- 添加文件、創建文件模版
- 刪除文件
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(); }
進入重要的創建項目環節
- 拉取當前組織下的模版
- 通過模版找到版本號信息
- 下載當前的模版
- 下載安裝依賴信息
拉取當前組織下的模版
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(二)