命令行工具是什么?
命令行工具是一个可执行脚本或者可执行文件。
平时开发中用到的 create-react-app
@vue/cli
vitejs
等都是命令行工具,这些命令行工具一般都安装到全局的/usr/local/bin
里面(注:类Unix环境)。打开/usr/local/bin
应该会看到平常安装过的nodejs全局命令行工具。
命令行文件与普通文件的区别
命令行文件需要在第一行指定一下"脚本文件的解释器" #!/usr/bin/env node
这句话是指定用nodejs作为解释器,执行当前文件下面的内容。
命令行是如何发布到全局中的
- 在package.json中的 bin中声明需要编写的命令行名称,比如下面的示例声明了一个 command-demo命令
json
{
"name": "command-demo",
"version": "0.0.1",
"description": "command-demo",
"bin": {
// 这里声明命令行名称
"command-demo": "./bin/index.js"
}
...
}
- 如果是本地开发或者测试 可以先执行
npm link
发布到本地全局,npm link
执行的位置是package.json同级的目录。 然后执行npm link <package name>
安装到其它目录。安装到其它目录的时候是需要在其它目录package.json同级目录执行。注意 package name是package.json中name
对应的值。 - 如果想发布到npm库 中可以先执行
npm publish
具体可以参考 npm publish; 发布成功之后,可以通过npm 或者yarn直接安装到全局中就可以直接使用这个命令行功能了 - bin中声明的命令并不是只可以安装到全局中,也可以安装到某一个项目中,安装之后会在node_moudles/.bin/这个目录中生成对应的命令。安装到某个项目中的的命令可以
./node_modules/.bin/<command>
这样执行
- 命令行对应的文件,比如上面例子中
./bin/index.js
,并不是一定要放到 ./bin 目录中,但是是推荐放到./bin目录中。
实践一下: 创建一个最简单的命令行
命令行功能描述:执行command-demo
的时候输出Hello World
js
#!/usr/bin/env node
console.log(`Hello World`);
按照上面介绍的方法
实现一个创建基础模版的脚手架
分析涉及的主要功能:
创建基础模版有两种方式:
- 通过远程下载。利用
download-git-repo
这个工具可以实现拉取远程的文件,然后下载到指定位置。这样的好处是远程文件修改之后,脚手架可以直接拉取最新的修改内容。坏处就是这个git库权限需要所有人访问。否则拉取失败。 - 本地拷备的方式。大概意思是脚手架包含需要的模版文件。这样安装脚手架的时候模版文件也会安装到本地。这样创建的时候只要拿到对应的文件直接拷备到指定的位置就可以了。
参数解析及help介绍:
脚手架肯定是少不了参数的比如vue-cli-service serve
vue-cli-service build
这样脚手架可以根据不同的参数执行不同的方法。这里借助commander
的能力。
介绍一下 commander
commander
可以帮助我们编写命令行。主要功能是将参数解析为选项(option)和命令参数(command),并且当命令行使用错误的时候提示错误,以及帮助系统。
比如 一个命令选项 --template {name}
这个选项必须在--template 后面跟一个name,如果不加的话就需要提示错误。
option用法
option主要用法是支持命令行传参数,一种参数是不需要对应的值,一种参数是必须要传值。
比如:我们想实现一个功能,比如命令行中包括 --info,-i 的时候去打印一些信息
//
#!/usr/bin/env node
const { program } = require("commander");
program
// 不需要对应的值
.option("-i, --info")
.option("-t, --tpl <string>");
// 解析,这一步很不重要,不能去掉
program.parse();
const options = program.opts();
// 输出解析之后的结果
console.log(options, " options");
上面的脚本可以这样执行:
- 只包含 info参数
node ./command.js -i
或者node ./command.js --info
- 只包含 tpl 参数
node ./command.js -t {模板名}
或者node ./command.js -tpl {模板名}
,注意上面的{模板名}不能省略 - 既包含 info 也包含 tpl参数。
node ./command.js -i -t {模板名}
或者node ./command.js -t {模板名} -i
上面的-i
或者-t
可以替换成--info
或者--tpl
。需要注意的:-t 后面的 {模板名}不能省略且只能放到 -t或者--tpl紧后面。
options配置的参数,可以通过program.opts() 返回的对象来获取内容。
command、description、arguments
command用来自义一个命令,然后arguments是定义参数的并且不能省略。description是用来描述当前命令。
javascript
const { program } = require("commander");
program
.command("split")
.description("Split a string into substrings and display as an array")
.argument("<string>", "string to split")
.option("--first", "display just the first substring")
.option("-s, --separator <char>", "separator character", ",")
.action((str, options) => {
const limit = options.first ? 1 : undefined;
console.log(str, " str");
console.log(options, " options");
console.log(str.split(options.separator, limit));
});
program.parse();
以上定义了一个split命令,参数是string;可选参数是 --first、--separator;具体操作在action里面。 注意:每个command对应一个action,写多个也只有最后一个生效。
action
action是对应command的操作。action的参数是一个函数。这个函数包含三个参数,第一个对应的是argument,第二个对应option,它是一个对象,对象的属性就是option的key,第三个就是program对象,包含对象的所有信息。
有了以上的知识储备,在开发一个脚手架工具就很容易了
javascript
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const { program } = require("commander");
function copyDirectoryContents(source, destination) {
// 读取源目录中的文件和子目录
const files = fs.readdirSync(source);
// 遍历源目录中的文件和子目录
files.forEach((file) => {
const sourcePath = path.join(source, file);
const destinationPath = path.join(destination, file);
// 判断当前项是文件还是目录
const isDirectory = fs.statSync(sourcePath).isDirectory();
if (isDirectory) {
// 如果是目录,则递归地拷贝目录中的内容
fs.mkdirSync(destinationPath);
copyDirectoryContents(sourcePath, destinationPath);
} else {
// 如果是文件,则直接拷贝文件
fs.copyFileSync(sourcePath, destinationPath);
}
});
}
program
.command("create")
.description("创建一个项目")
.argument("<string>", "项目名称")
.option("-t, --tpl <char>", "选择模版技术栈,默认vue", "vue")
.action((str, options, rest) => {
console.log(str, " str");
console.log(options, " options");
// 获取要创建项目的目录地址
const directoryPath = `${process.cwd()}/${str}`;
//目录不存在的时候创建目录
if (!fs.existsSync(directoryPath)) {
try {
console.log("开始创建项目目录");
fs.mkdirSync(directoryPath, { recursive: true });
console.log("项目目录创建成功");
//把内容拷到创建的目录中
// vue项目
if (options.tpl === "vue") {
copyDirectoryContents(`${__dirname}/../vue`, directoryPath);
// react项目
} else {
copyDirectoryContents(`${__dirname}/../react`, directoryPath);
}
} catch (err) {
console.error("无法创建目录", err);
}
} else {
throw new Error(`项目${str}已经存在`);
}
});
program.program.parse();
以上就实现了一个最简单的脚手架工具。目录结构如下:
当然一个完善的脚手架还有很多功能,比如判断当前是否有比较新的版本,检查输入内容等。