~~~~~~来回忆、或者复习下,不知道的同学也可以看看,当然知道的可以跳过回忆贴直接到正式部分啊~~~~~~~~~~~~~~~~~~~~~~
因为代码执行在node环境,所以需要提前安装了node,因为我们是从github上拉取的代码,你还需要知道一些git的知识~~~
在命令行中输入下面:当然是基于安装了node环境的:
#1)全局安装,可以在任意目录下执行vue这条命令npminstall-gvue-cli#2)选择一个工作目录,并且用cmd进入该目录,创建你一个项目名称为first-vue-project的vue项目vuecreatefirst-vue-project#运行vue项目cdfirst-vue-projectnpmrunserve
在按照上边过程安装时我们可以看到,首先我们的脚手架需要可以在命令行能够执行,能够解析用户输入的参数,(比如如果没有安装vue的脚手架直接在命令行输入vue是不能执行的,还有就是能够解析create参数)且在回车时,出现了选择如下图:有default\Manuallyselectfeatures两个可以选择项,选择后回车会出现loading效果、小图标等,安装成功后可以看到图1-3的文件内容,
(图1-1)
(图1-2)
(图1-3)
来下边就开始手撸代码了,创建我们自己的脚手架辣~~按照步骤来:(有浅及深)零基础构建一个CLi工具,~~~全栈架构师必备技能^_^
创建一个自己的空文件夹(lee-cli)来存放我们的项目,首先需要:
在当前目录命令行中按步骤输入下边的命令:
npminit-y#初始化package.jsonnpminstalleslinthusky--save-dev#eslint是负责代码校验工作,husky提供了git钩子功能npxeslint--init#初始化eslint配置文件
├──bin│└──www//全局命令执行的根文件├──src│├──main.js//入口文件│└──utils//存放工具方法||___constants.js//存放用户所需要的常量||___common.js│├──create.js//create命令所有逻辑│├──config.js//config命令所有逻辑│──.huskyrc//githook│──.eslintrc.json//代码规范校├──package.json|__README.md
#!/usr/bin/envnode//此文件是一个可执行文件console.log("这是我创建的一个文件,目录:/bin/www");~~~~~~ps:代码讲解及可能错误总结:
1)#!/usr/bin/envnode->我要用系统中的这个目录/user/bin/env的node环境来执行此文件,且需要注意必须放在文件开头。
"bin":{"lee-cli":"./bin/www"}
没有写上边的代码我们在命令工具是无法执行我们自己定义的命令的:
(图2-3-1)
~~~~~~ps:代码讲解及可能错误总结:
1)package.json中bin:内部命令对应的可执行文件的路径。很多包都有一个或多个可执行的文件希望被放到PATH中。(实际上,就是这个功能让npm可执行的)。上边的代码表示使用在命令工具使用命令lee-cli会调用bin/www文件
npmlink
1)使用npmlink,link将一个任意位置的npm包链接到全局执行环境,从而在任意位置使用命令行都可以直接运行该npm包。npmlink命令通过链接目录和可执行文件,实现npm包命令的全局可执行
2)我们可以看到上边的图2-3-1,有报错,就是因为bin/www中的文件原因是:
所以一定注意#!/usr/bin/envnode在文件的开头不能有空格。
#!/usr/bin/envnodeconsole.log("这是我创建的一个文件,目录:/bin/www");3)在执行npmlink如下报错:
是因为我们在建bin/www文件时建的文件时有扩展名的比如www.js,但是我们在package.json中的代码还是:"bin":{"lee-cli":"./bin/www"}改为"bin":{"lee-cli":"./bin/www.js"}既可以。
bin/www.js文件中引入main.js,www文件中使用main作为入口文件require('../src/main.js');
#!/usr/bin/envnode//此文件是一个可执行文件require('../src/main.js');
使用commander使用commander会自动生成help,解析参数。例如,我们使用vue的脚手架那样,vue-cli--help
1)安装模块
npmicommander2)src/main.js初步内容:
(图2-4-1)
如上图2-4-1所示,执行lee-cli--help已经有提示输出了,而且lee-cli-V是我们在代码中设置的内容。
3)动态获取版本号
第二步的version是我们写死的当前的cli的版本号,我们需要动态获取,并且为了方便我们将常量全部放到util下的constants.js文件中
const{name,version}=require('../../package.json');module.exports={name,version,};
main.js
constprogram=require('commander');const{version}=require('./utils/constants');program.version(version).parse(process.argv);//process.argv就是用户在命令行中传入的参数
process.argv属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数,其中:
4.1.2配置脚手架命令参数
在前言中我们有复习到在用vue-cli创建项目时,使用命令vuecreatefirst-vue-project
这个create参数是固定表示创建项目的,还可以使用其他的指令如vueconfigsetkv
那我们就来实现commander来实现吧!
在utils/common.js中
const{mapActions}=require('./utils/common');//Object.keys()Reflect.ownKeys(mapActions).forEach((action)=>{program.command(action)//配置命令的名字.alias(mapActions[action].alias)//命令的别名.description(mapActions[action].description)//命令对应的描述.action(()=>{//动作if(action==='*'){//访问不到对应的命令就打印找不到命令console.log(mapActions[action].description);}else{console.log(action);//分解命令到文件里有多少文件就有多少配置createconfig//lee-clicreateproject-name->[node,lee-cli,create,project-name]console.log(process.argv);}})});program.version(version).parse(process.argv);//process.argv就是用户在命令行中传入的参数
输出:lee-clicreatemy
我们在看看lee-cli--help
(图2-4-3)
发现已经有了我们配置的createconfig*指令。
2)子命令command,可以使用.command为你的最高层命令指定子命令。在之前的代码我们可以简化一个create代码来看,
program.command('create')//配置命令的名字.alias('c')//命令的别名.description('创建一个项目')//命令对应的描述.action(()=>{console.log('此处为create子命令');})
(图2-4-4)
4.1.3监听--help
监听--help命令,打印帮助信息,在之前配置命令的代码中我们可以看到examples,这个就是在告诉我们这个子指令的用法,我们可以通过--help查看案例。
(图2-4-5)
在main.js中增加代码:
//监听用户的help事件program.on('--help',()=>{console.log('\nExamples:');Reflect.ownKeys(mapActions).forEach((action)=>{mapActions[action].examples.forEach((example)=>{console.log(`${example}`);})})})
(图2-4-6)
4.1.4具体create命令所做的事情
图(2-4-7)
4.1.4.1如何实现create的主要作用呢?
为命令绑定一个操作处理程序(actionhandler),或者将命令单独写成一个可执行文件。
上图(2-4-7)中是main.js配置指令的代码,我们知道当我们输入lee-clicreatemy命令时可以输出信息,动作在action里边,因为我们可以配置很多子命令比如create\config\init...所以我们将具体的动作分发到不同的文件中,通过读取不同的文件名来调用相对于的内容。
在main.js中增加代码
.action(()=>{if(action==='*'){//访问不到对应的命令就打印找不到命令console.log(mapActions[action].description);}else{console.log(action);//分解命令到文件里有多少文件就有多少配置createconfig//lee-clicreateproject-name->[node,lee-cli,create,project-name]console.log(process.argv);require(path.join(__dirname,action))(...process.argv.slice(3));}}
在create.js中增加代码:
module.exports=(projectName)=>{console.log(`此处是文件${projectName}`);}
图(2-4-8)
1)require(path.join(__dirname,action))(...process.argv.slice(3));
在action中我们引入了create.js,并且将我们在命令行中输入的项目名传入到create.js中。process.argv.slice(3)在之前也讲过process.argv返回的是一个数组,从图(2-4-8)也可以看出了。执行lee-clicmy打印出来了此处是文件my
4.1.4.2拉取项目
此时我们就需要将我们git上的项目或者其他云上的项目拉取下来,获取仓库中的所有模板信息,这里就以git为例。
1)安装axio模块
npmiaxios
2)在create.js中增加代码
4.2.1使用inquirer一个用户与命令行交互的工具
1)安装inquirer模块
npmiinquirer
varinquirer=require('inquirer')inquirer.prompt([{type:'confirm',name:'test',message:'你确定使用这个吗',default:true}]).then((answers)=>{console.log('结果为:')console.log(answers)})
执行命令lee-clicmy结果为:
(图2-4-11)
上边的图2-4-11我们可以看到出现了这是一个测试项目(Y/n)我们可以选择Y出现先,这是因为我们的参数type:'confirm',
(图2-4-12)
我们在代码中输出inquirer.prompt()结果是一个promise,上边是用的default,如果我们想选择不同的仓库可以用choice,type:'list',
3)在我们项目中改为:create.js代码增加
const{repo}=awaitinquirer.prompt([{type:'list',name:'repo',message:'请选择一个你要创建的项目',choices:repos}]);console.log(`我现在选择了那个仓库?${repo}`);
(图2-4-13)
上图中我们可以看到有多出现了请选一个你要创建的项目,下边为我们的仓库列表通过上下键选择一个回车选中
(图2-4-14)
1)inquirer.prompt(参数):
{//表示提问的类型,下文会单独解释type:String,//在最后获取到的answers回答对象中,作为当前这个问题的键name:String,//打印出来的问题标题,如果为函数的话message:String|Function,//用户不输入回答时,问题的默认值。或者使用函数来return一个默认值。//假如为函数时,函数第一个参数为当前问题的输入答案。default:String|Number|Array|Function,//给出一个选择的列表,假如是一个函数的话,第一个参数为当前问题的输入答案。//为数组时,数组的每个元素可以为基本类型中的值。choices:Array|Function,//接受用户输入,并且当值合法时,函数返回true。当函数返回false时,//一个默认的错误信息会被提供给用户。validate:Function,//接受用户输入并且将值转化后返回填充入最后的answers对象内。filter:Function,//接受当前用户输入的answers对象,并且通过返回true或者false来决定是否当前的问题应该去问。//也可以是简单类型的值。when:Function|Boolean,//改变渲染list,rawlist,expand或者checkbox时的行数的长度。pageSize:Number,}4.2.2使用ora
npmiora
constspinner=ora('Loading测试中哈哈哈。。。').start();setTimeout(()=>{spinner.color='red';spinner.text='Loadingora哈哈哈';//成功spinner.succeed('拉取成功');},1000);
constora=require('ora');//封装loading效果constfnLoadingByOra=async(fn,message)=>{constspinner=ora(message);spinner.start();letresult=awaitfn();spinner.succeed();//结束loadingreturnresult;}module.exports={mapActions,fnLoadingByOra};
const{fnLoadingByOra}=require('./utils/common');module.exports=async(projectName)=>{letrepos=awaitfnLoadingByOra(fetchReopLists,'正在链接你的仓库...');repos=repos.map((item)=>item.name);//使用inquirer在命令行中可以交互const{repo}=awaitinquirer.prompt([{type:'list',name:'repo',message:'请选择一个你要创建的项目',choices:repos}]);console.log(`我现在选择了那个仓库?${repo}`);}
(图2-4-17)
1)在utils/common.js中增加更改信息
2)在create.js
constinquirer=require('inquirer');const{fnLoadingByOra,fetchReopLists,getTagLists}=require('./utils/common');module.exports=async(projectName)=>{letrepos=awaitfnLoadingByOra(fetchReopLists,'正在链接你的仓库...')();repos=repos.map((item)=>item.name);//使用inquirer在命令行中可以交互const{repo}=awaitinquirer.prompt([{type:'list',name:'repo',message:'请选择一个你要创建的项目',choices:repos}]);lettags=awaitfnLoadingByOra(getTagLists,`正在链接你的选择的仓库${repo}的版本号...`)(repo);tags=tags.map((item)=>item.name);console.log(`我现在选择了那个仓库?${repo}`);console.log(`仓库${repo}的版本信息列表:${tags}`);}
(图2-4-18)
3)增加选择版本信息
在create.js
lettags=awaitfnLoadingByOra(getTagLists,`正在链接你的选择的仓库${repo}的版本号...`)(repo);tags=tags.map((item)=>item.name);const{tag}=awaitinquirer.prompt([{type:'list',name:'tag',message:'请选择一个该项目的版本下载',choices:tags}]);console.log(`我现在选择了那个仓库?${repo}`);console.log(`仓库${repo}的版本信息列表:${tag}`);
(图2-4-19)
(图2-4-20)
npmidownload-git-repo
2)我们需要下载到本地的地址
下载前先找个临时目录来存放下载的文件,来存放,以备后期使用,这样的好处是,如果我们之前下载这个版本的项目可以直接从这个存放的地址拿来(相当于缓存),如果项目中更新我们将临时目录中的文件覆盖也可以。
2.1)在utils/constants.js增加常量
下载临时文件存放地址因为不同的电脑平台临时存放地址不同
constdownloadDirectory=`${process.env[process.platform==='darwin''HOME':'USERPROFILE']}/.myTemplate`;console.log(downloadDirectory);module.exports={name,version,downloadDirectory};
1)process.platform
比如小编使用的电脑:
(图2-4-21)
2.2)获取用户目录
在utils\common.js中增加代码
const{promisify}=require('util');constdownloadGit=require('download-git-repo');downloadGit=promisify(downloadGit);//将项目下载到当前用户的临时文件夹下constdownDir=async(repo,tag)=>{console.log(tag,'downDir方法');letproject=`lxy-cli/${repo}`;//下载的项目if(tag){project+=`#${tag}`;}//c:/users/lee/.myTemplateletdest=`${downloadDirectory}/${repo}`;//把项目下载当对应的目录中console.log(dest,'dest的内容。。。。。。。。。。');console.log(project,'dest的内容。。。。。。。。。。');try{awaitdownloadGit(project,dest);}catch(error){console.log('错误了吗???\n');console.log(error);}returndest;}
在create.js中增加代码并引入
//下载项目到临时文件夹C:\Users\lee\.myTemplateconsttarget=awaitfnLoadingByOra(downDir,'下载项目中...')(repo,tag);
(图2-4-22)
根据图2-4-22我们可以看出来当我们执行lee-clicmy创建项目时,选择了vue-tempalte,选择下载版本v1.0我还我们下载到临时文件夹目录是:下载项目到临时文件夹C:\Users\lee\.myTemplate所以我们可以找到地址:C:\Users\lee\.myTempalte\vue-tempalte打开可以看到下图(图2-4-23):对应我们github地址上的内容是一样的,如图(图2-4-24)
(图2-4-23)
(图2-4-24)
1)promisify(downloadGit);
这句代码我们知道是将downloadGit改为promise,因为download-git-repo不是promise,而我们在项目中都用asyncawait需要我们自己包装为promise。
npmincp
2)在utils\common.js增加方法
在create.js文件中增加代码
awaitcopyTempToLoclhost(target,projectName);
(图2-4-25)
我们跟上边一样的选择命令发现图2-4-25和图2-4-24、图2-4-23内容相同。
因为此情况有很多种形式,我们在此模拟了一个需要用户在命令行选择用户输入内容的特定的复杂模板。在其中一个仓库中存放一个ask.js中放入可以用inquirer模块执行的命令行参数,来询问生成相应的package.json文件,因为里边用到ejs