文章目录
关于process.argv
- process.argv 返回一个数组,其中包含node 启动时包含的输入参数
- 数组第一项是node的执行路径,process.execPath。第二项是正在执行的js文件路径,第三项开始是输入的参数。
node 的这个运行时可以进行参数处理,但是开发一个cli程序需要更方便的参数处理方式。
commander.js 是一个比较出名的cli 参数处理库(中文参考文档)
commander参数处理
const program = require('commander')
const getHelp = () => {}
program.name('better-clone') // 命令的名称
.version('0.0.1')// 版本
.option('-v --verbose', 'verbosity that can be increased') // 参数的描述
program.command('clone <source> [description]') //子命令, source必填的参数,description选填的参数
.option('-d --depths <level>', 'git clone depths')// 这个命令的缩写是什么,全称是什么
.description('clone a repository into a newly created directory')
.action((source, destination, cmdObj) => {
console.log(`start clone from ${source} to ${destination} with depth ${cmdObj.depths}`);
})
program.parse(process.argv)
- .option()方法来定义选项,同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(–后面接一个或多个单词),使用逗号、空格或|分隔。
- 通过.command()或.addCommand()可以配置命令,有两种实现方式:为命令绑定处理函数,或者将命令单独写成一个可执行文件
执行时什么都不传会默认展示help命令,自动生成的。
执行结果:
inquirer.js友好的交互
目标:
- 命令过长一步一输入。
- 配置错误给提示,从当前重新输入。
- 对于敏感信息,不留痕迹
inquirer提供灵活的cli 交互方式,包括:
input number confirm list rowlist expand checkbox password editor …
并且兼容windows/OSX/linux, 不关心平台底层实现细节
使用例子:
const inquirer = require('inquirer')
inquirer
.prompt([
/* Pass your questions in here */
{ type: 'input', name: 'username', message: "What's ur name?" },
{
type: 'checkbox',
name: 'gender',
message: "What's ur gender?",
choices: [ 'male', 'female' ]
},
{
type: 'number',
name: 'age',
message: 'How old are u?',
validate: input => Number.isNaN(Number(input))
? 'Number Required!' : true
},
{
type: 'password',
name: 'secret',
message: 'Tell me a secret.',
mask: 'x'
}
])
.then(answers => {
console.log(`Answers are:\n ${JSON.stringify(answers)}`)
})
.catch(error => {
if (error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else when wrong
}
})
chalk 高亮显示
const chalk = require('chalk');
const log = console.log;
// Combine styled and normal strings
log(chalk.blue('\nHello') + ' World' + chalk.red('!\n'));
// Compose multiple styles using the chainable API
log(chalk.blue.bgRed.bold('Hello world!\n'));
// Pass in multiple arguments
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz\n'));
// Nest styles
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!\n'));
效果:
shelljs、execa调用其他程序
shelljs
Shelljs是Node.js下的脚本语言解析器,具有丰富且强大的底层操作(Windows/Linux/OS X)权限
本质就是基于node的一层命令封装插件,让前端开发者可以不依赖linux也不依赖类似于cmder的转换工具,而是直接在我们最熟悉不过的javascript代码中编写shell命令实现功能。可以同步获得命令结果
var shell = require('shelljs');
if (!shell.which('git')) {
shell.echo('Sorry, this script requires git');
shell.exit(1);
}
// Copy files to release dir
shell.rm('-rf', 'out/Release');
shell.cp('-R', 'stuff/', 'out/Release');
// Replace macros in each .js file
shell.cd('lib');
shell.ls('*.js').forEach(function (file) {
shell.sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
shell.sed('-i', /^.*REMOVE_THIS_LINE.*$/, '', file);
shell.sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, shell.cat('macro.js'), file);
});
shell.cd('..');
// Run external tool synchronously
if (shell.exec('git commit -am "Auto-commit"').code !== 0) {
shell.echo('Error: Git commit failed');
shell.exit(1);
}
execa
- 结果promise化
- 跨平台支持shebang
- 获取进程结束信号
- 优雅退出
- 更好的windows支持,比如路径反斜杠
const execa = require('execa');
(async () => {
const {stdout} = await execa('echo', ['unicorns']);
console.log(stdout);
//=> 'unicorns'
})();
什么是shebang?
告诉bash解释器要用node 去执行而不是shell
创建一个脚手架的demo
需求:设计一个脚手架,根据命令选择不同的模板,按照指定的参数(工程名、作者名)在指定的路径生成一个样板工程
拆解需求:
- 参数的输入,结果的输出:commander.js inquirer.js chalk.js
- 模板在哪里维护:git仓库
- 如何获取模板:git clone 使用execa或者shell.js调用
#! /usr/bin/env node
/*
创建vue或者react的模板,并推送到指定远程仓库
1、交互选择模板类型
2、拉取模板代码到指定路径
3、根据输入的工程名字、作者名字修改package.json文件
4、推送代码到远程仓库
*/
const program = require('commander')
const chalk = require('chalk');
const inquirer = require('inquirer')
const shell = require('shelljs');
const fs = require('fs');
const path = require('path');
const log = console.log;
const inputConfig = [
{
type: 'input',
name: 'path',
message: "What's your output path?",
},
{
type: 'input',
name: 'packageName',
message: "package name:",
validate: function (v) {
return v !=='' && typeof v === 'string'
}
},
{ type: 'input', name: 'author', message: "author:" },
{ type: 'input', name: 'description', message: "description:" },
{
type: 'list',
name: 'type',
message: "choose a template type:",
choices: [ 'vue', 'react' ]
}
]
// 模板仓库的地址
const repositoryAddress = {
vue: 'vue-template.git 地址',
react: 'react-template.git 地址'
}
// 拉取代码
const cloneRepository = (repositoryPath, targetPath) => {
log(chalk.blue('clone start ...'))
//判定git命令是否可用
if (!shell.which('git')) {
shell.echo('Sorry, this script requires git');
shell.exit(1);
}
// 执行git clone
if (shell.exec(`git clone ${repositoryPath} ${targetPath}`).code !== 0) {
shell.echo(`Error: Git clone ${repositoryPath} failed`);
shell.exit(1);
}
log(chalk.blue('clone completed ...'))
}
// 修改package.json
const updatePkg = (res) => {
// 没有指定路径,用默认路径
const originName = res.type === 'vue'? 'vue-template' : 'react-template';
res.path = res.path?res.path: originName;
// 当前路径
let currentPath = process.cwd();
// 处理文件
shell.cd(path.resolve(currentPath, res.path) )
let _packageJson = JSON.parse(fs.readFileSync('./package.json') || {})
_packageJson.name = res.packageName;
_packageJson.author = res.author;
_packageJson.description = res.description;
fs.writeFileSync('./package.json', JSON.stringify(_packageJson, null, 2), function (err) {
if (err) log(chalk.red(err));
});
shell.cd(currentPath)
log(chalk.blue('修改package.json文件完毕 ...'))
}
// 提交到远程仓库
const pushRepository = (res) => {
// 当前路径
let currentPath = process.cwd();
// 处理文件
shell.cd(path.resolve(currentPath, res.path) )
log(chalk.yellow(process.cwd()))
log(chalk.blue('开始推送仓库 ...'))
//判定git命令是否可用
if (!shell.which('git')) {
shell.echo('Sorry, this script requires git');
shell.exit(1);
}
if (shell.exec('git init').code !== 0) {
shell.echo(`Error: [git init] failed`);
shell.exit(1);
}
if (shell.exec('git add .').code !== 0) {
shell.echo(`Error: [git add .] failed`);
shell.exit(1);
}
if (shell.exec(`git commit -m "init"`).code !== 0) {
shell.echo(`Error: [git commit -m "init"] failed`);
shell.exit(1);
}
if (shell.exec(`git remote remove origin`).code !== 0) {
shell.echo(`Error: [git remote remove origin] failed`);
shell.exit(1);
}
if (shell.exec(`git remote add origin ssh://git@xxx.com/${res.packageName}.git`).code !== 0) {
shell.echo(`Error: [git remote add origin ssh://git@xxx.com/${res.packageName}.git] failed`);
shell.exit(1);
}
if (shell.exec('git push -u origin master').code !== 0) {
shell.echo(`Error: [git push -u origin master] failed`);
shell.exit(1);
}
log(chalk.blue('推送仓库完毕 ...'))
shell.cd(currentPath)
}
const generateProject = (res, repositoryAddress) => {
cloneRepository(repositoryAddress, res.path)
updatePkg(res);
pushRepository(res);
}
program
.name('create-test-cli')
.version('1.0.5')
.description('创建vue或者react的模板,并推送到指定远程仓库')
.command('create')
.action(()=> {
inquirer
.prompt(inputConfig).then(res => {
switch (res.type) {
case "vue":
generateProject(res, repositoryAddress.vue);
break;
case "react":
generateProject(res, repositoryAddress.react);
break;
default:
break;
}
})
.catch(error => {
log(chalk.red(error))
})
})
program.parse(process.argv)
notes:
package.json
需要添加执行路径:
"bin": {
"create-test-cli": "./index.js"
},
npm publish
后:
安装:
npm i create-test-cli
使用:
create-test-cli create
- output path 为仓库克隆下来的路径,规范符合git clone 的规范, 不传时在当前目录生成vue-template 或者react-template
- name 必传, 为远程仓库名称,也是package.json 中的name