Appearance
node 搭建 cli

1、准备工作
新建项目目录mkdir demo-cli,并使用npm init 进行初始化,创建bin/index.js文件。
在package.json加入如下命令:
json
{
"bin": {
"demo": "./bin/index.js"
}
}
在你使用 demo 命令的时候,会去执行 bin 文档夹 下的 index.js
js
#!/usr/bin/env node
console.log('hello CLI');
注意: 一定要在开头加上#!/usr/bin/env node,告诉操作系统用 Node 来运行此文档,否则无法运行。
执行npm link,该命令主要做两件事情:
- 一是为 npm 包目录创建软链接,将其链到
{prefix}/lib/node_modules/<package> - 二为可执行文档(bin)创建软链接,将其链到
{prefix}/bin/{name}
将 npm 包链接到全局执行环境,从而在任意位置使用命令行都可以直接运行.
js
/usr/local/bin/demo -> /usr/local/lib/node_modules/demo-cli/bin/index.js
/usr/local/lib/node_modules/demo-cli -> /Users/wangzan/pro/demo-cli
在项目目录下执行demo,会打印出hello CLI.
2、解析命令参数
commander.js,可以自动的解析命令和参数,用于处理用户输入的命令。
安装npm i commander -S
index.js 定义 init 命令:
js
#!/usr/bin/env node
const program = require('commander');
program
.version('1.0.0', '-v, --version')
.command('init <name>')
.action((name) => {
console.log(name);
});
program.parse(process.argv);
调用 version('1.0.0', '-v, --version')会将 -v 和 --version 添加到命令中,可以通过这些选项打印出版本号。
调用 command('init <name>')定义 init 命令,name 则是必传的参数,为项目名。action() 则是执行 init 命令会发生的行为,要生成项目的过程就是在这里面执行的,这里暂时只打印出 name。
其实到这里,已经可以执行 init 命令了。我们来测试一下,在 demo-cli 的同级目录下执行: demo init HelloWorld
可以看到命令行工具也打印出了 HelloWorld,那么很清楚, action((name) => {})这里的参数 name,就是我们执行 init 命令时输入的项目名称。
命令已经完成,接下来就要下载模板生成项目结构了。
3、下载模板
download-git-repo,支持从 Github、Gitlab 和 Bitbucket 下载仓库,各自的具体用法可以参考官方文档。
js
#!/usr/bin/env node
const program = require('commander');
const download = require('download-git-repo');
program
.version('1.0.0', '-v, --version')
.command('init <name>')
.action((name) => {
download(
'https://github.com:zantop/vue-ssr-demo',
name,
{ clone: true },
(err) => {
console.log(err ? 'Error' : 'Success');
},
);
});
program.parse(process.argv);
download() 第一个参数就是仓库地址,注意格式仓库域名:用户名/仓库名 ,第二个参数是 clone 到的文件夹的名字。
demo init vue-ssr
会看到 demo 目录下,远程仓库vue-ssr-demo会下载到文件夹vue-ssr下。
4、命令行交互
inquirer.js命令行交互功能可以在用户执行 init 命令后,向用户提出问题,接收用户的输入并作出相应的处理。这里使用 来实现。
js
#!/usr/bin/env node
const program = require('commander');
const download = require('download-git-repo');
const inquirer = require('inquirer');
program
.version('1.0.0', '-v, --version')
.command('init <name>')
.action((name) => {
inquirer
.prompt([
{
type: 'input',
name: 'author',
message: '请输入作者名称',
},
])
.then((answers) => {
console.log(answers.author);
});
});
program.parse(process.argv);
问题就放在 prompt() 中,问题的类型为 input 就是输入类型,name 就是作为答案对象中的 key,message 就是问题了,用户输入的答案就在 answers 中,使用起来就是这么简单。
inquirer.js —— 一个用户与命令行交互的工具
5、修改 package.json
js
#!/usr/bin/env node
const fs = require('fs');
const program = require('commander');
const download = require('download-git-repo');
const inquirer = require('inquirer');
const chalk = require('chalk');
const symbols = require('log-symbols');
program
.version('1.0.0', '-v, --version')
.command('init')
.action((name) => {
inquirer
.prompt([
{
name: 'name',
type: 'input',
message: '项目名称',
validate: (value) => {
if (value === '') {
console.log(symbols.error, chalk.red('请输入项目名称'));
return false;
}
if (fs.existsSync(value)) {
// 错误提示项目已存在,避免覆盖原有项目
console.log(symbols.error, chalk.red('项目已存在'));
return false;
}
return true;
},
},
{
name: 'version',
type: 'input',
message: '项目版本号',
},
{
name: 'description',
type: 'input',
message: '项目描述',
},
{
name: 'author',
type: 'input',
message: '开发者',
},
{
name: 'template',
type: 'list',
message: '选择模板类型',
choices: ['Vue', 'React'],
},
])
.then((answers) => {
fs.mkdirSync(answers.name);
download(
'https://github.com:zantop/react-cnode',
answers.name,
{ clone: true },
(err) => {
console.log(err ? 'Error' : 'Success');
const fileName = `${answers.name}/package.json`;
const content = JSON.parse(fs.readFileSync(fileName).toString());
content.name = answers.name;
content.version = answers.version;
content.author = answers.author;
content.description = answers.description;
fs.writeFileSync(
fileName,
JSON.stringify(content, null, '\t'),
'utf-8',
);
},
);
});
});
program.parse(process.argv);
6、美化输出
log-symbols,可以在终端上显示出 √ 或 × 等的图标。
chalk,可以给终端的字体加上颜色。
ora,下载过程久的话,可以用于显示下载中的动画效果。
在用户输入答案之后,开始下载模板,这时候使用 ora 来提示用户正在下载中。
js
const ora = require('ora');
// 开始下载
const spinner = ora('正在下载模板...');
spinner.start();
// 下载失败调用
spinner.fail();
// 下载成功调用
spinner.succeed();
然后通过 chalk 来为打印信息加上样式,比如成功信息为绿色,失败信息为红色,这样子会让用户更加容易分辨,同时也让终端的显示更加的好看。
js
const chalk = require('chalk');
console.log(chalk.green('项目创建成功'));
console.log(chalk.red('项目创建失败'));
除了给打印信息加上颜色之外,还可以使用 log-symbols 在信息前面加上 √ 或 × 等的图标
js
const chalk = require('chalk');
const symbols = require('log-symbols');
console.log(symbols.success, chalk.green('项目创建成功'));
console.log(symbols.error, chalk.red('项目创建失败'));
新建template.json,选择不同的模板的时候,从不同的仓库下载。 完整示例:
js
#!/usr/bin/env node
const fs = require('fs');
const program = require('commander');
const download = require('download-git-repo');
const inquirer = require('inquirer');
const chalk = require('chalk');
const ora = require('ora');
const symbols = require('log-symbols');
const template = require('../template.json');
program
.version('1.0.0', '-v, --version')
.command('init')
.action((name) => {
inquirer
.prompt([
{
name: 'name',
type: 'input',
message: '项目名称',
validate: (value) => {
if (value === '') {
console.log(symbols.error, chalk.red('请输入项目名称'));
return false;
}
if (fs.existsSync(value)) {
// 错误提示项目已存在,避免覆盖原有项目
console.log(symbols.error, chalk.red('项目已存在'));
return false;
}
return true;
},
},
{
name: 'version',
type: 'input',
message: '项目版本号',
},
{
name: 'description',
type: 'input',
message: '项目描述',
},
{
name: 'author',
type: 'input',
message: '开发者',
},
{
name: 'template',
type: 'list',
message: '选择模板类型',
choices: ['vue', 'react'],
},
])
.then((answers) => {
console.log(template[answers.template]);
fs.mkdirSync(answers.name);
const spinner = ora('正在下载模板...');
spinner.start();
download(
template[answers.template],
answers.name,
{ clone: true },
(err) => {
if (!err) {
spinner.succeed();
const fileName = `${answers.name}/package.json`;
const content = JSON.parse(fs.readFileSync(fileName).toString());
content.name = answers.name;
content.version = answers.version;
content.author = answers.author;
content.description = answers.description;
fs.writeFileSync(
fileName,
JSON.stringify(content, null, '\t'),
'utf-8',
);
console.log(
symbols.success,
chalk.green(
`🤪 项目初始化完成!\ncd ${answers.name}\nnpm install\nnpm run dev\n`,
),
);
} else {
spinner.fail();
console.log(symbols.error, chalk.red('🥺 模板下载失败!'));
}
},
);
});
});
program.parse(process.argv);
7、发布到 npm
坑,切换 npm 源
js
npm config set registry http://registry.npmjs.org
发包 npm publish 失败:
解决方案:npm publish --access public
Commander.js 中文文档
记一次 nodejs 开发 CLI 的过程
基于 node.js 的脚手架工具开发经历
使用 Node.js 开发简单的脚手架工具
手把手教你如何使用 nodejs 编写 cli 命令行