【npm】搞个自己的CLI拉取基础工程
一、CLI原理
CLI
(command-line interface
— 命令行界面):commander
这个node
库,有很多cli
使用了它,它可以帮助开发者简化实现命令流程。不过我们的第一步还是应该先搞清楚,如何通过npm
如何实现命令行。(其实就和普通的bat
命令一样)
一个简单的cli
:
①package.json
中有一个 bin
字段,指定各个内部命令对应的可执行文件的位置。
②在包安装时,如果是全局安装,npm
将会把 package.json
里定义的 bin
文件软连接到全局 node_modules/bin
③如果是非全局安装,会软链接到项目文件夹./node_modules/.bin/
。
④根据下面代码的配置,当我们全局安装此包后,在任意位置运行 cli-test
,都会执行全局 node_modules
中的 cli-test
文件。
/*cli-test 的 package.json*/
"bin": {
"cli-test": "./bin/cli-test"
}
cli
文件:
下面的第一行必写,是告诉操作系统这个文件中的代码用node
可执行程序去运行它。 后面就做我们要做的事情就行了 。
#!/usr/bin/env node
//do something
一个简单的cli就完成了
其实有很多三方cli
也并没有用类似 commander
的 node
库,如果我们的 cli
足够简单,以上这样就可以了。
二、commander:node.js
命令行界面的完整解决方案
1. Commander.js中文文档链接一
2. Commander.js中文文档链接二
3. 仿vue的前端自定义cmd命令拉取项目脚手架
那下面开练吧! 我们先来看到脚手架工程中可能会用到的js~
A. commander.js:
//安装
npm install commander --save
//调用
const program = require("commander");
B. chalk.js:修改控制台中字符串的样式【打印日志的时候,根据日志级别的不同,显示不同的颜色】,包括:①字体样式(加粗、隐藏等);②字体颜色;③背景颜色。 创建log.js
如下:
//安装
npm install chalk --save-dev
//调用
const chalk = require('chalk');
const log = console.log;
const success = function (msg) {
log(chalk.bgGreen(' SUCCESS ') + chalk.green(' ' + msg));
}
const warn = function (msg) {
log(chalk.bgYellow(' WARN ') + chalk.yellow(' ' + msg));
}
const error= function (msg) {
log(chalk.bgRed(' ERROR') + chalk.red(' ' + msg));
}
const info = function (msg) {
log(chalk.bgBlackBright(' INFO ') + chalk.gray(' ' + msg));
}
const tip = function (msg) {
log(chalk.bgBlue(' TIP') + chalk.blue(' ' + msg));
}
module.exports = { success, warn, error, info, tip }
C. fs-extra.js:文件操作相关工具库,该模块是系统fs
模块的扩展,提供了更多便利的 API
,并继承了fs
模块的 API
//安装
npm install --save-dev fs-extra
//调用
const fs = require("fs-extra");
相关API
了解下:
a. 文件拷贝:copy(src, dest, [option],callback);【copySync()】
/**
option对应:
①clobber (boolean): 覆盖现有的文件或目录,默认true
②dereference (boolean): dereference symlinks, default is false
③preserveTimestamps (boolean): 最后修改和访问时间和原始的源文件一致,默认为false
④filter: 函数或正则表达式过滤复制文件,返回true包含,否则排除
*/
fs.copy('/tmp/myfile', '/tmp/mynewfile', function (err) {
if (err) return console.error(err);
console.log("success!")
}) //拷贝文件
fs.copy('/tmp/mydir', '/tmp/mynewdir', function (err) {
if (err) return console.error(err)
console.log('success!')
}) //拷贝目录
============================================
b. 清空目录:emptydir() 【emptyDirSync(), emptydirSync()】
/**
确保一个目录是空的。如果目录非空删除目录内容。
如果目录不存在,就创建一个。目录本身并不是删除。
*/
fs.emptyDir('/tmp/some/dir', function (err) {
if (!err) console.log('success!')
})
============================================
c. 创建文件:ensureFile() 【createFileSync(),ensureFileSync()】
/**
确保文件存在。如果被请求的文件的目录不存在,创建这些目录。
如果文件已经存在,它不修改。
*/
var file = '/tmp/this/path/does/not/exist/file.txt';
fs.ensureFile(file, function (err) {
console.log(err) ;
})
=============================================
d.创建目录:ensureDir() 【ensureDirSync()】
/**
确保目录的存在。如果目录结构不存在,就创建一个
*/
var dir = '/tmp/this/path/does/not/exist';
fs.ensureDir(dir, function (err) {
console.log(err);
})
D. inquirer.js:如果想自己做一个脚手架或者在某些时候要与用户进行交互,这个时候就不得不提到了这个库了。
由于交互的问题种类不同,inquirer为每个问题提供很多参数:
type:表示提问的类型,包括:input, confirm, list, rawlist,
expand, checkbox, password, editor
name: 存储当前问题回答的变量;
message:问题的描述;
default:默认值;
choices:列表选项,在某些type下可用,并且包含一个分隔符(separator);
validate:对用户的回答进行校验;
filter:对用户的回答进行过滤处理,返回处理后的值;
transformer:对用户回答的显示效果进行处理
(如:修改回答的字体或背景颜色),但不会影响最终的答案的内容;
when:根据前面问题的回答,判断当前问题是否需要被回答;
pageSize:修改某些type类型下的渲染行数;
prefix:修改message默认前缀;
suffix:修改message默认后缀。
案例如下:
const promptList = [{
type: 'input',
message: '设置一个用户名:',
name: 'name',
default: "test_user" // 默认值
},{
type: 'input',
message: '请输入手机号:',
name: 'phone',
validate: function(val) {
if(val.match(/\d{11}/g)) { // 校验位数
return val;
}
return "请输入11位数字";
}
},{
type: "confirm",
message: "是否使用监听?",
name: "watch",
prefix: "前缀"
},{
type: "confirm",
message: "是否进行文件过滤?",
name: "filter",
suffix: "后缀",
when: function(answers) { // 当watch为true的时候才会提问当前问题
return answers.watch
}
},{
type: 'list',
message: '请选择一种水果:',
name: 'fruit',
choices: [
"Apple",
"Pear",
"Banana"
],
filter: function (val) { // 使用filter将回答变为小写
return val.toLowerCase();
}
},{
type: "expand",
message: "请选择一种水果:",
name: "fruit",
choices: [
{
key: "a",
name: "Apple",
value: "apple"
},
{
key: "O",
name: "Orange",
value: "orange"
},
{
key: "p",
name: "Pear",
value: "pear"
}
]
},{
type: "checkbox",
message: "选择颜色:",
name: "color",
choices: [
"red",
"blur",
"green",
"yellow"
],
pageSize: 2 // 设置行数
},{
type: "password", // 密码为密文输入
message: "请输入密码:",
name: "pwd"
},{
type: "editor",
message: "请输入备注:",
name: "editor"
}]
E. execa:据称是更好的子进程管理工具。执行后续的示例,先执行npm install --save execa
const execa = require("execa");
execa("ls").then(result => console.log(result.stdout));
F.get-stream:Get a stream as a string, buffer, or array先执行npm install get-stream
//检查NodeJs版本
const execa = require('execa');
const getStream = require('get-stream');
const stream = execa('node', ['--version']).stdout;
return getStream(stream).then(data => {
})
G. download-git-repo:Download and extract a git repository (GitHub, GitLab, Bitbucket) from node. 若我们CLI
中的具体基础工程是从git
上下载的,那就需要用到该库了。
Vue-CLI
的vue-init
中就有调用了该函数
若我们自己的脚手架中包含了基础工程,则执行命令时拷贝基础工程到我们创建的初始工程中即可。
image.pngH. vue-cli
中用到的其他一些库
user-home:【Get the path to the user home directory
— 获取用户主目录的路径】
tildify【Convert an absolute path to a tilde path: /Users/sindresorhus/dev → ~/dev
— 将绝对路径转换为波形路径】
ora:【Elegant terminal spinner
— 在终端里有显示载入动画】
vue-cli
其他js中还引用了其他一些库,这里就先不介绍了,后面再将vue-cli
源码 Vue-cli 原理分析好好解读下。下图是vue-cli
的一个基本结构。
小结
搭建一个简易的cli
需要:
① 检查node
等的版本号,一般校验不能低于某个版本
② 日志的打印:chalk
③ 搭建过程中的一些提问: inquirer
④ 工程包的拷贝fs-extra
或下载 download-git-repo
⑤ pack.json
的配置
来看下这个最简易cli
的结构吧:
具体的创建过程,上面已提到过,即基础工程的拷贝过程。这里再一次贴一下相关代码吧
image.png相关package.json
也看下吧:
将该cli
发布到npm
中,具体发布过程见将自己的vue组件发布为npm包 & npm私有仓库搭建
发布成功后,安装cli
npm install eui2-cli -g
按装成功后,执行命令
//projectName: 你的工程名
eui2 create <projectName>
创建工程
image.png打开工程目录查看
image.png没错,就是它了