- 你想知道命令行工具是怎么运行的吗?
- 当你在终端输入一句命令
vue create my-project
,按下回车键的时候,发生了什么? - npx 是什么?
- 如何实现一个命令行工具
当我们运行 npx @vue/cli create my-project
创建一个项目时,就是通过 npx
执行了@vue/cli
包中的可执行文件。"可执行文件"就是我们通常说的脚本,许多 npm
包的可执行文件是用 JavaScript
编写的,例如: xx.ts
、xx.mts
、xx.js
等等,使用 Node.js
环境来运行。在 package.json
文件中,npm
包的开发者可以通过 bin
字段来指定可执行文件的路径,命令作为key
,可执行文件路径作为value
。例如 @vue/cli
可执行文件就是bin/vue.js
, 不论是执行 vue create my-project
还是执行 npx @vue/cli create my-project
,运行的都是bin/vue.js
文件的内容,具体的 create
命令是 bin/vue.js
文件中的 create program
。
get it!
现在我们知道了,vue create my-project
命令的运行逻辑,之所以可以创建 my-project 项目,是因为我们全局安装了vue-cli
,通过 package.json 中bin
字段的匹配, vue
命令对应的可执行文件为 bin/vue.js
,所以vue create
执行的就是bin/vue.js
中的 create 程序。npx @vue/cli create my-project
也是一样,只是找可执行文件的方式不同,npx @vue/cli
就会找 vue-cli
中的可执行文件,也就是 package.json中bin
字段的配置的bin/vue.js
,create 同样是执行 bin/vue.js
文件中的 create 程序。
Commander
上面我们提到可执行文件bin/vue.js
中的 create 程序,接下来我们看看 create 程序是什么。
Commander 完整的 node.js 命令行解决方案。如果我们需要写一个命令行工具,肯定会用到它,他可以帮助我们定义命令,以及命令的参数。在这里我们需要简单了解几个它的方法:
command()
通过该方法可以定义命令,比如 my-command.js:
ini
const program = new Command();
program.command('create [projectName]')
这样我们就定义了一个 create 命令,可以通过执行 node my-command.js create (node my-command.js create myProject)
来执行。[projectName] 表示给 create 命令定义了一个可选的参数,如果需要一个必选的参数,需要用 <projectName> 来定义,例如:
arduino
const program = new Command();
program.command('create <projectName>')
option()
该方法用来定义选项,那选项是什么呢?如下,这些 -p、-d、-i 这些就是选项,可以简单地把选项理解为额外的参数。
option() 同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|
分隔。例如:
arduino
const program = new Command();
program.option('-d, --debug', 'output extra debugging')
选项有全局选项和命令选项之分,全局选项是定义在 program
上的选项,可以在任何命令中使用。这里定义的 -d 就是一个全局选项,是直接绑定在program上的,我们可以可以通过执行: node my-command.js -d 来执行。
命令选项是定义在具体命令上,只能在该命令中使用,例如:
arduino
const program = new Command();
program
.command('create [projectName]')
.option('-d, --debug', 'output extra debugging')
这里定义的 -d 就是一个命令选项,是绑定在command上的,我们可以可以通过执行: node my-command.js create -d
来执行。
注意⚠️: 当我们定义了 command
后,commander
会进入"命令模式"。在命令模式下,所有输入都会被解析为命令或命令的选项。
arduino
const program = new Command();
program
.option('-d, --debug', 'output extra debugging')
.command('create [projectName]')
此时,全局选项不可单独使用:node my-command.js -d 🙅;只能这样使用:node my-command.js -d create(node my-command.js create -d)
action()
该方法用来定义处理函数,命令处理函数的参数,为该命令声明的所有参数,除此之外还会附加两个额外参数:一个是解析出的选项,另一个则是该命令对象自身。例如:
javascript
program
.option('-d, --debug', 'output extra debugging')
.command('create [projectName]')
.option('-s, --small', 'small pizza size')
.option('-p, --pizza-type <type>', 'flavour of pizza')
.action((argument, options, cmd) => {
console.log(argument, '====',options, '===', cmd);
});
如果我们执行: node my-command.js create -p cheese 这个命令
可以看到,第一个参数 argument 为 undefined
, options 为 { pizzaType: cheese }, cmd 为该命令对象自身。我们先不管 argument 参数,options 为 { pizzaType: cheese } 是因为我们的命令传入了 -p cheese
选项,没错,Commander 在暴露参数时会将多个单词的长选项名转为驼峰命名法(camel-case)作为 key。因为我们定义了 option('-p, --pizza-type ', 'flavour of pizza') ,所以这里抛出的参数是 { pizzaType: cheese }。
do it !
ok,现在我们知道了命令行工具是如何运行的,以及如何定义的,那现在我们来写一个工具:查找所有 ts 文件中的var
,将其替换为let
。
前置条件
文件: index.ts 、index.html
typescript
class User {
private nameVar: string;
private age_var: number;
constructor(name: string, age: number) {
this.nameVar = name;
this.age_var = age;
}
getUserInfo(): string {
return `用户名: ${this.nameVar}, 年龄: ${this.age_var}`;
}
}
// 创建用户实例的函数
function createUser(): void {
var userName: string = "张三";
var userAge: number = 25;
const user = new User(userName, userAge);
console.log(user.getUserInfo());
}
必备知识
我们这个工具的作用是查找所有ts文件中的var
,将其替换为let
,那我们就要读取文件,我们简单了解一下与操作文件相关的2个模块:fs
和 path
。
fs
模块和 path
模块 是 Node.js 的两个内置模块。
fs
模块(File System)提供了一系列用于与文件系统进行交互的 API,允许你对文件和目录进行读取、写入、修改、删除等操作。我们这里用到 3 个API:
fs.readdirSync(dir)
读取指定目录下的所有文件和子目录的名称,并将这些名称作为一个字符串数组返回fs.readFileSync(path, 'utf8')
用于读取指定文件的全部文件内容fs.writeFileSync(path, result, 'utf8')
将数据写入指定文件
path
模块提供了一些实用工具,用于处理和转换文件路径。我们这里用到path模块的 1 个API:
path.join(dir, fileName)
用于将所有给定的path
片段连接在一起,例如,如果dir
是/users/admin
,file
是example.txt
,那么path.join(dir, file)
将返回/users/admin/example.txt
。它可以确保你得到的路径是正确的,无论你是在 Windows(使用反斜杠 `` 作为路径分隔符)还是在 Unix/Linux(使用正斜杠/
作为路径分隔符)上运行代码。path.join
方法会自动处理这些差异,并返回一个适合当前操作系统的路径。
实现
- 找到 my-vue-project 目录下的所有 js 文件
- 读取 js 文件内容
- 将 js 文件内容,使用正则匹配替换将
var
替换为let
- 将替换后的内容重新写入文件内
ini
const fs = require('fs');
const path = require('path');
const commander = require('commander');
const program = new commander.Command();
program
.command('replace-var')
.option('-p, --path [path]', 'file path')
.action(( options, cmd) => {
const dir = options.path || '.';
// 获取所有文件,过滤出ts文件
const files = fs.readdirSync(dir).filter((filePath) => {
return filePath.endsWith('.ts');
});
files.forEach(file => {
// 拼接完整的文件路径
const fullPath = path.join(dir, file);
// 读取文件内容
const jsCode = fs.readFileSync(fullPath, 'utf8');
// 替换
const result = jsCode.replaceAll(' var ', ' let ');
if (result) {
// 将替换后的内容写入文件内
fs.writeFileSync(fullPath, result, 'utf8');
}
});
});
program.parse(process.argv);
finish~