依赖
- commander:提供
选项
和指令
功能 - inquirer:提供交互式命令行
- download-git-repo:提供github拉取模板的能力
- ora:提供任务加载loading的能力
- chalk:颜色插件,可以修改控制台输出的颜色
- handlebars.js:模板引擎工具
需求1:得到一个能够执行的脚手架
如果想要得到一个可以执行的命令行,在node已有现成方案,即使用:Commander
Commander里已有教程,这里只提供需求所需步骤。
步骤:
**
- 首先进行项目初始化:
****npm init -y**
-
安装
**commander**
: -
**npm install commander**
-
安装成功后,首先修改
**package.json**
,其主要有两个步骤:
- 首先需要将我们要写的脚手架名字写入配置
bin(type:Object)
中【其中key为我们要执行的脚手架名字,value为我们脚手架所对应要执行的js文件】 - 确定脚手架开发模式,即设置
pacekage.json
的type
属性;【选择 esm,需要将type改为module;选择commonjs,则需将type改为commonjs】
**
- 创建脚手架执行文件,其中主要有2个步骤:
**
-
第一步,创建文件并在文件第一行添加固定代码:
**#!/usr/bin/env node**
-
第二步,引入命令行类并实例化,实例化后调用
**parse()**
方法。#!/usr/bin/env node
import { Command } from 'commander' const program = new Command();
program.parse();
**
- 完成上述步骤后,在项目根目录打开终端,执行
**npm link**
指令,将我们脚手架注册到全局中去。【一次链接终生有效,无须该代码后再次**link**
】 - 检验:在控制台中执行自己的脚手架如果没有报错,则代表已成功创建脚手架。
**
至此,本轮目的需求已完成。
需求2:让脚手架支持-{xxx}的选项
上一步已经完成全局执行脚手架了,接下来,本轮的需求目的是让脚手架支持如:
my-cli -p
my-cli -p=8000
my-cli -p 8000
my-cli --port 8000
my-cli --port=8000
等,这一类操作。
首先,官方教程在[
这里
](github.com/tj/commande...%25E3%2580%2582 "https://github.com/tj/commander.js#options)%E3%80%82")**
当前代码:
javascript
#!/usr/bin/env node
import { Command } from 'commander'
const program = new Command();
program.parse();
**option**
方法解读:
参数
作用
参数1: **flag(string)**
第一个选项是我们所注册的选项,其格式为-${}, --${}
,其中前面的是缩写,后变得是完整写法。
参数2: **description(string)**
第二个选项是我们添加的选项的描述,该描述会在使用my-cli --help
的时候展示出来
参数3:defaultValue
选项默认值,一般不填写。
选项的输入能力解读:
commander
提供了三种输入能力:
当使用
< xx >尖括号
定义选项输入内容时必须要输入一个值,否则抛出错误使用
[ xx ]方括号
定义选项输入内容时,该参数就变为可选;当不输入时,则为布尔值(true/false);当输入时则为字符型变量;默认情况下,只能获得选项后第一位参数,如果想要获取多个参数,可以
[ xx... ]
在输入形参处添加...
,此行为代表该参数将为数组类型,将会收集选项后的多条参数(以空格分隔)【ps:如果默认没有添加输入能力,则该选项为布尔值】
步骤:
-
实例化的
program
上有一个option
方法,该方法用于给脚手架注册选项。#!/usr/bin/env node
import { Command } from 'commander' const program = new Command();
program .option('-p, --port ', '端口号') program.parse();
注意⚠️**:** **program.option()**
必须要在 **parse()**
之前调用。
-
针对选项要执行的逻辑处理,有两种方式:
-
自定义监听器
#!/usr/bin/env node
import { Command } from 'commander' const program = new Command();
program.on('option:port', function (port) { // 这里处理指令要跑得逻辑 console.log("测试:", port) }); program .option('-p, --port ', '端口号') program.parse();
好处是可以将逻辑都封装入一个匿名函数中
-
使用
program.opts().xxx
【这里xxx是在option里注册的--xxxx
】#!/usr/bin/env node
import { Command } from 'commander' const program = new Command();
program .option('-p, --port ', '端口号') program.parse();
const options = program.opts() if (options.prot) { console.log(options.port) }
所有注册的指令都可以在option.opts()
找到。
这里推荐使用函数式监听,因为可以将逻辑拆分为小模块。
至此本轮需求已完成。
需求3:让脚手架支持my-cli xxx的指令
上边提供了让脚手架实现选项的能力,接下来要实现:
my-cli add
my-cli add xxxx
首先官方教程在[
这里
指令输入能力与选项一致
有关于指令,有两个方法:.command()``.addCommand()
;
本处只使用.command()
command()
参数解读:
参数
作用
参数1:指令flag(string)
第一个参数就是我们所注册的指令,其格式为${} < xx > [ xxx ]
参数2:指令选项opts
第二个选项就是为指令配置的选项【一般不用,因为可以在下方添加.option
】
步骤:
-
首先要对指令进行注册,因此要使用
command()
进行注册。#!/usr/bin/env node
import { Command } from 'commander' const program = new Command();
program .command('clone [destination]')
program.parse();
注册完成后就可以使用该指令了。
-
成功注册完,要对指令的功能进行描述。因此,需要在指令的下方添加
.description()
方法,该方法就接收一个参数(string),其作用为:为指令添加描述。#!/usr/bin/env node
import { Command } from 'commander' const program = new Command();
program .command('clone') .description('这里是clone的描述')
program.parse();
至此指令+描述已成功实现,可通过my-cli -h
查看自己已注册的指令以及指令的描述。
-
为指令添加执行逻辑 ,指令的执行逻辑一般使用
.action()
来编写,action()
方法接收一个函数,当执行指令时如果有action(()=>{})
方法的话,将会在执行指令后,进入action
传入的函数体执行相关的逻辑。#!/usr/bin/env node
import { Command } from 'commander' const program = new Command();
program.on('option:port', function (port) { console.log("测试:", port) });
program .command('clone') .description('这里是clone的描述') .action((source, destination) => { console.log('clone command called'); }); program .command('add') .description('这里是add的描述') .action((source, destination) => { console.log('add command called'); });
program.parse();
在完整的编写完command``description``action
之后就已经添加了一条可以运行的逻辑的指令了。
ps:指令也可以想选项一样传入参数,只需要.command('clone <source> [destination]')
即可,输入的参数将会传入action()
的回调函数的参数内。
至此本轮需求已完成
需求4:让脚手架支持多条输入/[多选/单选]选项/确认
上方实现了注册指令后,就可以执行类似my-cli add xxx
的逻辑了【可能会需要其他依赖搭配实现。】
现在需求要求是要在脚手架收集更多的用户信息,因此仅仅依靠commander
是不够的;
因此引入了一个新的库inquirer
基础使用步骤【以多条输入为例】:
- 首先要对库进行安装:
npm install @inquirer/prompts
-
编写逻辑体:
#!/usr/bin/env node
import { Command } from 'commander' const program = new Command(); import { input } from '@inquirer/prompts';
program.action(async () => {
const answer = await input({ message: '请输入你的问题:' }); const answer1 = await input({ message: '请输入你的第二个问题:' }); const answer2 = await input({ message: '请输入你的第三个问题:' });
})
program.parse();
解读:
由于是为了进入输入环境,这里直接使用了action
了,其实还可以添加指令,再进入讯问环节;
这里首先是使用阻塞方式来等待用户输入,用户输入了内容会被存储到对应的变量名内,我们如果想要有其他的逻辑编写可以在下方编写。
在action内,有多少个await input()
就会有多少条输入。
除了input
纯文字输入还有其他形式,比如多选、单选、确认等,均可以从github上选择复制,按照上方提供的方式,在action
中编写逻辑从而实现想要的功能。
至此本轮需求已完成
需求5:让脚手架实现拉取github模板的能力
前边实现了my-cli clone
指令,但是没有对应的拉取模板的能力。
这边要实现拉取能力这里依靠download-git-repo
步骤:
-
首先要将依赖集成入项目中:
npm install download-github-repo
-
编写对应拉取模板的代码:
#!/usr/bin/env node
import { Command } from 'commander' const program = new Command();
import download from 'download-github-repo'
program .command('clone [url]') .description('这里是clone的描述') .action((url) => { download(url, 'download/test', function (err) { console.log(err) }) });
program.parse();
download()
解读:
有三个参数:
- 第一个参数是要拉取的模板的地址:由于内部经过处理,这里只需要使用
${github名字}/${仓库地址}
- 第二个参数是配置要将项目拉到本地的位置
- 第三个参数是错误回调,如果发生异常时会进入这里。
至此本轮需求已完成
需求6:除以上几项逻辑功能外添加额外能力
上述几项已经实现了一个脚手架的大部分功能,但是有时我们的需求是要对系统其他命令进行操作【包括并不限于git
系列命令、对文件操作等】。
首先在node基础上,我们可以通过fs
模块去实现对系统文件的部分操作。
一切逻辑皆在action中编写。
而对于更多的操作,可以编写shell
脚本;
node使用shell
脚本方法,有两种:
-
使用node的
child_process
模块的execFile
方法;该方法,第一个参数为shell文件路径,第二个参数为携带的选项【此处未过多探究】,第三个是一个回调函数(回调函数有三个参数,第一个和第三个是抛出错误,第二个是shell文件输出的内容);
import child from 'child_process';
child.execFile("./src/install.sh", [], (err, stdout, stderr) => { console.log(stdout) })
如果是执行一个shell指令,则可以使用exec
方法。
接合脚手架,代码:
javascript
#!/usr/bin/env node
import { Command } from 'commander'
const program = new Command();
import child from 'child_process'
program
.command("test")
.description("测试执行shell 脚本")
.action(() => {
child.execFile("./src/install.sh", [], (err, stdout, stderr) => {
console.log(stdout)
})
})
program.parse();
当执行my-cli test
时,将会运行指定路径的shell脚本
-
使用
shelljs
-
首先下载依赖:
npm install shelljs
-
编写逻辑代码
import shell from 'shelljs';
javascriptshell.exec('/Users/richguo/Desktop/self-cli/src/install.sh', function (...rest) { console.log(rest) })
目前测试shell.exec()
的第一个参数只能是绝对路径,第二个则为结果回调,与node的child_process类似。
需求7:自定义脚手架错误回调信息
在完成命令功能后,要对使用进行一次优化,良好的错误提示是非常重要的。
首先,我们可以使用.**exitOverride(()=>{})**
;
当全局出现错误时,可以捕获到对应的错误信息对象,如:
首先我们可以依据获得的错误信息给出良好的中文提示。
方式二:使用configureOutput
对错误进行重写:
自定义脚手架至此已能够自行开发大部分逻辑能力,其中shell部分需要额外的去对shell语法进行学习才能够继续进行功能扩展。