怎样开发一个 Node.js 命令行工具包

大家好,我是若川。最近组织了源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。

源码共读活动很多都是读的npm包,这篇文章很详细,推荐给大家。


1. 初始化项目

在一个合适的地方创建项目文件夹,为了演示,本次的项目名为 demo-cli,然后执行以下命令初始化项目:

npm init

执行以上命令之后,会先配置一些 package.json 的基础信息,按提示输入即可:

bceb84a167c8cece01c0868a58f1486b.png

1.1 配置 package.json

为了方便,我们把项目从 vscode 中打开,然后对 package.json 进行详细配置,篇幅有限,这里仅介绍其中比较重要的部分:

推荐阅读:package.json 详细配置。

1.1.1 name

项目名,同时也是发包的时候别人引入时的默认名称。

1.1.2 version

版本号,对于项目的每一次升级(引入新特性、打补丁、代码重构等),我们都需要对版本进行升级,遵循 major、minor、patch 原则。

推荐阅读:npm 语义化版本控制。

1.1.3 main

项目入口文件的位置,方便别人引入我们的包的时候,从哪里进行解析,这里也是我们进行接口导出的模块地址,稍后会进行详细介绍。

1.1.4 scripts

脚本指令,在这里可以自定义一些指令。

npm 脚本的原理非常简单。每当执行 npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。

比较特别的是,npm run 新建的这个 Shell,会将当前目录的 node_modules/.bin 子目录加入 PATH 变量,执行结束后,再将 PATH 变量恢复原样。

推荐阅读:npm scripts 使用指南。

1.1.5 bin

我们的项目所提供的自定义指令,以及对应的可执行文件的映射地址:

{..."bin": {"demo-cli": "bin/demo-cli"},...
}

当我们的自定义指令的名字就是项目名称的时候,可以简写为以下形式:

{..."bin": "bin/demo-cli",...
}

1.2 bin 命令是如何运行的

1.2.1 Linux bin 目录的作用

shell 任务的一个重要部分是搜索命令。Bash 是按照下一的步骤来完成的:检查命令是否包含斜杠。如果没有,首先检查函数列表是否包含一个我们寻找的命令。如果命令不是一个函数,那么在内建命令列表中检查。shell 内建命令是指 bash(或其它版本)工具集中的命令。一般都会有一个与之同名的系统命令,比如 bash 中的 echo 命令与 /bin/echo 是两个不同的命令,尽管他们行为大体相仿。当在 bash 中键入一个命令时系统会先看他是否是一个内建命令,如果不是才会查看是否是系统命令或第三方工具。所以在 bash 中键入 echo 命令实际上执行 bash 工具集中的 bash 命令也就是内建命令,而不是 /bin/echo 这个系统命令。备注:Linux 中的 type 命令如果命令既不是函数也不是内建命令,那么扫描列在 PATH 中的目录列表来进行查找。

通常如果我们要在 Linux 中执行自定义脚本,那么我们需要通过路径的形式来执行相应的文件,如果我们在 PATH 里的目录中注册了相应的指令或者通过 alias 对这个路径起了别名的话,就不需要输入完整路径。

linux 或者 MacOS 中的 bin 目录一般是用来存放可执行命令的文件夹,通常有:

  • /bin

  • /sbin

  • /usr/bin

  • /usr/local/bin

  • /usr/sbin

  • ...

要具体了解这些目录里有哪些指令,可以参考这篇文章:bin 目录简单区别

1.2.2 node bin

首先,我们需要找到我们的 node 的安装地址,这个可以通过在 Linux、MacOS 或者 VSCode 的终端里输入一下指令来获得:

echo $PATH

这会打印出当前所配置的环境变量,一般我们安装 node 的时候会自动在 PATH 里加入,node 的可执行脚本的目录地址:

9a77d6db9a1b66bf90889931972eef77.png

如上图所示,其中 “/Users/hopewlliu/.nvm/versions/node/v14.17.3/bin” 便是本电脑 node 中所有所有全局指令所在位置。

以下为当前电脑的全局指令、软连接的指令及其所映射的文件地址:

65b16c201808fd96b3e8ff95c6b6fde9.png

软链的创建方式很简单,比如我们对上图的 imserver 添加一个新的软链 imserver2,可以执行一下指令:

ln -s ../lib/node_modules/@tencent/imserver-cli/bin/imserver ./imserver2

现在我们就可以在全局上使用 imserver2 命令了,他和 imserver 的效果是一致的。

同时想要删除软连接也很简单,只需要执行以下指令即可:

rm ./imserver2

1.2.3 全局安装与非全局安装

1.2.3.1 全局安装

如果我们通过 -g 的形式来安装一个包的话,他会被安装到 node 相关文件夹中,在本文即为:

“/Users/hopewlliu/.nvm/versions/node/v14.17.3/lib/node_modules”

目录下,如果该包的 package.json 中存在 bin 字段的指令配置,同时会在:

“/Users/hopewlliu/.nvm/versions/node/v14.17.3/bin”

目录下创建相应的指令软链。

1.2.3.2 非全局安装

非全局安装的包存在于我们的项目的根目录的 node_modules 目录下,如果该包存在自定义指令,那么会在安装包的时候在当前项目的根目录的 node_modules/.bin 目录下添加相应的自定义指令的软链接,想要执行这个包的自定义指令,我们可以直接通过路径的形式来找到该包指令所在位置然后执行,但是通常的做法是在当前的项目的 package.json 中添加相应的 npm scripts 来执行,原理就是 npm scrpits 在执行的前一刻会开启新的 shell 并把当前项目的根目录的 node_modules/.bin 目录加入 PATH 环境变量中,然后在这个 shell 中执行自定义的脚本指令,并在执行完成之后将 PATH 恢复原样。

1.2.4 目标文件的执行原理

解释完指令的寻找与执行后,我们需要探讨一下相应的脚本是如何被执行的,通常我们写的自定义脚本文件的入口文件的上方都需要写上一行代码:

#!/usr/bin/env node

#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell,比如我们在写自定义 shell 脚本的时候可以在脚本的第一行指定当前脚本所使用的解释器:

  • #!/bin/bash

  • #!/bin/zsh

  • ...

这样写的目的是为了使该文件以可执行程序去运行的时候可以找到相应的解释器,当然如果将文件所在位置作为参数传递给解释器来执行的话,则不需要在自定义脚本的第一行添加上述代码(写了也没用),例如:

  • /bin/bash ./test.sh

  • ...

说了这么多,那么我们的 “#!/usr/bin/env node“又有什么不同呢?

#!/usr/bin/env <executableName> 是一种可移植指定解释器的方式:简而言之,它表示:执行 <executableName>,无论你(第一次)在 $PATH 变量中列出的目录中找到它(并隐式传递给文件的路径)在眼前)。

说白了就是告诉系统,当前的脚本需要通过 node 来执行,node 解释器所在位置需要在 $PATH 环境变量中所列举的目录中去寻找,这里可以对应到我在 2.2.2 节中第二张图中的 node 命令:

fa698ce4d8bc4b17be94b3204acac085.png

因此此文件就可以默认通过 node 来执行,并且我们也可以省略文件的后缀名(或者写啥后缀都行),与此同时也不需要我们显式的通过指定 node 解释器以文件路径作为参数的形式来执行,也就是类似于以下方式:

  • node ./test.js

  • ...

2. 目录结构规范

.
├── README.md
├── bin
│   └── demo-cli
├── dist
├── lib
├── node_modules // 依赖库
├── package-lock.json // 包版本控制
└── package.json // 包配置

2.1 README.md

项目的介绍文件,包括指令怎么用,指令有哪些选项等,以及其他信息。

2.2 bin

用于存放自定义指令对应的可执行文件。

2.3 dist

用于打包后发包,产物目录。

2.4 lib

源码所在位置,你可以根据需求自定义相关的文件结构,但是这里需要注意一点的是,如果你需要暴露 API 给外部使用,那么一定要和 package.json 中的 main 字段建立好联系。

3. 其他配置项

3.1 TypeScript 支持

为了方便开发与代码类型检查和提示,同时更好的组织代码,我们需要给项目添加 typescript 支持:

3.1.1 依赖安装

npm install --save-dev typescript @types/node rimraf

3.1.2 配置 tsconfig.json

{..."compilerOptions": {"baseUrl": ".","rootDir": "lib","lib": ["esnext"],"module": "commonjs","outDir": "dist/lib","allowJs": true,"strict": true,"declaration": true,"target": "es6","suppressImplicitAnyIndexErrors": true,},"include": ["lib"],...
}

详细配置:tsconfig.json。

3.1.3 配置 npm scripts

{..."scripts": {"clean": "rimraf dist","dev": "npm run clean && tsc -w","prepublish": "npm run clean && tsc"// 发包构建用},...
}

经过以上配之后,我们当前的 demo-cli 的项目结构可以是:

.
├── README.md
├── bin
│   └── demo-cli
├── lib
│   ├── core
│   │   └── index.ts
│   ├── index.ts
│   ├── library.ts
│   └── utils
│       └── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json

其中 library.ts 用于导出项目的对外暴露的 API,同时需要在 package.json 中配置 main 字段:

{..."main": "dist/lib/library.js",...
}

这样别人用我们的包的时候就可以使用相关的 API 了,但是我们的包定位是 cli 命令行工具,所以这一步是可选的index.ts 是项目的入口文件,也是指令执行调用的主文件。例如,我们可以在 **/bin/demo-cli** 中写好以下代码:

#!/usr/bin/env noderequire('../dist/lib/index.js');

然后在 **/lib/index.ts** 中写好以下代码:

function main() {console.log('这里是程序执行入口函数');
}main();

现在就可以通过:

node ./bin/demo-cli

命令来调试我们的代码了!不出意外,会产生如下输出:

8f434f59bcd203f1cdb111461d55f78b.png

但是这种方式每次都需要重新执行,才能看到已修改的代码的效果,所以我们可以在 vscode 中开启一个新的 shell 执行我们定义好的 npm scripts:

npm run dev

这样我们的文件就是动态变化的了,我们改了代码就会产生相应的 ts 编译后的结果,那么我们要怎样调试指令呢?通过 node ./bin/demo-cli 来调试还是不妥,这种 cli 工具我们都是要靠项目调试的,因此我们需要通过在本项目的根目录下执行以下指令:

npm link

这样的话,会在全局中创建关于我们的 demo-cli 的自定义指令的软链接,这其实相当于是一个全局指令注册,然后我们就可以直接在其他项目中使用 demo-cli 指令来运行调试我们的脚本了,调试完之后别忘了删除全局链接,只需要在项目的根目录下执行以下指令:

npm unlink

3.2 Eslint 与 Prettier

3.2.1 安装依赖

npm i -D eslint@7.32.0 @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-prettier prettier

经验证,7.32.0 版本比较好用,8.0 以上移除了一些 API,产生 eslint 加载失败,导致 VSCode 的 eslint 实时检查不生效

3.2.2 配置.eslintrc 与.eslintignore

.eslintrc.js:

{"root": true,"parser": "@typescript-eslint/parser", //定义ESLint的解析器"plugins": ["prettier","@typescript-eslint"],//定义了该eslint文件所依赖的插件,"extends": ["prettier"],"rules": {"no-var": "error","prettier/prettier": "error"}
}

.eslintignore:

dist
node_modules

3.2.3 配置.prettierrc 与.prettierignore

.prettierrc:

{"useTabs": false,"printWidth": 120,"singleQuote": true,"trailingComma": "es5","arrowParens": "always"
}

.prettierignore:

dist
node_modules

3.3 代码提交前检测

3.3.1 安装依赖

npm install -D husky lint-staged

3.3.2 配置 package.json

{..."lint-staged": {"*.{js,ts}": ["prettier-eslint --write","eslint --fix","git add"]},...
}

启动钩子:

npx husky install

添加钩子 pre-commit:

npx husky add .husky/pre-commit'echo \"git commit trigger husky pre-commit hook\" && npx lint-staged'

这样在 git commit 之前就能使用 lint-staged 去检查相应的文件,并执行相应的命令来修复我们的代码。

3.4 .gitignore

node_modules
package-lock.json
dist

3.5 .npmignore

# Dependency directories
node_modules
package-lock.json# source code
lib
.eslintrc.js
.eslintignore
.prettierrc
.prettierignore
.gitignore
tsconfig.json

经过以上配之后,当前项目的目录结构为:

.
├── bin
│   └── demo-cli
├── dist
├── lib
│   ├── core
│   │   └── index.ts
│   ├── index.ts
│   ├── library.ts
│   └── utils
│       ├── index.js
│       └── index.ts
├── node_modules
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json

4. CLI 常用第三方库

  • commander —— 提供 cli 命令与参数

  • glob —— 遍历文件

  • shelljs —— 常用的 shell 命令支持

  • prompts —— 读取控制台用户输入

  • fs-extra —— 文件读写等操作

  • inquirer —— 类似于 prompts

  • chalk —— 彩色日志

  • debug —— 类似于 chalk

  • execa —— 执行 shell 指令

  • ...

5. npm 发包

第一次发包:

npm adduser

否则:

npm login

然后:

npm publish

这里因为在 npm scripts 里添加了相应的 prepublish 钩子,所以在 publish 之前会先跑构建,从而确保我们的代码是最新的。

6. 总结

写个 cli demo 会遇到很多问题,最痛苦的还是 eslint 的 VSCode 配置问题,要调半天,如果说没有在 VSCode 中配置 eslint 插件或者说打开 VSCode 的控制台 output:

6f40172c34b20b7d7441dba258e443b7.png

有报错的话 (以上为正常运行),eslint 都不会生效,具体错误具体解决吧。

除此之外,理解 Linux 指令的运行原理以及 node bin 的执行原理对于理解 cli 命令是怎么跑的特别重要,从而还能扩展出一些其他用法,我们的项目还能不只是 JS 项目,还可以写 C++ 扩展模块。

站在巨人的肩膀上来开发,不要重复造轮子,好的模块应该是经得起考验的,但是要理解别人的代码是怎么写的,理解其中的原理,善于 “借鉴 “。

d1f9ba4108c845b6b34264508ed90f42.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

47a8397dbbb9e30614e75a3fca7c9a68.png

识别方二维码加我微信、拉你进源码共读

今日话题

略。分享、收藏、点赞、在看我的文章就是对我最大的支持~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/275096.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

印刷报价系统源码_皇家印刷术-设计系统案例研究

印刷报价系统源码重点 (Top highlight)Typography. It’s complicated. With Product Design, it’s on every screen. Decisions for a type scale affect literally every aspect of a product. When you’re working with an existing product, defining typography can fee…

React Hooks 完全使用指南

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。React HooksHook 是什么…

重新设计Videoland的登录页面— UX案例研究

In late October of 2019 me and our CRO lead Lucas, set up a project at Videoland to redesign our main landing page for prospect customers (if they already have a subscription, they will go to the actual streaming product).在2019年10月下旬&#xff0c;我和我…

全新的 Vue3 状态管理工具:Pinia

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。Vue3 发布已经有一段时间…

都快 2022 年了,这些 Github 使用技巧你都会了吗?

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。最近经常有小伙伴问我如…

Repeater\DataList\GridView实现分页,数据编辑与删除

一、实现效果 1、GridView 2、DataList 3、Repeater 二、代码 1、可以去Csdn资源下载&#xff0c;包含了Norwind中文示例数据库噢&#xff01;&#xff08;放心下&#xff0c;不要资源分&#xff09; 下载地址&#xff1a;数据控件示例源码Norwind中文数据库 2、我的开发环境&a…

网站快速成型_我的老板对快速成型有什么期望?

网站快速成型Some of the top excuses I have gotten from clients when inviting them into a prototyping session are: “I am not a designer!” “I can’t draw!” “I have no creative background!”在邀请客户参加原型制作会议时&#xff0c;我从客户那里得到的一些主…

EXT.NET复杂布局(四)——系统首页设计(上)

很久没有发帖了&#xff0c;很是惭愧&#xff0c;因此给各位使用EXT.NET的朋友献上一份礼物。 本篇主要讲述页面设计与效果&#xff0c;下篇将讲述编码并提供源码下载。 系统首页设计往往是个难点&#xff0c;因为往往要考虑以下因素&#xff1a; 重要通知系统功能菜单快捷操作…

figma设计_在Figma中使用隔片移交设计

figma设计I was quite surprised by how much the design community resonated with the concept of spacers since I published my 自从我发表论文以来&#xff0c;设计界对间隔件的概念产生了多少共鸣&#xff0c;我感到非常惊讶。 last story. It encouraged me to think m…

axios源码中的10多个工具函数,值得一学~

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。本文来自读者Ethan01投稿…

寄充气娃娃怎么寄_我如何在5小时内寄出新设计作品集

寄充气娃娃怎么寄Over the Easter break, I challenged myself to set aside an evening rethinking the structure, content and design of my portfolio in Notion with a focus on its 在复活节假期&#xff0c;我挑战自己&#xff0c;把一个晚上放在一边&#xff0c;重新思…

最全 JavaScript Array 方法 详解

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。我们在日常开发中&#…

管理沟通中移情的应用_移情在设计中的重要性

管理沟通中移情的应用One of the most important aspects of any great design is the empathetic understanding of and connection to the user. If a design is ‘selfish’, as in when a product designed with the designer in mind and not the user, it will ultimatel…

网易前端进阶特训营,邀你免费入营!一举解决面试晋升难题!

网易等大厂的前端岗位一直紧缺&#xff0c;特别是资深级。最近一位小哥面进网易&#xff0c;定级P4&#xff08;资深&#xff09;&#xff0c;总包60W&#xff0c;给大家带来真实面经要点分享。网易的要求有&#xff1a;1.对性能优化有较好理解&#xff0c;熟悉常用调试工具2.熟…

angelica类似_亲爱的当归(Angelica)是第一个让我哭泣的VR体验

angelica类似It was a night just like any other night. I finished work for the day and closed my laptop. I had dinner and after an hour, I put on my Oculus Quest headset in order to begin my VR workout.就像其他任何夜晚一样&#xff0c; 这 是一个夜晚。 我完成…

面试官:请手写一个带取消功能的延迟函数,axios 取消功能的原理是什么

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。本文仓库 https://githu…

facebook 面试_如何为您的Facebook产品设计面试做准备

facebook 面试重点 (Top highlight)Last month, I joined Facebook to work on Instagram DMs and as a way to pay it forward, I 上个月&#xff0c;我加入了Facebook&#xff0c;从事Instagram DM的工作&#xff0c;作为一种支付方式&#xff0c;我 offered to help anyone…

8年了,开始写点东西了

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。今天分享一位大佬的文章…

荒径 弗罗斯特_弗罗斯特庞克,颠覆性城市建设者

荒径 弗罗斯特Most gamers are familiar with Will Wright’s famous SimCity series. It created the city building genre and there have been many attempts over the years to ape it. But few developers have been bold enough to completely deconstruct the formula; …

Gitee 如何自动部署博客 Pages?推荐用这个GitHub Actions!

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。前段时间我把自己的博客…