这是一篇篇幅很长的文章,分为四个部分,每个部分都可以单独食用。
npm 为我们打开了连接整个 JavaScript 天才世界的一扇大门。它是世界上最大的软件注册表,每星期大约有 30 亿次的下载量,包含超过 600000 个 包(package) (即,代码模块)。来自各大洲的开源软件开发者使用 npm 互相分享和借鉴。包的结构使您能够轻松跟踪依赖项和版本。
npm 由三个独立的部分组成:
我们可以从npm上获取大量优秀第三方包,当然我们也可以上传自己的包。
下面简单介绍如何使用npm创建和发布自己的包。
要上传我们自己的包,首先要有npm账号,如果你没有npm账号,可以去[注册页](https://www.npmjs.com/signup)注册并登录自己的账号。输入用户名、邮件、密码即可创建。
在命令行输入`npm login`,根据提示输入账号、密码,即可在命令行登录我们的账号。后续可以通过此账号上传我们开发的npm包。
在命令行输入`npm whoami`可以测试自己是否登录成功,登录成功则会打印出你的用户名。
npm包分为public和private两种类型。
在此之前,我们先了解这么一个概念——scopes(中文意思是作用域)。我们在注册npm账号和创建组织时,你将被授予一个与你的用户或组织名称匹配的范围,即你获得了一个适用范围(scope),这个范围是你的用户名或者创建的组织名。你可以将此范围用作相关包的命名空间。如你有一个package名叫mypackage,你的用户名为myusername,则你可以把这个package放到你的域里`@myusername/mypackage `
作用:
避免与别人的包重名,发生冲突。
限制该包的访问权限。
作为npm用户或组织成员,你可以创建和发布公共包,任何人都可以下载并在他们自己的项目中使用。
**unscoped:**的公共包存在于全球公共注册表(http://registry.npmjs.org)的命名空间中,并且可以在package.json文件中仅以包的名称来引用:package-name。
**scoped:**的公共包属于某个用户或组织,在package.json文件中作为依赖关系包含时,必须在前面加上用户或组织名称。@username/package-name@org-name/package-name
要使用私有软件包,你必须使用npm 2.7.0或更高版本,并且有一个付费用户或组织账户。
通过npm私有包,你可以使用npm注册表来托管只对你和选定的合作者可见的代码,允许你在项目中管理和使用私有代码与公共代码。
私有包总是有一个范围,而范围内的包默认为私有。
用户范围的私有包只能由你和被你授予读或读/写权限的合作者访问。
组织范围的私有包只能由被授予读或读/写权限的团队访问。
发布到注册表的软件包必须包含一个package.json文件。package.json文件会表明你的包名、版本、作用、列出你的项目所依赖的软件包、关键词等等。这份文件是你需要知道的关于package.json文件中需要的所有内容。它必须是实际的JSON,而不仅仅是一个JavaScript对象字面。所以package.json对npm包来说至关重要。我们使用动手创建demo来学习。
打开命令行,在指定目录创建一个demo项目mkdir zzmathcd zzmath
创建package.json文件npm init,根据提示输入你的包的信息。
使用编辑器打开package.json文件。
{
"name": "zzmath",
"version": "1.0.0",
"description": "一些简单的数据计算方法封装",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"math",
"js"
],
"author": "",
"license": "ISC"
}
这就是创建package.json文件后会有的一些信息。name:包名version: 版本号description:包的描述scripts: 脚本main:指定包的入口文件keywords: 包的关键词author: 作者license: ISC
更多字段请参考官方介绍
为了帮助其他人在npm上找到你的软件包,并在他们的项目中拥有使用你的代码的良好体验,建议在你的包的根目录中包括一个README文件。你的 README 文件可以包括安装、配置和使用你的软件包中的代码的说明,以及任何其他用户可能会发现的有用信息。README文件将显示在包页面上。
为了保持JavaScript生态系统的健康、可靠和安全,每当你对自己拥有的npm包进行重大更新时,建议在package.json文件中发布该包的新版本,并在其上标注更新的版本号,以遵循语义版本规范。遵循语义版本规范有助于依赖你的代码的其他开发人员了解特定版本的变化程度,并在必要时调整自己的代码。
软件包版本从1.0.0开始,遵循以下规则。
语义版本控制的概念很简单:所有的版本都有 3 个数字:x.y.z。
1. 第一个数字是主版本。
1. 第二个数字是次版本。
1. 第三个数字是补丁版本。
当发布新的版本时:
当进行不兼容的 API 更改时,则升级主版本。
当以向后兼容的方式添加功能时,则升级次版本。
当进行向后兼容的缺陷修复时,则升级补丁版本。
npm 设置了一些规则,可用于在 package.json 文件中选择要将软件包更新到的版本(当运行 npm update 时)。规则使用了这些符号:
^: 只会执行不更改最左边非零数字的更新。 如果写入的是 ^0.13.0,则当运行 npm update 时,可以更新到 0.13.1、0.13.2 等,但不能更新到 0.14.0 或更高版本。 如果写入的是 ^1.13.0,则当运行 npm update 时,可以更新到 1.13.1、1.14.0 等,但不能更新到 2.0.0 或更高版本。~: 如果写入的是 〜0.13.0,则当运行 npm update 时,会更新到补丁版本:即 0.13.1 可以,但 0.14.0 不可以。>: 接受高于指定版本的任何版本。>=: 接受等于或高于指定版本的任何版本。<=: 接受等于或低于指定版本的任何版本。<: 接受低于指定版本的任何版本。=: 接受确切的版本。-: 接受一定范围的版本。例如:2.1.0 - 2.6.2。||: 组合集合。例如 < 2.1 || > 2.6。
可以合并其中的一些符号,例如 1.0.0 || >=1.1.0 <1.2.0,即使用 1.0.0 或从 1.1.0 开始但低于 1.2.0 的版本。
还有其他的规则:
无符号: 仅接受指定的特定版本(例如 1.2.1)。latest: 使用可用的最新版本。
继续使用前面的demo,我们来开发一个简单的工具包,并发布到npm上。
新建index.js文件,封装两个简单的数学计算方法。
//index.js
/**
* @description: 返回数组中最大数的两倍
* @param {Array} arr
* @return {*}
*/
function maxDouble(arr){
return Math.max(...arr) * 2;
}
/**
* @description: 返回数组中最小数的两倍
* @param {Array} arr
* @return {*}
*/
function minDouble(arr){
return Math.min(...arr) * 2;
}
module.exports = {
maxDouble,
minDouble,
};
在命令行执行npm publish,即可发布我们的第一个npm包

到npm官网搜索即可找到我们刚发布的npm包
使用发布的包
新建一个test-demo小项目,并执行安装命令,安装我们的包


新建test.js,导入我们的包
//test.js
const zzmath = require('zzmath')
console.log(zzmath.maxDouble([2, 10, 11]));
console.log(zzmath.minDouble([2, 10, 11]));
在控制台 执行命令 node test.js,查看输出。
zhuxiaodong@zhuxiaodongdeMacBook-Pro test-demo % node test.js
22
4
前面说过,我们的包需要严格遵守语言版本管理,我们更新包的时候需要更新包的版本号。我们可以通过修改package.json文件里version来更改包的版本。或者通过执行命令npm version patch:更新补丁版本npm version major:更新主版本npm version minor:更新次版本
我们修改一下我们的包,然后更改版本号,执行npm publish即可更新包。

(dist-tags)是人类可读的标签,你可以用它来组织和标记你发布的软件包的不同版本。dist-tags是对语义版本的补充。除了比语义版本号更容易让人读懂之外,标签还允许发布者更有效地发布他们的软件包。
比如我们可以为包打上beta的标签,告诉人们此包还处于beta测试中,请谨慎使用。
:::warning
:由于dist-tags与语义版本共享一个命名空间,因此要避免dist-tags与现有版本号发生冲突。建议避免使用以数字或字母 "v "开头的dist-tag。
:::
使用命令npm publish --tag 为包打标签。
也可以使用以下命令为指定的版本打上标签npm dist-tag add
我们为我们的包打上beta标签,在打标签之前需要先更新版本号。


使用npm发布插件包就讲到这里,更多知识请查看官方文档。
commander.js是完整的 node.js 命令行解决方案。可以为你的脚手架工具定义执行命令、选项,处理传入的参数以及自动生成帮助信息等。基本上是现在脚手架工具必装的插件。
npm install commander
#!/usr/bin/env node
const { program } = require('commander');
//or
const { Command } = require('commander');
const program = new Command();
首先体验一个小例子:
easy-cli,进入文件夹,执行npm init根据提示一直回车,新建package.json文件。npm install commander安装commander。
根据前两部操作完成之后大概是这样。
bin文件夹,在文件夹中新建easy.js文件。package.json文件中设置命令名到本地文件名的映射。 "bin": {
"easy": "bin/easy.js"
},
easy文件开头输入#!/usr/bin/env node,指定easy.js的解释程序是node。#!/usr/bin/env node
//easy.js
const { Command } = require("commander");
const program = new Command();
//easy.js
//..other code
program.option('-c, --cons', 'console hello world!')
//解析
program.parse()
const options = program.opts();
if(options.cons) console.log("hello world!");
测试我们的命令:
在控制台输入npm i -g ../easy-cli 安装我们的cli,mac用户可能需要使用sudo权限sudo npm i -g ../easy-cli安装。这样就在全局和我们的cli项目文件建立了一个连接。
使用npm list -g可以查看我们在全局安装的所有包。
zxd@zxddeMBP easy-cli % npm list -g
/usr/local/lib
├── @vue/[email protected]
├── @vue/[email protected]
├── [email protected] -> ./../../../Users/zxd/learnSpace/easy-cli
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]
可以看到我们的easy-cli已经安装到了全局。
只需建立一次连接,在后续修改cli后,不需要再次执行。
在不需要调试的时候可以使用npm unlink -g easy-cli删除连接。
在命令行执行我们的命令以及选项 easy -c
输出:
hello world!
//easy.js
//..other code
program.option('-c, --cons', 'console hello world!')
program
.command("double")
.arguments("" )
.description("将输入的参数x2")
.action((number) => {
console.log(number*2);
});
program.parse()
const options = program.opts();
if(options.cons) console.log("hello world!");
在命令行输入 easy double 2执行这个命令
输出:
4
在命令行输入 easy double 2 -c
输出:
4
hello world!
大家可以尝试执行以下命令,看看会打印什么
easyeasy doubleeasy -c double 2easy -c
带着好奇和疑问,开始学习commander.js
option是我们能够通过命令执行的选项。例如:在安装npm包时,将包安装在dev依赖里。npm i ,--save-dev就是定义的选项。
Commander 使用.option() 方法来定义选项,同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(–后面接一个或多个单词),使用逗号、空格或|分隔。
program.option('-s, --small', 'small pizza size')
// 短名称 长名称 选项简介
有两种最常用的选项,一类是 boolean 型选项,选项无需配置参数(上面体验的那种),另一类选项则可以设置参数(使用尖括号声明在该选项后,如–expect )。如果在命令行中不指定具体的选项及参数,则会被定义为undefined。带参数的又分为必填参数和可选参数
选项可以通过在Command对象上调用
.opts()方法来获取
通过program.parse(arguments)方法处理参数,没有被使用的选项会存放在program.args数组中。该方法的参数是可选的,默认值为process.argv。
使用尖括号定义的参数是必填的。
#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();
program.option("-c, --cons ", "console input"); //参数
program.parse()
const options = program.opts();
if(options.cons) console.log(options.cons);
执行命令 easy -c "hey,how are you?"
输出:
hey,how are you?
执行命令 easy -c
输出:
error: option '-c, --cons ’ argument missing
使用方括号定义的参数是可选的。选项在不带参数时可用作boolean选项,在带有参数时则从参数中得到值。
#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();
program.option("-c, --cons [input]", "console input or console hello world!"); //参数
program.parse();
const options = program.opts();
if (options.cons===true){
console.log("hello world!");
}else{
console.log(options.cons);
}
执行命令 easy -c "hey,how are you?"
输出:
hey,how are you?
执行命令 easy -c
输出:
hello world!
可以通过在选项解释后面添加第四个值来为选项添加默认值。
:::warning
必填选项的默认值只在输入命令时 不输入必填选项 时使用,如果输入必填选项,但是没有输入必填选项的参数则会报错。
:::
#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();
program.option("-c, --cons [input]", "console input or console hello world!","Hola"); //可选选项 默认值
program.option(
"-ch, --cheese " ,
"add the specified type of cheese",
"blue"
);//必填选项 默认值
program.parse();
const options = program.opts();
console.log(options.cons);
console.log(`cheese: ${program.opts().cheese}`);
执行命令 easy -c "hey,how are you?"
输出:
hey,how are you?
cheese: blue
执行命令 easy -c
输出:
Hola
cheese: blue
执行命令 easy -c -ch
输出:
error: option '-ch, --cheese ’ argument missing
定义选项时,可以通过使用…来设置参数为可变长参数。在命令行中,用户可以输入多个参数,解析后会以数组形式存储在对应属性字段中。在输入下一个选项前(-或–开头),用户输入的指令均会被视作变长参数。与普通参数一样的是,可以通过–标记当前命令的结束。
#!/usr/bin/env node
program
.option('-n, --number ' , 'specify numbers')
.option('-l, --letter [letters...]', 'specify letters');
program.parse();
console.log('Options: ', program.opts());
console.log('Remaining arguments: ', program.args);
执行命令 easy -n 1 2 3 --letter a b c
输出:
Options: { number: [ '1', '2', '3' ], letter: [ 'a', 'b', 'c' ] }
Remaining arguments: []
执行命令 easy -n 1 2 3 --letter a b c -- ext
输出:
Options: { number: [ '1', '2', '3' ], letter: [ 'a', 'b', 'c' ] }
Remaining arguments: [ 'ext' ]
version方法可以设置版本,其默认选项为-V和--version,设置了版本后,命令行会输出当前的版本号。
#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();
program.version("0.0.1");
//通常使用package.json文件里的version
//program.version(require("../package.json").version)
//可以在version方法里再传递一些参数(长选项名称,描述信息),用法与option方法类似。
//program.version('0.0.1', '-v, --vers', 'output the current version');
program.parse()
执行命令:easy -V
输出:0.0.1
commander.js会自动生成帮助信息。默认帮助选项帮助信息是-h,--help。
命令行执行easy -h
输出:
Usage: easy [options]
Options:
-V, --version output the version number
-h, --help display help for command
我们也可以自定义帮助信息
#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();
program
.option('-f, --foo', 'enable some foo');
program.addHelpText('after', `
Example call:
$ custom-help --help`);
program.parse(process.argv);
命令行执行easy -h
输出:
Usage: easy [options]
Options:
-f, --foo enable some foo
-h, --help display help for command
Example call:
$ custom-help --help
更多帮助信息的设置请参考官方文档。
命令就是我们使用脚本执行的方法,如我们使用npm安装包的时候使用 npm i 或者npm install 命令执行安装方法,i 和 install就是方法名,i是方法的简称,install是方法的全称,-s就是命令的选项。将包安装在dependencies。
通过.command()或.addCommand()可以配置命令,有两种实现方式:
.command()的第一个参数可以配置命令名称及命令参数,参数支持必选(尖括号表示)、可选(方括号表示)及变长参数(点号表示,如果使用,只能是最后一个参数)。跟选项类似。尖括号(例如)意味着必选,而方括号(例如[optional])则代表可选。可以向.description()方法传递第二个参数,从而在帮助中展示命令参数的信息。该参数是一个包含了 “命令参数名称:命令参数描述” 键值对的对象。
#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();
program
.command("clone [destination]" )
//定义命令名称clone 必填参数source 可选参数destination
.description("clone a repository into a newly created directory") //命令的描述
.action((source, destination) => {//命令处理函数
console.log(`repository has been cloned from ${source} to ${destination}`);
});
program.parse(process.argv);
命令行执行easy clone a b
输出:
repository has been cloned from a to b
也可以将命令写成单独的可执行文件
当.command()带有描述参数时,就意味着使用独立的可执行文件作为子命令。 Commander 将会尝试在入口脚本(例如 ./examples/pm)的目录中搜索program-command形式的可执行文件,例如easy-start, pm-install。通过配置选项executableFile可以自定义名字。
#!/usr/bin/env node
//easy.js
const { Command } = require("commander");
const program = new Command();
program
.command("start", "每隔1秒输出当前时间秒数")// 需要有 easy-start.js可执行文件
.command("start2","每隔2秒输出当前时间秒数", { executableFile: 'milliseconds' })//需要有milliseconds.js 可执行文件
program.parse(process.argv);
新建easy-start.js文件
#!/usr/bin/env node
setInterval(() => {
console.log(new Date().getSeconds());
}, 1000);
新建milliseconds.js文件
#!/usr/bin/env node
setInterval(() => {
console.log(new Date().getSeconds());
}, 2000);
分别执行命令:easy start、easy start2,查看输出。
在参数名后加上...来声明可变参数,且只有最后一个参数支持这种用法。
program
.command('rmdir ' )
.action(function (dirs) {
dirs.forEach((dir) => {
console.log('rmdir %s', dir);
});
});
命令行输入easy rmdir 1 2 34 45
输出:
rmdir 1
rmdir 2
rmdir 34
rmdir 45
单独的可执行文件接收参数
#!/usr/bin/env node
//easy.js
const { Command } = require("commander");
const program = new Command();
program
.command("start " , "遍历number")// 需要有 easy-start.js可执行文件
program.parse(process.argv);
#!/usr/bin/env node
//easy-start.js
const { Command } = require("commander");
const program = new Command();
program.parse();
console.log(program.args)
命令行输入easy start 5 12345 4 23
输出:[ ‘5’, ‘12345’, ‘4’, ‘23’ ]
需要通过制定参数位置来取参数。 "5"是length,剩下的都是number。
监听命令和选项可以执行自定义函数。
#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();
program.option("-c, --cons", "console hello world!");
program
.command("clone [destination]" )
//定义命令名称clone 必填参数source 可选参数destination
.description("clone a repository into a newly created directory") //命令的描述
.action((source, destination) => {
//命令处理函数
console.log(`repository has been cloned from ${source} to ${destination}`);
});
program.on("option:cons", function () {
console.log("输入选项 --cons")
});
program.on("command:*", function (operands) {
console.error(`error: unknown command '${operands[0]}'`);
const availableCommands = program.commands.map((cmd) => cmd.name());
console.log("availableCommands:", availableCommands);
});
program.parse();
const options = program.opts();
if (options.cons) console.log("hello world!");
命令行输入easy clone a b -c
输出:
输入选项 --cons
repository has been cloned from a to b
hello world!
命令行输入easy pull a b -c
输出:
输入选项 --cons
error: unknown command 'pull'
availableCommands: [ 'clone' ]
hello world!
const { Command } = require('commander');
const program = new Command();
program
.version('0.0.1')
.option('-c, --config ' , 'set config path', './deploy.conf');
program
.command('setup [env]')
.description('run setup commands for all envs')
.option('-s, --setup_mode ' , 'Which setup mode to use', 'normal')//命令的选项
.action((env, options) => {
env = env || 'all';
console.log('read config from %s', program.opts().config);
console.log('setup for %s env(s) with %s mode', env, options.setup_mode);
});
program
.command('exec