让我们来体验一下需求驱动开发,来完成一个脚手架吧!

依赖

  • commander:提供选项指令功能
  • inquirer:提供交互式命令行
  • download-git-repo:提供github拉取模板的能力
  • ora:提供任务加载loading的能力
  • chalk:颜色插件,可以修改控制台输出的颜色
  • handlebars.js:模板引擎工具

需求1:得到一个能够执行的脚手架

如果想要得到一个可以执行的命令行,在node已有现成方案,即使用:Commander

Commander里已有教程,这里只提供需求所需步骤。

步骤:

**

  • 首先进行项目初始化

****npm init -y**

  • 安装 **commander**

  • **npm install commander**

  • 安装成功后,首先修改 **package.json**,其主要有两个步骤:

  1. 首先需要将我们要写的脚手架名字写入配置bin(type:Object)中【其中key为我们要执行的脚手架名字,value为我们脚手架所对应要执行的js文件】
  2. 确定脚手架开发模式,即设置pacekage.jsontype属性;【选择 esm,需要将type改为module;选择commonjs,则需将type改为commonjs】

**

  • 创建脚手架执行文件,其中主要有2个步骤:

**

  1. 第一步,创建文件并在文件第一行添加固定代码: **#!/usr/bin/env node**

  2. 第二步,引入命令行类并实例化,实例化后调用 **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提供了三种输入能力:

  1. 当使用< xx >尖括号定义选项输入内容时必须要输入一个值,否则抛出错误

  2. 使用[ xx ]方括号定义选项输入内容时,该参数就变为可选;当不输入时,则为布尔值(true/false);当输入时则为字符型变量;

  3. 默认情况下,只能获得选项后第一位参数,如果想要获取多个参数,可以[ xx... ]在输入形参处添加...,此行为代表该参数将为数组类型,将会收集选项后的多条参数(以空格分隔)

【ps:如果默认没有添加输入能力,则该选项为布尔值】

步骤:

  1. 实例化的program上有一个option方法,该方法用于给脚手架注册选项

    #!/usr/bin/env node

    import { Command } from 'commander' const program = new Command();

    program .option('-p, --port ', '端口号') program.parse();

注意⚠️**:** **program.option()**必须要在 **parse()**之前调用。

  1. 针对选项要执行的逻辑处理,有两种方式:

  2. 自定义监听器

    #!/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();

好处是可以将逻辑都封装入一个匿名函数中

  1. 使用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

首先官方教程在[

这里

](github.com/tj/commande...)

指令输入能力与选项一致

有关于指令,有两个方法:.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

基础使用步骤【以多条输入为例】:

  1. 首先要对库进行安装:
    npm install @inquirer/prompts
  1. 编写逻辑体:

    #!/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中编写逻辑从而实现想要的功能。

github.com/SBoudrias/I...

至此本轮需求已完成

需求5:让脚手架实现拉取github模板的能力

前边实现了my-cli clone指令,但是没有对应的拉取模板的能力。

这边要实现拉取能力这里依靠download-git-repo

步骤:

  1. 首先要将依赖集成入项目中:
    npm install download-github-repo

  2. 编写对应拉取模板的代码:

    #!/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脚本方法,有两种:

  1. 使用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脚本

  1. 使用shelljs

  2. 首先下载依赖:npm install shelljs

  3. 编写逻辑代码

    import shell from 'shelljs';

    javascript 复制代码
    shell.exec('/Users/richguo/Desktop/self-cli/src/install.sh', function (...rest) {
      console.log(rest)
    })

目前测试shell.exec()的第一个参数只能是绝对路径,第二个则为结果回调,与node的child_process类似。

需求7:自定义脚手架错误回调信息

在完成命令功能后,要对使用进行一次优化,良好的错误提示是非常重要的。

首先,我们可以使用.**exitOverride(()=>{})**;

当全局出现错误时,可以捕获到对应的错误信息对象,如:

首先我们可以依据获得的错误信息给出良好的中文提示。

方式二:使用configureOutput对错误进行重写:

自定义脚手架至此已能够自行开发大部分逻辑能力,其中shell部分需要额外的去对shell语法进行学习才能够继续进行功能扩展。

git仓库:github.com/DockorDo/se...

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui