Skip to content

Node CLI工具原理解析

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本文将主要介绍CLI相关周边知识,通过本文读者可以了解到CLI的基本工作原理注册全局指令的几种方式、Node CLI的基本工作原理

前言

CLI(Command-Line Interface) 命令行界面

搞开发的同学,或多或少的都会接触到许多的命令行工具。

有生产力工具,也有有意思的小玩意、自动化任务处理等等。

命令行工具的安装方式就很多了。

win上大部分是通过软件安装包安装,安装同时会通过环境变量配置相关指令。

linuxmac上就比较丰富了,前者常用yumapi-get、mac 上就brew

也有使用wgetcurl拉取相关工具的shell脚本执行安装。

说了这么多工具,都不是本文要讲的工具,前端搬砖当然首选node,然后基于npm做包的分发。

PS:文中的示例都以mac为主

可执行shell

unix系上大部分可执行文件都是基于shell的脚本。

比如随手写个hello world

文件名hello,内容如下

sh
echo "Hello world"

图片

此时我们直接执行是会提醒没有执行权限,我们为当前用户加1个可执行权限

sh
chmod u+x hello

然后再当前目录执行,就看到输出结果了

sh
./hello

图片

注册全局指令

为了使“指令”在全局任意位置都能被使用,就需要做相关操作了。

环境变量

相信大多数首次接触这个词的朋友都在win上深有体会。装JDKMySQL时都避免不了有配置的操作。

如果想在其它目录直接执行hello就生效呢?那这就离不开环境变量配置了

咱们先看终端用的shell工具是什么。

sh
echo $0

我这里使用的是zsh,其它的常见的还有bash

图片

相应的配置文件分别是.zshrc.bashrc

图片

alias指令

使用 alias指令设置别名

指令格式

sh
alias <>=<指令或可执行文件路>

添加内容如下

sh
alias hello=/Users/sugar/Documents/diy-cli/hello

立即生效配置

sh
source ~/.zshrc

export指令

使用export命令添加添加相关目录

指令格式

sh
export PATH=$PATH:<路径 1>:<路径 2>:<路径 N>

添加内容如下

sh
export PATH=$PATH:/Users/sugar/Documents/diy-cli

以上2种方案都能达到目标的效果

图片

如果每个工具都单独配一条规则。那会导致相关配置文件非常的庞大,也不方便维护。

实际上我们在用npm i -g安装的全局包的时候,并没有手动配置。那么这个是如何做到的呢。这个就离不开下面将要说到的符号链接

符号链接

软链接类似于快捷方式,它可以指向任意文件系统中的一个文件或目录;硬链接也可以看作是文件或目录的快捷方式,但源文件删除了也不影响硬链接

先通过which npm看一下npm所在位置

打印一下$PATH的值,可以看到npm指令对应文件所在目录就在其中

图片

展开目录内容可以看到文件类型都是l(软连接)

图片

因此咱们可以小结出来 通过向已添加到$PATH变量中的目录,直接创建短链可以实现指令的自动注册全局

下面实践演示一下

ln指令

指令格式

sh
# 硬链接
ln source target
# 软连接
ln -s source target

接着上面之前的例子,再使用export完成对目录的添加后。咱们再随便建立个文件hello2.sh进行操作

内容如下

sh
echo "Hello world2"

创建一个软链

sh
ln -s <source>/hello.sh <target>/hello2

操作结果如下

图片

前面代码都是简单的写的shell脚本

前端当然是羧js,咱们把代码改成js。

hello.js

js
console.log('hello js')

按照前面的步骤,完成可执行权限添加和软链的创建。

图片

结果可以预测是会报错的,默认会被当做shell脚本进行执行。

那么如何指定为使用node去执行这个文件?

这就是我们下文要说到的hashbang

Hashbang

Hashbang(也称为Shebang)是一个由井号和叹号构成的字符序列 #!,通常出现在文件开头,例如 #!/usr/bin/env bash

用于指定脚本的运行环境

于是,我们给前面的hello.js头部加上#!/usr/bin/env node 再次运行就成了

图片

至此基本清楚了,如何将1个js脚本便捷的注册为1个全局可执行指令

Node CLI

node官配包管理工具npm,通常每个项目中有一个package.json文件,用于描述项目的一些信息或者包含项目相关的配置内容

指令注册

其中bin属性用于设置指令名称执行脚本所在位置

json
{
    "name":"pkgName",
    "bin": {
        "command": "exec/filepath.js"
    }
}

使用npm install安装依赖,会根据bin中的描述,创建1个commandexec/filepath.js的软链

软链所在目录区别于是否是global安装

这个目录可以通过npm bin指令查看

图片

全局路径和前面使用 which npm获取的一致,当前项目的路径即在node_modules/.bin

如果是本地开发CLI时,可以使用npm link指令根据bin描述信息,自动创建软链到npm bin所示的目录中,通过-g参数区别是否是全局

sh
# 项目工作目录下执行
npm link
# or
npm link -g

命令行参数

前面主要都在围绕命令展开介绍。要实现工具的丰富功能离不开参数的组合,本小节就介绍下Node里如何处理CommandOptions

我们可以通过process.argv方法获取到运行时的 命令行入参

js
console.log(process.argv);

图片

各位置参数释义

  • 0:Node可执行文件所在路径
  • 1:执行的js脚本路径
  • >1:用户运行时传入的参数

通过这些参数,就能区分出用户要执行的行为

当然在实际开发中大部分场景下,都会使用第三方库去解析命令行参数,来降低代码的复杂度,提高可读性。

下面是一个使用commander的例子

js
#!/usr/bin/env node

const { Command } = require('commander')
const pkg = require('./package.json')

const program = new Command()
program.version(pkg.version)

program
    .command('hello [paths...]')
    .description('hello world demo')
    .alias('h')
    .option('-p, --pkg <path>', 'set package.json path')
    .action((paths, options) => {
        console.log('😄😄😄');
        console.log(paths);
        console.log(options);
    })

program.parse(process.argv)

图片

可以看到使用第三方库辅助处理参数,已经非常完善了

除了老牌的commander之外还有其它的相同作用的库,这里就不展开介绍了。

彩色打印

这个大家都不陌生了,大部分CLI打印结果都是五颜六色

比如下面的例子

sh
echo 'hello  world'

图片

相关知识点是ANSI Escape code,这里就不展开说明了。

实际开发中,也很少直接写这种原始的数值。通常会使用chalk这个库辅助,比如上面这个颜色对应代码如下。

js
const Chalk = require('chalk');

console.log(Chalk.cyan('hello world'));

渐变色打印就常用gradient-string这个库

js
const gradient = require('gradient-string');

console.log(gradient('cyan', 'pink')('Hello world!'));

图片

简单两行代码效果就出来了

终端交互

在使用 例如Vue CLI 此类工具进行项目初始化的时候,会有输入单选多选等交互操作。

相关原理涉及内容太“抽象”,篇幅较大,后续通俗精简了再做分享

常用的第三方库就是inquirer这个库

下面是简单checkbox示例

js
const inquirer = require('inquirer');

inquirer
    .prompt([
        {
            type: 'checkbox',
            message: '水果选择',
            name: 'fruits',
            choices: [
                {
                    name: '🍌',
                },
                {
                    name: '🍉',
                },
                {
                    name: '🍇',
                },
            ]
        },
    ])
    .then((answers) => {
        console.log(answers);
    });

图片

最后

本文没有阐述非常深奥的知识点,只涉及日常的一些基操,有助于读者了解Node CLI 背后的工作原理。

如内容有不妥之处,可以评论区交流;有感兴趣希望深入了解的知识点也可评论区@。

完整示例代码移步=>Github