实现自定义脚手架mfy-cli(一)
写在前面: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(二)