本文简述了命令行的意义和优势, 介绍了解释型命令行的运行机制, 同时介绍了几个 Node.JS 相关的命令行工具, 推荐了几款撰写命令行程序常用的包, 最后, 概述了发布包和使用 scope 的发布情况.
源起
植根于 Unix 系统环境下的程序, 很多都把贯彻 Unix 系统设计的哲学作为一种追求. Unix 系统管道机制的发明者 Douglas McIlroy 把 Unix 哲学总结为三点:
专注做一件事, 并做到极致.
程序协同工作.
面向通用接口, 如文本数据流.
随着 Unix/Linux 系统在服务器上影响力越发强大, 以及各种跨平台解决方案的发展, 这种哲学也被带到了各种平台上. 若干年前, 笔者第一次接触 Node.JS 和其包管理解决方案 NPM 时候, 就感觉到其官方倡导的风格, 和 Unix 系统哲学非常契合. 近年来, 随着 Node.JS 在服务端以及前端构建领域上的不断开拓, Node.JS 的这种思想也正快速的渗透到这些领域.
其实, Node.JS 的本身, 也是开发命令行程序的一个重要利器. 本文就将介绍几个常用的 Node.JS 相关命令行程序, 之后介绍几个开发命令行中常用的组件, 最后介绍发布 NPM 包以及带 scope 的包的发布方法.
命令行是如何工作的
命令行, 可以简单定义为是一种基于文本流的用户交互接口和交互方式. 命令行程序常常通过命令行参数的传递来得到不同的运行方式. 而由于一切命令的下达, 都是基于文本的, 所以也为元编程, 提供了便利.
命令行程序可以是编译执行的, 也可以是解释执行的. 对于编译后的命令行程序, 将直接以机器码执行. 而对于大多数的解释型的命令行程序, 运行往往需要借助命令行解释程序.
这篇文章中提到的命令行程序特指需要解释程序的命令行程序.
可以充当命令行解释程序的, 其实包含了大家听说过的常见的解释器, 比如 bash,zsh,perl,python,Ruby,tcl 等等, 当然还有 Node.JS.
打开一个命令行程序, 比较标准的写法是在第一行写明解释程序的路径, 如:
#!/usr/local/opt/python/bin/python3.6
这里 #! 称为 shebang, 一般位于文件的最开头. 在 Unix 系统中,#! 所在行后面的部分将被视为解释器指令. 同时会把文件所在路径作为参数附在解释器后面. 上例中, 如果文件是 / usr/local/bin/pip, 则直接运行 / usr/local/bin/pip 的效果, 等同于 / usr/local/opt/python/bin/python3.6 /usr/local/bin/pip.
这样做, 使得用户无需关心解释程序, 无需关心代码编写的语言, 直接运行对应的命令行程序本身就好了. 这也是 shebang 存在的意义. 不过, 由于系统设定的原因, 使用 Windows 的同学可能无法享受这种便利, 一般还需手动指定解释程序的路径. 但是, 他们可以双击运行:-).
可以试着用文本编辑工具打开一个 Node.JS 写成的脚本如: webpack, 会发现其第一行是 #!/usr/bin/env node. 这句话并不是直接的 Node.JS 的解析程序. 这里, /usr/bin/env 是一个程序, 目的是从系统的 PATH 中寻找对应名字的解释程序的地址. 此时, 解释程序可以被安装在各种路径, 只要在系统 PATH 中注册过, 就可以找到了.
可能大家遇到过这种问题, 在运行某些 Node.JS 程序会出现报错:
/usr/bin/env: node: No such file or directory
此时可以从系统 PATH 中是否有 node 这个文件路径, 某些版本的 Node.JS 是否名为 node 等方向来排查问题.
Node.JS 相关: 好用的命令行工具
在 Node.JS 目前已经成为前端工作流的主力语言的情况下, babel 和 webpack 基本已经成为前端开发, 测试, 发布上的重要工具. 同时围绕 babel 和 webpack 有一系列周边的工具包和插件协助开发者完成日常开发的方方面面.
同时, 目前最为流行的前端框架 Angular,react,vue(以首字母为序), 各自有自带的脚手架和开发辅助工具. 如 ng-cli,create-react-App 和 vue-cli 等等. 更有 Poi1 这样的通吃 React 和 Vue 的脚手架工具.
上面这部分每一个都可以独立出来单独讲解. 有兴趣的读者可以参考上述工具的官方网站获取更多信息.
下面来说几个其他方面的 Node.JS 相关的软件包.
1. 多版本共存 n/nvm
大多数情况, 我们只需面对单一的 Node.JS 版本. 等到时机成熟, 再统一把 Node.JS 版本升级到更高版本.
不过笔者就曾经遇到过一个年久失修失修的项目需要重新维护的情况. 此时需要把 Node.JS 版本切到老版本. 同时, 我们也不想舍弃大多数项目运行的新版本 Node.JS 环境.
这种情况可以使用 n 或 nvm. 下图展示了, 用 n 下载并切换到一个新版本的过程.
除了下载之外, n 还提供了列表的方式切换多个版本, 以及删除某个版本的方法. 读者可以在安装之后使用 n -h 查看所有可用参数.
n 采用 bash 编写. 但提供了一个 NPM 仓库安装的入口, 可以使用大家传统意义的 NPM 安装法进行全局安装, 前提是你必须有一个可以运行的 Node.JS 环境.
NPM install -g n
或者在没有 Node.JS 的环境下, 可以使用 n-install 脚本. 安装只需运行: curl -L https://git.io/n-install | bash.
如果是 Windows 用户, 在 windows10 下面可以安装 wsl 来获得 Linux 脚本运行环境, 官方仓库的一个 issues, 对此有一个操作说明 2.
对 windows10 以下的用户, 可以考虑折腾下 Cygwin.
除了 n 之外, 还有一个管理工具为 nvm, 也是采用 bash 脚本编写. 安装亦可使用安装脚本来完成. 如:
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash 或 wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
. 这里的 v0.34.0 是版本号, 可能会随着版本迭代而变化.
使用 Windows 的读者, 除了上述 wsl 和 Cygwin 之外, 可以考虑使用 nvm-windows3 这个用 Golang 编写的版本.
就目前的最新版本来说, n 和 nvm 的都会尝试处理公共的依赖库, 然而处理方式是不一样的.
n 和 nvm 都会在首次使用某个版本时将此版本的 Node.JS 下载至本地, 不同的是: n 将尝试用新版本代替系统路径中, 关键路径如 bin,lib,include,share 的包. nvm 则是保留每一个版本的副本, 并将 Node.JS 的系统路径指向. nvm 维护的沙箱地址.
从处理上, nvm 显得更轻量和高效, 但是需要修改系统的 PATH, 这一步 nvm 脚本会自动完成. n 则无需入侵系统路径, 但每次修改时候均需操作系统路径, 且此时最好使用 sudo n 运行, 避免因权限不足, 拒绝向系统路径复制.
由于 nvm 会修改 PATH 地址, 所以如果同时默认安装 nvm 和 n,n 会运转不正常. 一种方案是避免同时安装, 另外可以手动修改 PATH, 使默认的 Node.JS 路径先于 nvm 的系统路径, 如修改 PATH 片段为:
/usr/local/bin:/Users/leon/.nvm/versions/node/v10.6.0/bin:
2. 执行辅助 nodemon/npx
nodemon 是一个执行器, 意义在于, 如果版本变化或者程序变化, 无需重新启动. 这在开发时候非常有用.
nodemon 还可以指定运行的端口, 如:
nodemon ./server.JS localhost 8080
除了控制 Node.JS 包之外, nodemon 还可以控制非 Node.JS 脚本, 比如: nodemon --exec "python -v" ./App.py, 将监控 App.py 的内容, 并在最开始以及发生变化时候, 调用 python -v 进行解析. 当然, 如果你的 App.py 指定了 shebang, 也可以不需指定解析函数.
nodemon 有很多灵活的配置, 通过这些配置, 可以实现环境变量设置, 延迟启动, 命令执行, 监控定制扩展名, 优雅重启, 事件监听等功能. 做法是在需要这些配置的目录下, 提供相关的配置 nodemon.JSON, 也可以在 package.JSON 中通过 nodemonConfig 字段指明.
在这里 4 有官方提供的一份配置文件的样例, 供读者参考.
再来说说 npx. 什么是 npx 呢? 简单说, 就是找到并运行一个包, 并且 "用完即走".
这里有两层意思:
找到. 从哪里找: 先是当前的依赖, 然后是 PATH, 还找不到就到网上找来安装.
用完即走. 即使从网上安装的, 运行完就会删掉, 不会留下运行的包. 读者可以试着运行下: npx GitHub:piuccio/cowsay "awesome npx" 体验下.
这实在是居家旅行, 开发调试的利器. 比如我要在当前目录下开一个 http 服务, 可以直接运行: npx http-server
之后就可以直接在浏览器访问这个地址进行调试了.
另外, 如果你需要临时用一个老版本的 node 来运行某个脚本, 也可以祭出 npx, 这个 node 会被临时安装, 临时使用, 用完即走.
npx -p node@6 NPM init
3. 切换 Node.JS 注册表 nrm/yrm
nrm/yrm 维护了一个列表, 包括 NPM 主站和其他镜像. 可以使用 nrm/yrm use 快速切换, 以达到最快的下载速度. nrm 维护的是 NPM 的注册表, yrm 维护的是 yarn 注册表.
辅助编写 Node.JS 包
除了直接用大神们写好的命令之外, 我们也可以按照自己的需求定制自己需要的 Node.JS 包. 我们知道, 命令行其实也是一种人机交互, 因此, 交互上有很多可以借鉴的效果. 编写者只需将包倒入就可以使用这些交互效果. 这里笔者给大家推荐几个包
1. 命令行参数读取 commander
命令行的一个特点就是根据参数的不同调整运行策略. 然而处理命令行输入以及验证是一个非常繁琐的事情. 为此, TJ 大神曾经创立了 commander 包. 最基础的用法如下:
- var program = require('commander');
- program
- .version('0.1.0')
- .option('-p, --peppers', 'Add peppers')
- .option('-P, --pineapple', 'Add pineapple')
- .option('-b, --bbq-sauce', 'Add bbq sauce')
- .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
- .parse(process.argv);
- console.log('you ordered a pizza with:');
- if (program.peppers) console.log('- peppers');
- if (program.pineapple) console.log('- pineapple');
- if (program.bbqSauce) console.log('- bbq');
- console.log('- %s cheese', program.cheese);
默认地, commander 会自动创建 - h 的帮助文件, 即利用每一个 option 的输入产生帮助文案.
用户的每一个输入, 都会放置在 program 对应 option 长名的字段的驼峰形式上, 如果没有提供长名, 则放在短名字段上. 上例中, 如使用: testcommander -p 111 -P 222 -b 333 则依次存储在 program 的 peppers,pineapple 和 bbqSauce 上.
同时, commander 提供多种验证方式, 如正则表达式:
program.option('-s --size <size>', 'Pizza size', /^(large|medium|small)$/i, 'medium')
则指定只能输入特定的值.
同时, commander 提供一个方案, 允许用户设置子命令. commander 称之为 Git 风格的子命令.
- var program = require('commander');
- program
- .version('0.1.0')
- .command('install [name]', 'install one or more packages')
- .command('search [query]', 'search with optional query')
- .command('list', 'list packages installed', {isDefault: true})
- .parse(process.argv);
这个例子中, 假设命令行名字为 pm, 则当用户输入 pm-install,pm-search 或 pm-list 时候, commander 会尝试在入口文件的同一级目录找到 install,search 或 list, 并交给这个文件去执行.
2. 进度条 progress
在编写 Web 程序时候, 大家经常会展示一个进度条. 用以缓解用户在等待时候的焦虑. 其实在命令行程序中也会有这种交互方式. 比如 wget 就会在下载过程中给出进度提示.
在 Node.JS 中也有这样的效果可以使用. 这就是 progress 包. 下面的代码, 运行结果是下载 CentOS 安装盘. 在下载之中, 会实时打印进度
- const ProgressBar = require("progress")
- const request = require("request")
- const progress = require("request-progress")
- const fs = require("fs")
- const download = (url, headers, target, totalSize) => {
- let percent = 0
- const bar = new ProgressBar('下载中: ├:bar┤ 完成: percent 预估完成时间: eta 秒 用时: elapseds', {
- total: 100,
- complete: "█",
- incomplete: "─",
- width: 60
- })
- let opt = {
- headers,
- url: url
- }
- return new Promise((resolve, reject) => {
- progress(request.get(opt))
- .on('progress', function (state) {
- let progressFix = ((state.percent) * 100).toFixed(2)
- delta = progressFix - percent
- bar.tick(delta)
- percent = progressFix
- })
- .on("error", () => {
- return reject()
- })
- .on('end', () => {
- bar.tick(100 - percent)
- console.log('\n')
- return resolve(target)
- })
- .pipe(fs.createWriteStream(target));
- })
- }
- const foo = {
- getHeaders: () => {
- const headers = {
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
- 'Accept-Charset': 'UTF-8,*;q=0.5',
- 'Accept-Encoding': 'gzip,deflate,sdch',
- 'Accept-Language': 'en-US,en;q=0.8',
- 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:13.0) Gecko/20100101 Firefox/13.0'
- }
- return Object.assign({}, headers)
- },
- download: function (url, target, totalSize){
- let headers = this.getHeaders()
- headers = Object.assign(headers)
- download(url, headers, target, totalSize)
- }
- }
- foo.download("http://mirrors.cmich.edu/centos/7.6.1810/isos/x86_64/CentOS-7-x86_64-DVD-1810.iso",
- "CentOS-7-x86_64-DVD-1810.iso", 4508876.8
- )
运行的结果如图:
这个包的核心就是根据内置和自定义的 token 在命令行打印出相应的字符, 用以完成交互.
3. 交互着色 chalk
chalk 是一个命令行交互的着色工具. 在命令行支持的情况下, 可以支持最多 16 位色域 (前提是命令行终端可以支持). 一般可以配合 console.log 使用, 如:
- const chalk = require('chalk');
- const log = console.log;
- // Combine styled and normal strings
- log(chalk.blue('Hello') + 'World' + chalk.red('!'));
笔者曾经做过一个在命令行下显示图片的程序, 就是利用的 chalk 和 console.log 进行的配合.
4. 交互式问答 inquirer
在需要不断的同用户进行交互式问答, 并根据用户的输入进行验证和路径选择, 这个时候 inquirer 是非常趁手的工具. 它内置了单选, 多选, 问答等多种交互方式. 大家可以感受下:
甚至可以通过插件实现 suggest
vue 框架的脚手架 vue-cli 是一个使用 inquire 的绝佳案例, 读者可以通过阅读源码, 感受下大神出神入化的使用.
5. 小图标 ora
ora 打印出一个优雅的文本小图标, 用于在各种情况下给出用户优雅而清晰的提示. 用法很简单:
- const ora = require('ora');
- const spinner = ora('Loading unicorns').start();
- setTimeout(() => {
- spinner.color = 'yellow';
- spinner.text = 'Loading rainbows';
- }, 1000);
6. 命令行玩浏览器 puppeteer
puppeteer 是谷歌开发的无头浏览器, 使得命令行亦可操作浏览器, 并能根据浏览器的执行结果进行进一步操控. 因为 puppeteer 源自官方, 所以之前类似项目 PhantomJS 的开发者决定不再更新 PhantomJS.
目前 puppeteer 已经广泛用于前端测试, 端对端测试, 以及爬虫.
鉴于篇幅无法展开介绍, 读者可以参考其官方文档 5. 同时, 奇舞周刊中黄小璐老师的的这篇文章 6 以及李光钊老师的这篇文章 7 都曾经介绍过 puppeteer 的使用.
发布 Node.JS 包
写好的 Node.JS 包需要发布出去, 才能给大家使用. NPM publish 就是为了这个需求而产生的. 为了发布你需要在 NPM 上注册用户, 并登录, 然后发布就好了. NPM 的详情页面以及各个镜像会在一段时间内自动更新.
如果你的 Node.JS 包, 是使用尚未广泛支持的语法写成的. 那么需要在 package.JSON 的 script 字段加入 prepublish 命令, 调用 babel 等预编译器处理, 使得程序可以有更多的兼容性.
对于希望用户在全局使用的命令, 要注意在根目录写好入口, 一般是在 package.JSON 中的 bin 字段, 指定入口文件. 在安装时, 如果是全局安装, NPM 将会使用符号链接把这些文件链接到 prefix/bin, 如果是本地安装, 会链接到./node_modules/.bin/.
除了通常的包, 还有一种是带有 scope 的包, vue-cli 的 3.0 版本就是 @vue 开头的. 这个 scope 是组织的名字. 每一个带有 scope 的包有公有和私有之分, 私有的需要付费给 NPM.
目前 NPM 的读写权限策略如下:
如果是个人, 可以考虑增加公有的命名空间. 如果是企业付费用户, 你在发布相关包之前, 需要申请成为这个 scope 的 member.
对公有 scope, 首先将包的 name 改为 @scope 名字 / 包名, 同时, 在发布时, 使用 NPM publish --access public 即可.
小结
本文简述了命令行的意义和优势, 介绍了解释型命令行的运行机制, 同时介绍了几个 Node.JS 相关的命令行工具, 推荐了几款撰写命令行程序常用的包, 最后, 概述了发布包和使用 scope 的发布情况. 希望给大家的 Node.JS 命令行相关开发和技术选型, 提供一些有用的帮助.
文内链接:
- https://poi.js.org/
- https://github.com/tj/n/issues/511
- https://github.com/coreybutler/nvm-windows
- https://github.com/remy/nodemon/blob/master/doc/sample-nodemon.md
- https://github.com/GoogleChrome/puppeteer
- https://mp.weixin.qq.com/s/TZgQPKrpIskIgxX3LCXZYw
- https://mp.weixin.qq.com/s/Xypg-9qZ8OqsPczEKdn6JA
[本文是 51CTO 专栏机构 360 技术的原创文章, 微信公众号 "360 技术 ( id: qihoo_tech)"]
戳这里, 看该作者更多好文
来源: http://zhuanlan.51cto.com/art/201904/595044.htm