4.6.1 其他参数配置
有少数附加功能可以直接构造Argument
对象,对参数进行更详尽的配置。
program .addArgument(new commander.Argument('<drink-size>', 'drink cup size').choices(['small', 'medium', 'large'])) .addArgument(new commander.Argument('[timeout]', 'timeout in seconds').default(60, 'one minute'))
4.6.2 自定义参数处理
选项的参数可以通过自定义函数来处理(与处理选项参数时类似),该函数接收两个参数:用户新输入的参数值和当前已有的参数值(即上一次调用自定义处理函数后的返回值),返回新的命令参数值。
处理后的参数值会传递给命令处理函数,同时可通过.processedArgs
获取。可以在自定义函数的后面设置命令参数的默认值或初始值。
program .command('add') .argument('<first>', 'integer argument', myParseInt) .argument('[second]', 'integer argument', myParseInt, 1000) .action((first, second) => { console.log(`${first} + ${second} = ${first + second}`); }) ;
4.7 处理函数
命令处理函数的参数,为该命令声明的所有参数,除此之外还会附加两个额外参数:一个是解析出的选项,另一个则是该命令对象自身
program .argument('<name>') .option('-t, --title <honorific>', 'title to use before name') .option('-d, --debug', 'display some debugging') .action((name, options, command) => { if (options.debug) { console.error('Called %s with options %o', command.name(), options); } const title = options.title ? `${options.title} ` : ''; console.log(`Thank-you ${title}${name}`); });
测试:
ljy-create-react-app % ljy --title=hello kevin -d Called ljy with options { title: 'hello', debug: true } Thank-you hello kevin
如果你愿意,你可以跳过为处理函数声明参数直接使用 command。 this
关键字设置为运行命令,可以在函数表达式中使用(但不能从箭头函数中使用)。
program .command('serve') .argument('<script>') .option('-p, --port <number>', 'port number', 80) .action(function() { console.error('Run script %s on port %s', this.args[0], this.opts().port); });
处理函数支持async
,相应的,需要使用.parseAsync
代替.parse
。
async function run() { /* 在这里编写代码 */ } async function main() { program .command('run') .action(run); await program.parseAsync(process.argv); }
使用命令时,所给的选项和命令参数会被验证是否有效。凡是有未知的选项,或缺少所需的命令参数,都会报错。 如要允许使用未知的选项,可以调用.allowUnknownOption()
。默认情况下,传入过多的参数并不报错,但也可以通过调用.allowExcessArguments(false)
来启用过多参数的报错。
4.8 生命周期钩子
可以在命令的生命周期事件上设置回调函数。
program .option('-t, --trace', 'display trace statements for commands') .hook('preAction', (thisCommand, actionCommand) => { if (thisCommand.opts().trace) { console.log(`About to call action handler for subcommand: ${actionCommand.name()}`); console.log('arguments: %O', actionCommand.args); console.log('options: %o', actionCommand.opts()); } });
钩子函数支持async
,相应的,需要使用.parseAsync
代替.parse
。一个事件上可以添加多个钩子。
支持的事件有:
事件名称 | 触发时机 | 参数列表 |
preAction , postAction |
本命令或其子命令的处理函数执行前/后 | (thisCommand, actionCommand) |
preSubcommand |
在其直接子命令解析之前调用 | (thisCommand, subcommand) |
5、设计 create action
一般我们会将这些指令单独放在一个地方去归档,以便于以后维护,比如在根目录中新建一个lib来专门放这些指令的信息,将help指令的信息放在lib/core/help.js
,创建指令的信息放在lib/core/create.js
中。
5.1 helpOptions
下面实现 helpOptions
:
// lib/core/help.js const program = require('commander'); const helpOptions = () => { // 增加自己的options program.option('-d --dest <dest>', 'A destination folder,例如: -d /src/home/index.js') program.option('-f --framework <framework>', 'Your framework,例如: React / Vue') // 监听指令 program.on('--help', function(){ console.log('') console.log('Others') console.log(' others') }) } module.exports = helpOptions;
在 bin/index.js
中使用:
// bin/index.js #!/usr/bin/env node const program = require('commander'); // 查看版本号 program.version(require('../package.json').version); const helpOptions = require('../lib/core/help'); // 帮助和可选信息 helpOptions(); program.parse(process.argv);
测试
ljy-create-react-app % ljy --help Usage: ljy [options] Options: -V, --version output the version number -d --dest <dest> A destination folder,例如: -d /src/home/index.js -f --framework <framework> Your framework,例如: React / Vue -h, --help display help for command Others others
5.2 createCommands
再实现 createCommands
:
const program = require('commander'); const createCommands = () => { program .command('create <project> [others...]') .description('clone a repo into a folder') .action((project, others) => { console.log('project', project); console.log('others', others) }) } module.exports = createCommands;
并引入到 bin/index.js
中
#!/usr/bin/env node const program = require('commander'); // 查看版本号 program.version(require('./package.json').version); const helpOptions = require('./lib/core/help'); const createCommands = require('./lib/core/create'); // 帮助和可选信息 helpOptions(); // 创建指令 createCommands(); program.parse(process.argv);
测试:
ljy-create-react-app % ljy create xxx project xxx others []
5.3 createProjectActions
action
的回调函数就是我们脚手架的核心流程了,将其抽离到一个单独的文件 lib/core/actions.js
中:
// lib/core/actions.js // 封装create指令的acitons const createProjectActions = (project, others) => { console.log('project', project); console.log('others', others) // 1,clone项目 // 2,运行 npm install // 3,运行 npm run dev } module.exports = createProjectActions;
在 lib/core/create.js
中使用:
const program = require('commander'); const createProjectActions = require('./actions'); const createCommands = () => { program .command('create <project> [others...]') .description('clone a repo into a folder') .action(createProjectActions) } module.exports = createCommands;
5.3.1 clone项目
clone
项目一般会用到一个工具库:download-git-repo,它是放在 npm
和 gitlab
上的,在 github
上面没有仓库,vue-cli
用的也是这个来下载项目模板。
先下载库:
npm install download-git-repo
这个库不好的地方就是它使用的写法比较老旧:
download('flippidippi/download-git-repo-fixture', 'test/tmp', function (err) { console.log(err ? 'Error' : 'Success') })
我们一般会将repo地址提取出来,方便以后进行维护:
// lib/config/repo-config.js // github 为了政治正确 把默认分支改成了main,所以这里需要再后面带一个分支信息,否则会报错 const reactRepo = "direct:https://github.com/ian-kevin126/react18-ts4-webpack5-starter#main"; module.exports = { reactRepo, };
我们的操作都是在回调里面去做,如果当操作很多的时候,就会造成回调地狱。所幸,node提供了一个模块可以改变这种操作—— promisify
,可以将这种方法转化成 promise
形式。
// 封装create指令的acitons const { promisify } = require('util') const download = promisify(require('download-git-repo')) const { reactRepo } = require('../config/repo-config') // callback ---> promisify ---> Promise ---> async await const createProjectActions = async (project, others) => { // 1,clone项目 await download(reactRepo, project, { clone: true }) // 2,运行npm install // 3,运行npm run dev } module.exports = createProjectActions
现在我们测试一下这个指令,新建一个测试文件夹,然后在终端运行:
ljy create demo-repo
可以发现,我们刚刚配置的 github repo
里面的项目被我们clone下来了。
5.3.2 执行 npm install
接下来,我们希望在clone完项目代码之后,自动执行 package.json
中的 dependencies
依赖的安装,并且将依赖下载的信息打印在控制台里。
需要注意的是,我这里使用的是 pnpm,如果你没有安装这个工具,可能会报错。
还可以使用 chalk
对控制台信息做一些美化,先安装依赖:
npm i chalk@4.0.0
然后就可以在代码中使用了
// 封装create指令的acitons const chalk = require("chalk"); const { promisify } = require("util"); const download = promisify(require("download-git-repo")); const { reactRepo } = require("../config/repo-config"); const { spawnCommand } = require("../utils/terminal"); // callback ---> promisify ---> Promise ---> async await const createProjectActions = async (project, others) => { console.log(chalk.green.underline.bold("> Start download repo...")); // 1,clone项目 await download(reactRepo, project, { clone: true }); console.log(chalk.green("> 模板下载完成,开始 pnpm install...")); // 2. 执行 npm install // 需要判断一下平台,window 执行 npm 命令实际执行的 npm.cmd const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; await spawnCommand(command, ["install"], { cwd: `./${project}` }); // 3,运行npm run dev await spawnCommand(command, ["run", "dev"], { cwd: `./${project}` }); }; module.exports = createProjectActions;
附终端命令相关的执行方法封装:
// lib/utils/terminal.js /** * 终端命令相关 */ const { spawn } = require("child_process"); // 创建进程执行终端命令 const spawnCommand = (command, args, options) => { return new Promise((resolve, reject) => { // 通过 spawn 创建一个子进程,并把进程返回 const childProcess = spawn(command, args, options); // 将子进程输出的东西放进当前进程的全局变量 process 的 stdout 中 // 比如说,当子进程执行 npm install,执行完的时候,会输出一些信息 // childProcess.stdout 就是这个输出信息流,通过 pipe 将流信息存到当前进程(主进程) childProcess.stdout.pipe(process.stdout); // 将子进程错误信息放进当前进程 childProcess.stderr.pipe(process.stderr); childProcess.on("close", (code) => { if (code === 0) { resolve(); } else { reject(`错误:${code}`); } }); }); }; module.exports = { spawnCommand, };
执行 ljy create demo-repo
:
就可以看到已经将代码拉到了本地了!
5.4 命令行 loading 效果
ora
使用非常简单,可以直接看下面的案例。更多使用: ora 文档,利用 ora
来实现一个简单的命令行 loading
效果。
先安装 ora:
npm i ora@5.4.0
然后在 acrtions.js
中使用:
// ... const ora = require("ora"); // ... // 定义一个loading const gitRepoSpinner = ora("Downloading github repo, please wait a while..."); // callback ---> promisify ---> Promise ---> async await const createProjectActions = async (project, others) => { console.log(chalk.green.underline.bold("> Start download repo...")); gitRepoSpinner.start(); // 1,clone项目 await download(reactRepo, project, { clone: true }); gitRepoSpinner.succeed(); // ... }; module.exports = createProjectActions;
重新执行 ljy create demo-repo
,就可以看到控制台有了 loading
效果:
5.5 ASCII 的艺术字
figlet 模块可以将 text
文本转化成生成基于 ASCII
的艺术字。
先安装依赖:
npm install figlet
然后在 actions.js
中使用:
// ... var figlet = require("figlet"); // ... // callback ---> promisify ---> Promise ---> async await const createProjectActions = async (project, others) => { console.log( "\r\n" + figlet.textSync("LJY-CLI", { font: "Big", horizontalLayout: "default", verticalLayout: "default", width: 80, whitespaceBreak: true, }) + "\r\n" ); // ... }; module.exports = createProjectActions;
重新执行 ljy create demo-repo
,就会看到
是不是有那味儿了!figlet
提供了多种字体,可以去官网选择你喜欢的字体。
6、发布脚手架
- 登录npm账号
- 在终端
npm login
,输入账户密码 npm publish
就这么简单,然后你就可以在 npm 官网看到你的 package
了!
这是我的:
- npm package:ljy-create-react-app
- github repo:ljy-create-react-app-cli
end~