快速入门
MixCli
是一个命令行应用开发框架,其主要是对commander
和prompts
的封装,提供了更加友好的命令行开发体验。
以下我们将通过一个典型的monorepo
工程,来介绍如何使用MixCli
开发命令行应用。
拟开发一个名为flex
应用,该应用提供了@flex/cli
,需要达成以下效果:
@flex/cli
提供了名称为flex
的命令flex init
用来初始化vue
、react
、angular
应用flex dev
用来启动开发应用- 开发者要使用
flex
时,只需要安装@flex/cli
,然后: 如果要开发vue
应用,则安装@flex/vue
,则可以使用flex init
创建vue
应用开发 如果要开发react
应用,则安装@flex/react
,则可以使用flex init
创建react
应用开发 如果要开发angular
应用,则安装@flex/angular
,则可以使用flex init
创建angular
应用开发 - 如果开发者没有安装
@flex/cli
、@flex/react
、@flex/angular
中的任意一个,则执行flex dev
时会用户选择其中的一个,或者在命令行通过-t
来传入。 - 考虑到创建
vue
、react
、angular
应用的相关逻辑的不一样,我们不希望将所有逻辑代码均放在@flex/cli
中,而分别位于packages/vue
,packages/react
,packages/angular
中。这样最大的优点在于,由于应用逻辑并不存在于@flex/cli
,@flex/cli
仅是一个入口,所以@flex/cli
可以保持稳定。
第1步:创建工程
首先创建一个monorepo
工程,工程结构如下:
flex
|-- packages
|-- cli
|-- vue
|-- react
|-- angular
|-- package.json
flex
|-- packages
|-- cli
|-- vue
|-- react
|-- angular
|-- package.json
示例工程名为flex
,工程中的包名分别是@flex/cli
、@flex/angular
、@flex/vue
、@flex/react
。
第2步:创建命令行应用
@flex/cli
是命令行应用,对外提供名称为flex
的命令。
1. 安装依赖
npm install mixcli
npm install mixcli
pnpm add mixcli
pnpm add mixcli
yarn add mixcli
yarn add mixcli
2. 创建cli.js
在@flex/cli
包中创建cli.js
文件,内容如下:
flex
|-- pacakges
|-- cli
|-- cli.js
|-- init.js
|-- package.json
flex
|-- pacakges
|-- cli
|-- cli.js
|-- init.js
|-- package.json
主要内容如下:
const { MixCli } = require("mixcli")
const cli = new MixCli({
name: "flex",
version: "1.0.0",
include: /^\@flex\//,
logo: String.raw`
____ ____ __
\ \ / /___ ___________| | _______
\ Y / _ \_/ __ \_ __ \ |/ /\__ \
\ ( <_> ) ___/| | \/ < / __ \_
\___/ \____/ \___ >__| |__|_ \(____ /
\/ \/ \/`,
})
cli.run()
const { MixCli } = require("mixcli")
const cli = new MixCli({
name: "flex",
version: "1.0.0",
include: /^\@flex\//,
logo: String.raw`
____ ____ __
\ \ / /___ ___________| | _______
\ Y / _ \_/ __ \_ __ \ |/ /\__ \
\ ( <_> ) ___/| | \/ < / __ \_
\___/ \____/ \___ >__| |__|_ \(____ /
\/ \/ \/`,
})
cli.run()
{
"name": "@flex/cli",
"version": "1.0.0",
"bin": {
"flex": "cli.js"
}
}
{
"name": "@flex/cli",
"version": "1.0.0",
"bin": {
"flex": "cli.js"
}
}
@flex/cli
仅仅是一个命令行的入口:
- 重点:
include: /^\@flex\//
的意思是告诉mixcli
,当执行flex
命令时,会在当前工程中搜索以@flex/
开头的包,然后包中声明在cli
文件夹下的所有命令被合并到flex
命令中。 - 上面所说的
当前工程
指的是安装了@flex/cli
的工程,而不是我们的示例工程。 @flex/cli
中使用cli.register(initCommand)
,注册一个通用的init
命令,该命令的实现在init.js
中。 一般可以在此工程提供一些通用命令,而其他的命令声明逻辑在分别在@flex/*/cli/*.js
等包中实现。
第3步: 创建init命令
接下来,我们在@flex/cli
中创建一个init
命令。
flex
|-- pacakges
|-- cli
|-- cli.js
|-- init.js
|-- package.json
flex
|-- pacakges
|-- cli
|-- cli.js
|-- init.js
|-- package.json
编写packages/cli/init.js
文件,内容如下:
const { MixCommand } = require('mixcli');
/**
* @param {import('mixcli').MixCli} cli
*/
module.exports = (cli)=>{
const initCommand = new MixCommand("init");
initCommand
.description("创建应用")
.option("-t, --type <type>", "应用类型",{choices:["vue","react","angular"]})
.action((options)=>{
console.log("Run init:",options.type)
})
return initCommand
}
const { MixCommand } = require('mixcli');
/**
* @param {import('mixcli').MixCli} cli
*/
module.exports = (cli)=>{
const initCommand = new MixCommand("init");
initCommand
.description("创建应用")
.option("-t, --type <type>", "应用类型",{choices:["vue","react","angular"]})
.action((options)=>{
console.log("Run init:",options.type)
})
return initCommand
}
然后,我们在packages/cli/index.js
中注册init
命令。
const { MixCli } = require("mixcli")
const initCommand = require("./init")
const cli = new MixCli({
name: "flex",
include: /^\@flex\//,
//...
})
cli.register(initCommand)
cli.run()
const { MixCli } = require("mixcli")
const initCommand = require("./init")
const cli = new MixCli({
name: "flex",
include: /^\@flex\//,
//...
})
cli.register(initCommand)
cli.run()
现在执行flex
命令,可以看到init
命令已经被注册到flex
命令行中了。
如果运行flex init
,则会执行init
命令就会自动提示用户选择,然后执行action
函数。
第4步:命令选项分布式处理
在本例中,我们为init
命令设计了["vue","react","angular"]
三个选项。
- 常规处理方式
常规情况下,我们会按照如下方式处理命令选项:
// packages/cli/init.js
const { MixCommand } = require('mixcli');
module.exports = (cli)=>{
const initCommand = new MixCommand("init");
initCommand
.description("创建应用")
.option("-t, --type <type>", "应用类型",{choices:["vue","react","angular"]})
.action((options)=>{
if(optins.type === "vue"){
// ...
}else if(optins.type === "react"){
// ...
}else if(optins.type === "angular"){
// ...
}
})
return initCommand
}
// packages/cli/init.js
const { MixCommand } = require('mixcli');
module.exports = (cli)=>{
const initCommand = new MixCommand("init");
initCommand
.description("创建应用")
.option("-t, --type <type>", "应用类型",{choices:["vue","react","angular"]})
.action((options)=>{
if(optins.type === "vue"){
// ...
}else if(optins.type === "react"){
// ...
}else if(optins.type === "angular"){
// ...
}
})
return initCommand
}
这种处理方式下,我们需要在@flex/cli
中包含所有的vue/react/angular
处理逻辑的代码,这样会导致@flex/cli
包变得臃肿并且不易维护。
更好的处理方式是,将处理逻辑分别放在@flex/vue/cli/init.js
、@flex/react/cli/init.js
、@flex/angular/cli/init.js
中。
- 分布式处理方式
MixCli
提供了这样的分布式处理命令选择的能力。
我们分别在@flex/vue/cli/init.js
、@flex/react/cli/init.js
、@flex/angular/cli/init.js
中实现init
命令的处理逻辑。
const { MixCommand,BREAK } = require('mixcli');
module.exports = (cli)=>{
cli.find("init").then(initCommand=>{
initCommand
.action((options)=>{
if(options.type === "vue"){
console.log("[vue] Run init :",options.type)
return BREAK
}
})
});
}
const { MixCommand,BREAK } = require('mixcli');
module.exports = (cli)=>{
cli.find("init").then(initCommand=>{
initCommand
.action((options)=>{
if(options.type === "vue"){
console.log("[vue] Run init :",options.type)
return BREAK
}
})
});
}
const { MixCommand,BREAK } = require('mixcli');
module.exports = (cli)=>{
cli.find("init").then(initCommand=>{
initCommand
.action((options)=>{
if(options.type === "react"){
console.log("[react] Run init :",options.type)
return BREAK
}
})
});
}
const { MixCommand,BREAK } = require('mixcli');
module.exports = (cli)=>{
cli.find("init").then(initCommand=>{
initCommand
.action((options)=>{
if(options.type === "react"){
console.log("[react] Run init :",options.type)
return BREAK
}
})
});
}
const { MixCommand,BREAK } = require('mixcli');
module.exports = (cli)=>{
cli.find("init").then(initCommand=>{
initCommand
.action((options)=>{
if(options.type === "angular"){
console.log("[angular] Run init :",options.type)
return BREAK
}
})
});
}
const { MixCommand,BREAK } = require('mixcli');
module.exports = (cli)=>{
cli.find("init").then(initCommand=>{
initCommand
.action((options)=>{
if(options.type === "angular"){
console.log("[angular] Run init :",options.type)
return BREAK
}
})
});
}
- 在
src/cli
目录下创建init.js
文件,用于声明init
命令。cli
目录下的所有js
文件会被自动加载,每个文件均导出一个函数,该函数需要返回一个或多个MixCommand
实例。cli
目录是一个默认的约定目录,可以通过cli.cliDir
参数修改。 - 创建
MixCommand
实例,用于声明命令。MixCommand
继承自commander
的Command
类,因此可以使用commander
的所有特性。 package.json
只需要将mixcli
添加为依赖即可。
第5步: 开发子命令
以上是分布式处理命令选项的方式,MixCli
也支持创建子命令。
以下在@flex/vue
中创建init vue
子命令。
// @flex/vue/cli/create_vue.js
const { MixCommand } = require('mixcli');
module.exports = (cli)=>{
cli.find("init").then((initCommand)=>{
const initVueCommand = new MixCommand("vue");
initVueCommand
.description("创建Vue应用")
.option("-a, --app <value>", "应用名称",{validate:(value)=>value.length>5})})
.action((options)=>{
console.log("创建Vue应用:",options.app)
})
initCommand.addCommand(initVueCommand)
})
}
// @flex/vue/cli/create_vue.js
const { MixCommand } = require('mixcli');
module.exports = (cli)=>{
cli.find("init").then((initCommand)=>{
const initVueCommand = new MixCommand("vue");
initVueCommand
.description("创建Vue应用")
.option("-a, --app <value>", "应用名称",{validate:(value)=>value.length>5})})
.action((options)=>{
console.log("创建Vue应用:",options.app)
})
initCommand.addCommand(initVueCommand)
})
}
然后当执行flex init vue
时,会看到如下输出:
此时执行flex init --help
可以看到vue
子命令的帮助信息:
第6步: 自动推断交互提示
接下来我们创建一个dev
命令, 用于启动开发服务器,展示交互提示。
const { MixCommand } = require('mixcli');
/**
* @param {import('mixcli').MixCli} cli
*/
module.exports = (cli)=>{
const devCommand = new MixCommand("dev");
devCommand
.description("开发模式")
// 指定了默认值且强制提示
.option("--count <value>","数量",{default:5,prompt:true})
// 没有指定默认值,使用,分割多个值
.option("-r,--routes <value...>","路由(多个值采用,分割)")
// 指定了默认值时不进行提示
.option("-p,--port <port>","指定端口号",3000)
// 有默认值且强制显示提示
.option("-d,--debug" ,"调试模式",{ default:true,prompt:true })
.option("--color <value...>","显示颜色",{choices:["red","yellow","blue"],prompt:"multiselect"})
// 未指定默认值,使用自动完成,可以输入任意值
.option("--filter <value>","文件过滤",{choices:["src","test","debug"],prompt:"autocomplete"})
.option("-h,--host <host>","指定主机名",{default:"localhost",prompt:true}) // 自动提示(没有输入且无默认值时)
// 始终不进行提示取,取决env是可选还是必选
.option("-e,--env [value]","环境变量",{ prompt:false })
.option("-m,--mode <mode>","指定模式",{choices:["development","production","test","debug"]})
.option("-f,--framework [value]","开发框架",{choices:[
{title:"vue",value:1},
{title:"react",value:2,description:"默认"},
{title:"angular",value:3}
]})
.option("-o,--open","自动打开浏览器",{prompt:{ // 自定义提示
type:"toggle",
message:"是否自动打开浏览器?",
}})
.action((options)=>{
console.log(" run dev app")
console.log("dev app",options)
})
return devCommand
}
const { MixCommand } = require('mixcli');
/**
* @param {import('mixcli').MixCli} cli
*/
module.exports = (cli)=>{
const devCommand = new MixCommand("dev");
devCommand
.description("开发模式")
// 指定了默认值且强制提示
.option("--count <value>","数量",{default:5,prompt:true})
// 没有指定默认值,使用,分割多个值
.option("-r,--routes <value...>","路由(多个值采用,分割)")
// 指定了默认值时不进行提示
.option("-p,--port <port>","指定端口号",3000)
// 有默认值且强制显示提示
.option("-d,--debug" ,"调试模式",{ default:true,prompt:true })
.option("--color <value...>","显示颜色",{choices:["red","yellow","blue"],prompt:"multiselect"})
// 未指定默认值,使用自动完成,可以输入任意值
.option("--filter <value>","文件过滤",{choices:["src","test","debug"],prompt:"autocomplete"})
.option("-h,--host <host>","指定主机名",{default:"localhost",prompt:true}) // 自动提示(没有输入且无默认值时)
// 始终不进行提示取,取决env是可选还是必选
.option("-e,--env [value]","环境变量",{ prompt:false })
.option("-m,--mode <mode>","指定模式",{choices:["development","production","test","debug"]})
.option("-f,--framework [value]","开发框架",{choices:[
{title:"vue",value:1},
{title:"react",value:2,description:"默认"},
{title:"angular",value:3}
]})
.option("-o,--open","自动打开浏览器",{prompt:{ // 自定义提示
type:"toggle",
message:"是否自动打开浏览器?",
}})
.action((options)=>{
console.log(" run dev app")
console.log("dev app",options)
})
return devCommand
}
const { MixCli } = require("mixcli")
const initCommand = require("./init")
const devCommand = require("./dev")
const cli = new MixCli({
name: "flex",
version: "1.0.0",
include: /^\@flex\//,
})
cli.register(initCommand)
cli.register(devCommand)
cli.run()
const { MixCli } = require("mixcli")
const initCommand = require("./init")
const devCommand = require("./dev")
const cli = new MixCli({
name: "flex",
version: "1.0.0",
include: /^\@flex\//,
})
cli.register(initCommand)
cli.register(devCommand)
cli.run()
当执行flex dev
时,会看到如下输出:
- 命令行的交互体验与使用
commander
时完全一样 - 仅当选项未指定默认值或满足一定条件时,才会根据一定的规则自动推断交互提示类型。详见自动推断交互提示
MixCli
使用prompts
来实现交互提示,因此支持prompts
的所有交互类型特性。详见prompts
第7步: 开发命令
最后就是开发命令了,此时轮到开源工具库logsets
上场了。
logsets
是一个终端增强显示组件,用来在终端中显示表格、列表、树形结构等数据,提供更好友好的终端交互体验。
以下是logsets
的使用示例:
const { MixCommand } = require('mixcli');
module.exports = (cli)=>{
const devCommand = new MixCommand("dev");
devCommand
.description("开发模式")
.action(async ()=>{
const tasks = logsets.createTasks([
{
title:"任务处理被停止",
execute:async ()=>{
await delay(100)
return "abort"
}
},
{
title:"开始扫描文件",
execute:async ()=>{await delay(100);return 1}
},
{ title:"准备对文件进行预处理",
execute:async ()=>{throw new Error("已安装")},
},
{ title:"准备对文件进行预处理",
execute:async ()=>{
await delay(100)
return "已完成"
}
},
{ title:"执行过程中显示进度",
execute:async ({task})=>{
for(let i=0;i<100;i++){
await delay(100)
task.note(i+"%")
}
}
},
{
title:"读取文件并编译成exe文件",
execute:async ()=>{
await delay(100)
return ['stop',"不干了"]
}
},
{
title:"任务处理被停止",
execute:async ()=>{
await delay(100)
return ["abort",'真的不干了']
}
},
"-",
{
title:"任务执行失败",
execute:async ()=>{throw new Error("TimeOut")},
error:["ignore","忽略:{message}"]
},
{
title:"任务待办状态",
execute:async ()=>{throw new Error("TimeOut")},
error:"出错了"
},
"出错处理",
{
title:["下载文件:{},大小:{}, 已下载{}","package.json",122,344],
execute:async ()=>{throw new Error("TimeOut")},
error:"出错了:{message}"
},
{
title:["下载文件:{},大小:{}, 已下载{}",["package.json",122,344]],
execute:async ()=>{throw new Error("TimeOut")},
error:()=>"X"
},
{
title:["下载文件:{},大小:{}, 已下载{}",["package.json",122,344]],
execute:async ()=>{throw new Error("TimeOut")},
error:()=>"skip"
},
],{ignoreErrors:true})
try{
let results = await tasks.run(["开始执行{}任务",5])
console.log(results)
}catch(e){
console.error(e)
}
})
const { MixCommand } = require('mixcli');
module.exports = (cli)=>{
const devCommand = new MixCommand("dev");
devCommand
.description("开发模式")
.action(async ()=>{
const tasks = logsets.createTasks([
{
title:"任务处理被停止",
execute:async ()=>{
await delay(100)
return "abort"
}
},
{
title:"开始扫描文件",
execute:async ()=>{await delay(100);return 1}
},
{ title:"准备对文件进行预处理",
execute:async ()=>{throw new Error("已安装")},
},
{ title:"准备对文件进行预处理",
execute:async ()=>{
await delay(100)
return "已完成"
}
},
{ title:"执行过程中显示进度",
execute:async ({task})=>{
for(let i=0;i<100;i++){
await delay(100)
task.note(i+"%")
}
}
},
{
title:"读取文件并编译成exe文件",
execute:async ()=>{
await delay(100)
return ['stop',"不干了"]
}
},
{
title:"任务处理被停止",
execute:async ()=>{
await delay(100)
return ["abort",'真的不干了']
}
},
"-",
{
title:"任务执行失败",
execute:async ()=>{throw new Error("TimeOut")},
error:["ignore","忽略:{message}"]
},
{
title:"任务待办状态",
execute:async ()=>{throw new Error("TimeOut")},
error:"出错了"
},
"出错处理",
{
title:["下载文件:{},大小:{}, 已下载{}","package.json",122,344],
execute:async ()=>{throw new Error("TimeOut")},
error:"出错了:{message}"
},
{
title:["下载文件:{},大小:{}, 已下载{}",["package.json",122,344]],
execute:async ()=>{throw new Error("TimeOut")},
error:()=>"X"
},
{
title:["下载文件:{},大小:{}, 已下载{}",["package.json",122,344]],
execute:async ()=>{throw new Error("TimeOut")},
error:()=>"skip"
},
],{ignoreErrors:true})
try{
let results = await tasks.run(["开始执行{}任务",5])
console.log(results)
}catch(e){
console.error(e)
}
})
执行flex dev
,会看到如下输出:
更多的logsets
使用示例,请参考logsets
小结
MixCli
是一个基于commander
的命令行工具开发框架,提供了一套命令行开发的最佳实践。MixCli
能对所有命令行选项自动推断交互提示类型,当用户没有输入选项时,会自动引导用户输入选项,提供友好的用户体验。MixCli
可以在当前工程自动搜索满足条件的包下声明的命令进行合并,从而实现扩展命令的目的。此特性可以保持@flex/cli包的精简和稳定,给用户一致的体验。