什么是npm?
几乎每一门主流的编程语言都有自己的package(后面翻译为“包”)管理工具。而npm就是javascript(后面简称为“js”)这门编程语言的包管理工具。众所周知,js当前有两个主流的运行时平台:浏览器和nodejs。所以,我们在做前端或者nodejs端开发的时候,都离不开npm。npm是如此的重要,值得我们去深入探究和系统梳理一下它。
npm是包管理工具,这是一个泛化的,不够具体的陈述。具体来说,npm是由三部分来组成的:
- npm CLI。这是一个命令行工具。通过它,我们可以完成围绕包的【增,删,改,查】等各种操作。
- npm web 官网。web官网很核心的一块的是提供了npm的使用文档。除此之外,我们还可以在上面完成大部分可以通过 npm CLI 来完成的操作。
- npm registry。registry是一个数据库,里面存放着 js 包的源码和与包的元数据(meta data)。而npm (public) registry就是由npm官方提供的巨型公共数据库。npm registry托管在CouchDB这个云数据库服务平台上。它的地址是:registry.npmjs.org。 这也是npm CLI所默认的registry地址。当然,我们可以创建自己的私有registry,并在对包进行操作的时候关联到这个registry。因此,在使用npm CLI时,存在需要【切换registry】的需求和说法。
在这个npm整个软件体系中,开发者只对js 包拥有自主权。从万物皆服务的角度来看,npm对于js开发者来说,其实是一个PaaS平台(什么是PaaS?这篇文章讲得挺好)。这是从【服务】这个角度来理解“什么是npm”的一个看法。
从各个维度来理解npm
下面我们不妨从实际的使用需求出发,从下面的几个的维度来理解npm,加深我们对npm的系统认知:
- 包
- 人
- 钱
从【包】的维度
什么是npm包?答曰:一个包含package.json的js项目文件夹就是一个npm包。
既然npm是 js 的包管理工具。因此,我们首先从包的维度来梳理一下npm。针对包我们会有两个重要概念和四个常见操作。这两个重要概念是:
- 包的类型
- 包的scope(后面翻译为“作用域”)
四个常见的操作就是:
- 增
- 删
- 改
- 查
下面我们从这个六个方面来看看。
包的类型
在npm中,包有两种类型:公共包(public)和私有包(private)。
公共包和私有包的不同之处主要体现在该包所属的registry范围内,普通用户【是否能够查看和下载】这个包。如果一个包是公共包,那么,在这个包所属的registry下注册的普通用户都是能够看到并下载这个包的。而如果一个包是私有包的话,那么普通用户是无法看到的,除非,这个用户被这个包的owner授予了【读】权限。
一般而言,scoped的包是私有包。注意,这里强调的是 【一般而言】。那特殊情况是什么呢?特殊情况是一个包虽然是scoped的,但是我们可以通过在发布包的时候来将它转换为公共包。比如说,现在我们有一个包叫:@littlepoolshark/test-npm-publish
,那么我们可以通过以下命令来将它转换为公共包:
$ npm publish --access public
关于scope与包类型的关系,npm官网有三个十分重要的论断:
- unscoped的包一定是公共包;
- 私有包一定是scoped的包;
- scoped的包默认是私有包。但是我们可以通过在发布的时候给access参数指定为“public”来将它转换为公共包;
用一句话来总结就是:“scoped是私有包的必要不充分条件”。
是不是谁都可以发布私有包呢?当然不是。npm本质上还是商业组织,它们也追求盈利。所以,只有付费账户或者付费organization旗下的被授予【写】权限的member才能发布私有包。如果你霸王硬上弓,那么npm CLI会报给你一个402:
公共包和私有包在发布这一块的限制是一致的。也就是说,只有这个包的owner和被owner授予【读/写】权限的人才能去发布这个包(详情可以参详我整理的npm权限表)。
包的scope
在npm中,包是有scope的说法的。一个包是否被scoped,就是看这个包的package.json文件name字段的值。name字段的值是一个字符串,如果这个字符串是以“@xxx/”(这里的“xxx”就是你所选的scope名)为前缀的,那么我们就说这个包是一个“scoped包”。否则,这个包就是个“unscoped包”。
scope名是可以乱起的,但是npm给不给你发布又是另外一回事。比如说,我当前登陆npm CLI的账号的name是littlepoolshark
。那么假如我的package.json是这样的话:
{
"name": "@sam/test-npm-publish",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
那么,npm会让我发布成功吗( 因为当前账户不是付费账号,所以我用了--access
参数)?执行:
$ npm publish --access public
此时,我的命令行界面会得到一个报错:
引起这个错误的原因并不是真的如果报错信息里面所说的:
真正的原因是你包的scope名(sam)跟你当前登陆的账号的name(littlepoolshark)不一致(可以参详一下这个stackoverflow问题)。我们可以通过以下的命令快速地查看一下自己当前账号的name是什么:
$ npm profile get name
// 或者
$ npm whoami
scope名除了可以是当前的账号名相同外,还可以是当前账号所归属的organization名(也就是说,scope名有两种类型,【user scope】和【organization scope】)。除了这两种情况,其他的scope名,npm CLI都会报错。
至于,包的scope跟包的类型之间的关系,上面的小节中已经给出了相关论断了,这里就不赘述了。
包的【增】
这里的“增”,是指往当前账号所关联的registry上面发布一个新的npm包。在发布之前,我们必须新建一个npm包。就像上面对“npm包”的定义所提到的那样,一个文件夹只要有package.json文件,我们就说这个文件夹是一个npm包。我们可以手工创建或者通过npm CLI来创建这个package.json文件。当然,我们会选择后者啦。
首先,我们来创建一个package.json文件:
$ npm init -y
没错,一个简单的命令,我们就可以快速地创建一个package.json文。一般情况下,我们会执行$ npm init
。这个命令会通过问答的交互方式来帮助你创建package.json。npm甚至支持我们来定制这个问答的模板。假如,你觉得你不要这个问答过程或者你想快速地完成创建,那么你就可以增加一个-y
参数来跳过这个流程。
创建完package.json文件后,即使你不安装任何的依赖包或者进行源码级别的开发,只要这个包的包名在npm public registry上面是唯一的话,那么就可以发布这个包了。发布包的命令很简单(当然,发布的前提是你要登陆,使用$ npm whoami
来确认自己是否已经登陆了):
$ npm publish
对于发布公共包,这已经足够了。如果你是一个没有付费的账号,但是你又想把包名改为在你账户名下的scoped包,那么就需要上面已经提到过的参数--access
,将它的值设置为public
即可:
$ npm publish --access public
参数--access
只对scoped包有效果,它的默认值是“restricted”。在一个unscoped包上面使用,npm会直接报错。
包的【删】
从当前registry上删除一个包,有两种需求场景。
第一种是单纯想删除这个包的某个特定版本,这时候,我们可以使用这个命令:
$ npm unpublish [<@scope>/]<pkg>@<version>
第二种是想完完全全删除这个包,也就是说删除这个包的所有已发布版本。那么,我们可以使用这个命令:
$ npm unpublish [<@scope>/]<pkg> --force
包的【改】
“改”这里指包的更新。包的更新使用跟包的发布同一个命令。只不过你更新一次,你必须要遵循semver语义化版本规范,根据你的实际修改变更一下你的版本号才能发布。重复发布同一个版本号是会报403错误的。
包的【查】
从包查找的结果来看,包的查找分两种类型:1)查找一个包名; 2)查看某个特定的包的具体信息;
当我们想通过某个关键字来查找包的时候,那么我们就可以一下命令来查找:
$ npm search <keyword>
如果你想显示更详尽的信息,不妨在以上命令上加上--long
这个参数。$ npm search <keyword>
还支持很多参数,不妨$ npm help search
来查看更详尽的介绍。
当你确定了一个包名之后,你想查看这个包的更具体的信息的时候,那么,我们可以使用以下命令来查看更详尽的信息:
$ npm view <pkg>
$ npm view
的完整语法如下:
$ npm view [<@scope>/]<name>[@<version>] [<field>[.<subfield>]...]
从上看的语法来看,我们查看的信息可以具体到某个字段,甚至是某个字段的子字段的身上。
因为一个包的完整信息也不会太多,所以具体到某个字段的查找也不太常用。除了一种情况,那就是我们查看某个包的所有已发布的版本。比如,我经常去查看react的所有版本号:
$ npm view react versions
当然,以上的查找操作,我们也可以在npm的web官网上完成。通过npm CLI来查找的话,就是比较便捷,通过web官网来查看的话,可视化效果会更好。这两种方法各有优劣,交叉使用,体验会更好。
从【人】的维度
从人的角度来梳理个人与npm这个商业组织的关系的时候,我们会发现一个隐匿在其背后的一个组织架构:
个人与npm用户账号(user account)
众所周知,注册npm用户账号是免费的。注册npm用户账号的主要凭证是【电子邮箱】,理论上说,我们可以免费注册无限多个电子邮箱,因而个人是可以拥有无限个npm用户账号。
用户账号与organization
一个用户账号下可以管辖着多个organization。organization的存在是为了对个人名下所拥有的“包”和其他邀请过来的“人”进行【分而治之】。organization名下会有对应的包列表,但是organization不会直接来管理包,而是通过team这个组织进行间接管理。具体的操作时,npm会将你的【将包添加到organization】操作迁移到【将包添加到organization名下的特定的team】操作上来。对于“人”来说也是一样,通过发送电子邮件邀请而来,添加到organization的某个npm用户,最终会被加入到某个team里面。
organization下面有三个列表:包列表,成员列表和team列表:
这三个列表的关系是:包和成员最终会加入到team里面,由team这个组织进行管理。从这个角度来看,organization就是一个空壳公司。
organization的操作
- 增。只要有一个在npm registry上不重复的名字,我们就可以随意地创建一个organization。
- 删。npm不支持通过npm CLI或者web界面的方式来删除某个organization。要想删除某个organization,用户必须主动联系npm support。
- 改。这里是改organization的名字。npm不支持原地修改原来的organization名字,而是需要用户手动地创建一个新的organization,然后手动把之前填充老的organization的操作再做一遍,最后才是主动联系npm support去把老organization名下的包删除掉,最后的最后,npm才会帮我们把老的organization删除掉。经过这么一系列的繁复的操作,我们才会实现一个“organization重命名”的操作。
- 其他操作。上面提到的是针对organization这个对象可以进行的操作,我们可以在organization的内部进行一些操作。比如说:
- 把一个包(1.当前用户是这个包的owner; 2.不是organization scope的包也能添加进来)添加到organization来;
- 邀请一个npm用户,把他添加到organization来;
- 从某个organization中删除某个member;
- 创建一个新的team;
- 删除一个现有的team;
organization的权限管理
在organization里面的人可以从权限的角度分为三类:
- owner:organization的owner就是创建这个organization的人。当然,最初的owner还可以赋予别的member权限,使得他也可以成为owner。owner的权力最大,可以进行organization层面的操作。
- admin:admin的权力次于owner,它最多也只可以在team层面进行操作。
- member:member就是我们平常所说的【普通成员】,他的权限最小,只能在organization scope去发布新包。
从member到admin,从admin到owner,它们的权限是一层层地叠加上去,具体看npm权限表里面的organization角色权限。
organization与team
team是npm所提供的对同类人(对同一批包具有相同的权限的这些人,我们称之为同一类人)进行归类的管理机制。
organization之下可以创建多个team。就像上面所说的,team是organization里面包和member的实际管理组织:
team的操作
team对包的管理内容包括:
- 把某个npm用户添加到team中。注意,某个npm用户能加入到某个team中的前提条件是这个npm用户是该team所属的organization的成员;
- 从某个team中,把某个成员移除出去;
- 把某个包添加到team里面来;
- 对某个包设置相应的权限。用户成员对包有两种权限:【只读】和【读/写】权限。在team中,把某个包设置为【只读】权限的话,那么这个team里面的所有成员只能查看和下载这个包;把某个包设置为【读/写】权限的话,那么这个team里面的所有成员既能查看和下载这个包,还能去修改它的源码并发布上去。
从【钱】的维度
使用npm服务的过程,如果我们想要私有化自己的npm包,那么我们无非有三种方式:
- 使用public npm registry,但是通过付费来使用私有化服务;
- 付费使用企业版的npm;
- 搭建自己的私有化npm服务(比如: sinopia,verdaccio等)。
在这里,我暂不讨论后两者,只讨论第一种情况。如果我们使用的public npm registry,同时我们又想要私有化服务,那无非是【交钱给npm】了。
public npm registry下使用私有化服务,npm会在组织架构上的两个节点上对我们进行收费:
- user
- organization
如果我们的私有包是想发布到user scope之下的话,那么我们首先要对这个user account缴费;如果我们想在某个organization scope下发布包的话,我们就要对这个organization缴费。
npm的收费标准倒是挺统一的,都是每个【人头】每个月收取7美元。这意味着,如果你只想在user scope下享受私有化服务的话,那么你每个月只要向npm缴纳7美元即可;如果你想在某个organization scope下享受私有化服务的话,那么你这个organization里面有多少个member(不管这个member有没有发包权限),每个月就要在7美元的基础上乘以这个人数。比如说sam-test
这个organization里面有5个人,那么我作为owner,我就要给这个organization每个月要缴纳5 * 7 = 35(美元)
;
这里值得指出的一点的是,npm的这种私有化服务收费,不会因为你这个organization是归属于某个user account而免除缴费的。也就是说,如果你既想把包发布在user scope之下,又想把包发布到sam-test
之下的话,那么你每个月需要交两次费,一次是为user account而交,一次是为sam-test
这个organization而交。
缴费的话,通过完善并关联信用卡信息即可,这里就不赘述了。
npm配置
npm支持很多的配置项,我们不妨用下面的命令来查看所支持的查看所有的配置项:
$ npm config ls -l
如下图,我们可以打开npm配置文档,一边对照,一边查看相关的配置项的含义:
配置项的三个源头
在npm中,一个配置项的值有可能存在于以下的三个源头:
- npm CLI命令行参数
- 系统环境变量(npm的环境变量以“npm_config_”为前缀,并且配置项名称需要用“_”来分隔)
- .npmrc配置文件
如果一个配置项同时存在于以上的三个源头,那么npm取值的优先级是: “npm CLI命令行参数” > “系统环境变量” > “.npmrc配置文件”。
假如,我们需要执行以下命令(在mac osx系统上):
export npm_config_registry=https://registry.npmjs.org && npm install react-dom --registry=http://localhost:1234
与此同时,我们的.npmrc用户配置文件也有一个配置项:
registry = "https://registry.npm.taobao.org/"
那么根据上面所提到的优先级,registry最终的值是:http://localhost:1234
。
四种.npmrc配置文件
.npmrc配置文件是上面所提到的三个源头的其中一个。而配置文件又分为四种类型:
- 项目相关的.npmrc文件(Per-project config file)。这类的配置文件都是放置在项目的根目录之下。
- 用户配置的.npmrc文件(Per-user config file)。文件的路径是:
~/.npmrc
。“~”代表的是用户在系统的home路径,比如我的是:/Users/samliu
。我们也可以通过$ npm config get userconfig
来查看完整的路径。 - 全局配置的.npmrc文件(Global config file)。文件路径是:
$PREFIX/etc/npmrc
。“$PREFIX”是npm所设定的prefix,我们可以使用命令$ npm config get prefix
来查看。比如,我的是/usr/local
。我们也可以通过$ npm config get globalconfig
来查看完整的路径。 - 内置配置的.npmrc文件(Built-in config file)。文件路径:
path/to/npm/itself/npmrc
。详情查看Built-in config file。
以上的四个配置文件也是有优先级的。它们的优先级如下(同理,"a > b"表示"a的优先级比b要高"):
项目相关的.npmrc文件 > 用户配置的.npmrc文件 > 全局配置的.npmrc文件 > 内置配置的.npmrc文件
npm在读取配置项的值的时候,优先级高的文件配置项会覆盖优先级低的文件配置项。
有哪些常用的配置项
日常开发中,我们需要接触的配置项不多,下面简单提几个我日常接触过的几个配置项。
registry
这个配置项肯定是最重要了。因为npm CLI就是根据包名和版本号到当前关联的registry去download相应的包的。日常开发中,我们需要查看,设置和切换registry。
查看当前的registry
$ npm config get registry
设置当前的registry
$ npm config set registry http://localhost:1234/
如果我们只是想在某次的npm CLI命令上使用一次,那么我们可以使用--registry
参数来进行一次性设置:
$ npm install react --registry http://localhost:1234/
在实际开发中,我们可能会搭建自己的私有化npm服务,这个时候,我们可能会遇到一些公共的新包需要到npm public registry上下载,一些公司内部的私包需要切换到公司内部的registry下载。无论是通过设置用户配置.npmrc文件还是命令参数的形式,都是既不方便和繁琐的。这个时候,我们可以考虑到使用npmrc这个npm包来管理registry字段(但不限于registry字段),以便我们实现便捷的registry切换。
我们想根据我们自己的场景,创建三个.npmrc,分别叫:default,work,taobao。那么首先,我们可以现将现有的用户配置.npmrc转换为【default】(这是首次执行$ npmrc
命令的行为):
$ npmrc
然后,我们在创建一个叫【work】的.npmrc,并且设置它对应的registry:
$ npmrc -c work
$ npm config set registry http://localhost:1234
最后,我们创建一个叫【taobao】的.npmrc,并且设置它对应的registry:
$ npmrc -c taobao
$ npm config set registry https://registry.npm.taobao.org/
经过以上操作后,那么我们就有了三个配置好registry的.npmrc文件了,再次执行$ npmrc
命令,我们会看到
那么下次,如果你想在执行某个npm操作之前去切换registry,那么你就可以简单地执行$ npmrc [name]
来切换到对应的registry上来。
prefix
prefix是一个路径,对应于node被安装的目录路径。有时候,我们需要知道npm全局安装的包被安装到哪里了,这个时候我们就要知道prefix。一般而言,我们只需要知道prefix,而不需要修改prefix的值:
$ npm config get prefix
//output: /usr/local
npm官方文档说,在unix系统下,全局安装的包是安装在:{prefix}/lib/node_modules
文件夹之下。
editor
有时候,我们需要去修改项目中的npm包的源码(比如在研究某个js的源码过程中去验证自己的想法的时候),我们需要快速打开这个npm包。恰好,npm CLI提供了一个叫$ npm edit <pkg>
的命令去快速打开这个npm包。但是npm的配置里面,它默认是用vi
命令打开的:
$ npm config get editor
// vi
我们想用VSC来打开,那么我们可以将它设置为VSC的命令code
:
$ npm config set editor code
此时,我们来执行$ npm edit react
的话,那么npm就会用VSC来帮我们打开react这个npm包。
userconfig
有时候,我需要查看我们的用户配置.npmrc文件放置的路径是什么,那么,我们可以用以下命令来查看:
$ npm config get userconfig
// output: /Users/samliu/.npmrc
globalconfig
同上,我们可以执行以下命令来查看全局配置的.npmrc文件的路径:
$ npm config get globalconfig
// output: /usr/local/etc/npmrc
npm CLI
npm CLI支持很多命令,我们可能没办法一一记住。所以,如果你的脑海里面存在以下疑问的时候:
- npm到底支持哪些命令啊?
- 我想执行某个操作,我该使用哪个命令啊?
- 我记住了命令的名字,但是忘记它是干嘛的了?
这个时候,你不妨在你的命令行终端敲下:
$ npm help
我们会看到npm CLI会打印出所有的支持的命令:
$ npm help
Usage: npm <command>
where <command> is one of:
access, adduser, audit, bin, bugs, c, cache, ci, cit,
clean-install, clean-install-test, completion, config,
create, ddp, dedupe, deprecate, dist-tag, docs, doctor,
edit, explore, fund, get, help, help-search, hook, i, init,
install, install-ci-test, install-test, it, link, list, ln,
login, logout, ls, org, outdated, owner, pack, ping, prefix,
profile, prune, publish, rb, rebuild, repo, restart, root,
run, run-script, s, se, search, set, shrinkwrap, star,
stars, start, stop, t, team, test, token, tst, un,
uninstall, unpublish, unstar, up, update, v, version, view,
whoami
假如此时,你忘记了某个命令的写法,不妨通过
$ npm <command> -h
来快速查看这个命令的简单写法。倘若,你还想更近一步了解更多或者面临的是一个之前从未接触的命令,那么此时就可以祭出终极杀手锏来查看针对这个命令最详细的介绍:
$ npm help <command>
介绍几个常用的命令
围绕【身份】的几个命令
npm login
: 登陆npm;npm logout
: 退出登陆;npm whoami
: 看看当前登陆账号的用户名,经常用来确认自己是否已经登陆;npm profile get
: 查看当前账号的完整个人信息;
从当前registry对【包】进行操作
npm init
+npm publish
: 创建一个新包,并把它发布到registry上;npm unpublish
: 从当前registry上删某个包或者删除某个包的特定版本;npm deprecate
: 在当前registry上废弃某个包或者某个包的特定版本;npm view
: 查看某个包的meta信息;
从当前project对【包】进行操作
npm install
: 装包;npm uninstall
: 卸载包;npm update
: 将项目中某个包或者所有包更新到最新版本;npm ls
: 查看该项目下npm包的依赖树;
用nodejs写命令行界面不可或缺的命令
npm link
: 在当前目录下执行该命令,npm会将当前的npm包软链到全局安装的包目录里面,与此同时,package.json中bin
字段也会被软链到{prefix}/bin/{name}
里面去。有了这个功能,我们开发nodejs命令行界面的时候,我们可以实时地修改我们的nodejs源码(注意,如果你是用ts或者ES6+语法去写的话,那么你肯定是还要把源码编译为当前nodejs支持的js语法),实时地去测试。因此,这是开发nodejs命令行界面场景下比不可少的一个npm命令。npm link <pkg-name>
: 如果你是另外一个目录(这个目录是这个包的消费者)下执行这个命令的话,那么npm就会全局包安装的地方去查看这个pkg-name
包,并把这个包软链到当前目录的node_modules之下。这样一来,你对pkg-name
包所的任何修改都会同步到当前的node_modules的这个包来。我们结合上第一个命令npm link
,我们就可以在开发环境下快速去开发一个包,并通过在另外一个地方去使用它来达到一个测试和验证的目的。详情可以执行npm help link
去查看文档给出的例子。
深入理解package.json
name
上面已经解释过,一个js项目文件夹一旦拥有一个叫package.json的文件的时候,那么我们就可以称这个js项目文件夹为一个【npm包】。一个npm包既可以选择发布到对应的registry,也可以选择不发布,而是作为一个普通的js项目(这个项目虽然自己是npm包,但是实际目的是消费别的npm包)。下面,我们讨论前者。
如果一个npm包是要发布到对应的registry的话,那么name
字段和version
字段是必需的。因为name
+version
是一个npm包的唯一标识。
name的命名除了遵循npm所要求的命名规则之外,**最重要的一点是它必须在你当前关联的registry是唯一的,**否则的话,npm CLI在你发布的时候会报错并拒绝发布。
name里面带“@xxx/”前缀是属于npm包里面的“scope”概念,上面的小结已经提到过,在此不再赘述。
version
版本号需要遵循语义化版本规范,因而它是能被node-semver
这个包解析的(在npm的内部实现中,使用了node-semver
这个包解析版本号。当然我们也可以自己安装来用)。
description
描述你的npm包用来干啥的一段话,执行npm search
命令的时候,它会作为搜索结果展示出来。
keywords
用一个数组来罗列跟你个这npm包相关的关键词,好让别用的用户在执行npm search <keyword>
来找到你的包。
homepage
它的值是一个url。一般用于指向你的官网或者项目的github地址。
"homepage": "https://github.com/owner/project#readme"
bugs
当用户在使用你这个npm包的过程中发现了bug的时候,他们可以通过这个字段来上报bug。这个字段用于告诉用户上报bug的页面url或者邮箱地址。形如:
"bugs": {
"url" : "https://github.com/serverless/serverless/issues",
"email" : "1145694956@qq.com"
}
你也可以单单制定一项:
"bugs": "https://github.com/serverless/serverless/issues"
// 或者
"bugs": "1145694956@qq.com"
这个字段用于协助npm bugs <pkg-name>
命令来快递地开发bug上报页面。
license
license字段值有以下几种形式:
-
没有license:
{"license": "UNLICENSED"}
。这种情况是属于当前的包是私包或者还在开发中,不授予任何人任何权利去使用它。 -
自定义license:
{"license" : "SEE LICENSE IN <filename>"}
。然后在你的npm包的根目录下包含这个<filename>
文件即可。 -
单个license:
{"license": "ISC"}
。这种情况是属于当前的包是私包或者还在开发中,不授予任何人任何权利去使用它。 -
多个license:
{"license": (MIT OR Apache-2.0)}
。多个license用OR
关键字去隔开,最后用圆括号去包住它们,遵循SPDX license语法。
author, maintainers, contributors
author是一个“person”,maintainers和contributors是由多个“person”所组成的数组。"person"指的是一个包含人的信息的数据结构。它可以是字符串,也可以是一个json对象。人的信息包括三个字段:name
,email
和url
。其中,name
是必需的,email
和url
是可选的。比如,我们可以这么写:
"author":{
"name" : "Barney Rubble",
"email" : "b@rubble.com",
"url" : "http://barnyrubble.tumblr.com/"
}
可以把name
,email
和url
浓缩在一个字符串中:
"author":"Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)"
同理,maintainers和contributors是一样的,只不过他们的值是一个数组而已,这里就不赘述了。
funding
向外公布给自己资助的渠道。它的值可以是字符串,json对象,由json对象组成的数组。
形如:
"funding": {
"type" : "patreon",
"url" : "https://www.patreon.com/my-account"
}
又或者
"funding": "http://example.com/donate"
又或者:
"funding": [
{
"type" : "individual",
"url" : "http://example.com/donate"
},
"http://example.com/donateAlso",
{
"type" : "patreon",
"url" : "https://www.patreon.com/my-account"
}
]
files
这是一个很重要的字段,但是讽刺的是,这个字段是可选的。这个字段用于告诉npm CLI在发布npm包的时候要上传的文件或者文件夹有哪些。
为什么重要呢,因为这个字段的值反映的是当前这个npm包最终交付给用户的样子。一个npm包源码里面包含了很多乱七八糟的文件,并且你的源码可能是用了ts或者ES6+的语法来写的,此时你npm包的最终交付的“样子”离的你源码是隔着万水千山的。这对于阅读和研究某个npm包源码是十分不利的。
一旦我们知道了files字段的具体含义,那么我们就能够在研究npm包源码的时候反推出哪些是构建出来的文件,哪些是项目里面本来就有的源码文件,从而深入研究。
下面的这些文件是默认上传的(不管你配不配置files字段或者在.npmignore文件中包含了它们):
- package.json
- README
- CHANGES / CHANGELOG / HISTORY
- LICENSE / LICENCE
- NOTICE
- main字段所指定的文件
而下面的这些文件是默认不上传的,不管你在是否配置files字段的时候包含了它们:
当你不配置files字段的时候,默认是除了默认不上传的文件之外,所有的文件都会被上传。
main
main字段代表的是整个npm包的被依赖时候的入口模块(也称之为“main module”)是谁。比如说,你的npm包叫foo
,那么当用户安装后,在自己的源码中写require("foo")
,那么你入口模块导出的东西就会被引入进去。
main字段的值应该是一个相对于你根目录的相对路径。
browser
这个字段是在告诉用户,这是一个前端包。
bin
bin也是一个十分重要的字段。当一个npm包志在被当作一个命令去执行的话(也就是nodejs的命令行工具),那么这个字段是必须。这也是我们在写nodejs命令行工具时需要理解的字段。
bin字段的值可以包含一个或这多个命令。一个命令的时候,可以简写为:
"bin": "./path/to/some.js"
上面这个写法等同于:
"bin": {
"pkg-name":"./path/to/some.js" // 也就是说命令名等同于包名
}
多个命令的时候:
"bin": {
"foo":"./path/to/some1.js",
"bar":"./path/to/some2.js",
"baz":"./path/to/some3.js"
}
这种写法之所以能让相应的可执行js文件(nodejs文件)变得可以执行,是因为当我们安装一个npm包的时候,npm CLI帮我们做了一些事。
-
当我们是把npm包安装在本地的时候,那么npm CLI会帮我们把
bin
字段所引用的js文件软链到./node_modules/.bin
目录之下。 -
当我们是把npm包安装到全局去的时候,那么npm CLI会帮我们把
bin
字段所引用的js文件软链成prefix/bin/
目录之下。
以上两种情况都会以命令名在对应的目录下生成相应的软链文件。举个例子,如果你的bin字段是这样的:
"bin": {
"foo":"./cli.js"
}
当用户全局安装了你的npm包之后,那么npm CLI会把你的cli.js
文件软链到/usr/local/bin/foo
(假设你的prefix是/usr/local
),这样以来,用户就可以在命令行直接执行foo xxx
命令了。
man
给出man文件的一个或者多个链接:
"man":"./man/doc.1"
// 或者
"man": [
"./man/foo.1",
"./man/bar.1"
]
directories
如果你某个目录下都是同一类型的文件,比如说都是bin
文件,doc
文件,doc
文件或者man
文件。那么相比于,你在package.json文件的bin
字段用数组去一一罗列,那么npm在这个字段提供了一个语法糖给我们开发者,也就是只需要给出文件夹的名字即可,比如我们看看npm的package.json文件:
"directories":{
"bin": "./bin",
"doc": "./doc",
"lib": "./lib",
"man": "./man"
}
repository
通过这个字段来表明你的源码托管的地址。形如:
{
"repository": {
"type": "git",
"url": "https://github.com/npm/cli.git"
}
}
它的值跟dependencies
字段一样,有四种类型的写法,详情见dependencies
。
config
config
能够配置一些在script
字段里面使用的环境变量。比如我们在config
里面有个foo
配置项,那么我们就可以在script
字段里面去使用它:
{
"name": "@sam-test/test-npm-publish",
"config": {
"foo":"bar"
},
"scripts": {
"print-env": "echo ${npm_package_config_foo}"
}
}
执行$ npm run print-env
,我们会得到bar
输出。我们可以通过在script
命令里面直接设置,比如:
"scripts": {
"print-env": "npm_package_config_foo=foo-bar && echo ${npm_package_config_foo}"
}
也可以通过$ npm config set @sam-test/test-npm-publish:foo foo-bar
方式来修改。
engines
通过这个字段,我们可以告诉用户,我们的npm包只能在什么版本node或者npm上面运行,比如:
{
"engines": {
"node": ">=0.10.3 <15",
"npm": "~1.0.20"
}
}
os
通过这个字段,我们可以告诉用户我们的npm包只能或者不能运行在特定的操作系统。通过!
操作符,我们来表达不支持某个操作系统,比如:
"os":[
"darwin",
"linux"
]
// 或者
"os": [
"!win32"
]
cpu
跟os
一样的用法,用于告诉用户我们的npm包只能或者不能运行在特定的cpu架构。比如:
"cpu":[
"x86",
"x64",
"ia32",
]
// 或者
"os": [
"!win32",
"!mips"
]
private
一个npm一旦将这个字段设置为true
,那么npm CLI就会拒绝发布它。npm CLI会报以下的错误:
npm ERR! code EPRIVATE
npm ERR! This package has been marked as private
npm ERR! Remove the 'private' field from the package.json to publish it.
所以,如果一个npm包还处在开发当中,又或者当前的npm包只是一个普通的js项目,为了防止意味地将它发布出去,那么我们可以将private
字段的值设置为true
。
publishConfig
这个字段用于放置一些跟发布时候相关的配置项,一般包括tag
,registry
和access
。比如说:
- 因为
tag
配置是默认为latest
,如果我们不想这样的话,我们可以在这里覆盖全局设置。 - 如果我们确定当前这个npm包只会发布到某个内部的registry,那么我们可以在这里写死它。
- 如果当前这个npm是scoped的,但是我们又是一个免费账号,为了避免每次在命令行发布的时候都要追加
--access public
参数,那么我们在这里设置好。
下面给出个简单的例子:
"name": "@sam-test/test-npm-publish",
"version": "1.0.0",
"publishConfig": {
"tag":"not-latest",
"registry":"http://localhost:1234",
"access":"public"
}
workspaces
npm的workspace机制适用于某个root package依赖多个subpackage,与此同时subpackage也需要向外单独发布的业务场景。比如,我们有这样的目录结构:
.
+-- package.json
`-- workspace-a
`-- package.json
.
代表当前目录,因为当前目录下有package.json
文件,所以,当前目录是一个npm包。于此同时,当前的目录下还有一个叫workspace-a
的npm包。
如果我们想在.
目录下执行$ npm install
命令的时候,让npm CLI自动帮我们完成将workspace-a
这个npm包软链到上级的node_module
之下的话,那么我们可以通过配置root package的workspaces
达到这个目的。
为了探索,我们继续丰富.
目录,在.
目录下添加index.js
:
const test = require('workspace-a');
test();
在workspace-a
目录下添加index.js
:
module.exports = function test() {
console.log('hello from npm workspace!')
}
此时我们的目录结构如下:
.
+-- index.js
+-- package.json
`-- workspace-a
`-- index.js
`-- package.json
与此同时,我们在.
目录的package.json文件进行相应的配置:
{
"name": "@sam-test/test-npm-publish",
"version": "1.0.0",
"workspaces":[
"workspace-a"
]
}
那么当我们在.
目录下我们执行完$npm i
之后,我们就会得到这样的目录结构:
.
+-- index.js
+-- node_modules
| `-- workspace-a -> ../workspace-a
+-- package-lock.json
+-- package.json
`-- workspace-a
`-- index.js
`-- package.json
也就是说,配置了workspaces
字段之后,npm会在执行$ npm i
的时候自动把workspace所对应的npm包软链到root package的node_modules
文件夹之下。这对于一个root package以来来个subpackage的项目来说,是十分便利的。
现在,我们了验证软链的是否生效,我们不妨在.
目录下执行以下命令:
$ node index.js
// output:hello from npm workspace!
可以看出,模块依赖是成功的了。我们接着修改一下workspace-a/index.js
的代码
module.exports = function test() {
console.log('hello from npm workspace!again!')
}
我们在执行一次$ node index.js
,可以看到,在没有重新执行$ npm install
的前提下,终端成功打印出:hello from npm workspace!again!
。可见,workspace-a
真的被软链到root package的node_module
之下了。
不过这里指出的一点是,我们平常在做业务开发的项目中很少有用到这个workspaces
字段。这个字段一般采用了monorepo架构的开源js项目才会使用,比如,next.js。我们平常的业务项目之所很少用,是因为我们没有遇到【某个root package依赖多个subpackage,与此同时subpackage也需要向外单独发布的业务场景】。如果你的某个“subpackage”不会向外单独发布,而你又生硬地用上这个架构的话,那就是“多裤子放屁”-多余了。
dependencies
如果一个npm包的源码会被使用方打包到发布的源码中去的话,那么这个npm包应该添加到dependencies
对象中去。
dependencies
字段的值是一个json对象,以包名为key,value可以是以下的四种类型:
-
版本号或者用操作符串联的多个版本号。比如:
"dependencies": { "foo": "1.0.0 - 2.9999.9999", "bar": ">=1.0.2 <2.1.2", "baz": ">1.0.2 <=2.3.4", "boo": "2.0.1", "qux": "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0", "til": "~1.2", "elf": "~1.2.3", "two": "2.x", "thr": "3.3.x", }
-
git URL。比如:
"dependencies": { "npm":"git://github.com/npm/cli.git#v1.0.27" }
-
GitHub URL或者npm URL:
"dependencies": { "react": "github:preact-compat/react#1.0.0", "react-dom": "github:preact-compat/react-dom#1.0.0", "react-ssr-prepass": "npm:preact-ssr-prepass@^1.0.1" }
-
本地文件系统的相对路径。这种做法只是适用与npm包的开发环境。比如:
"dependencies": { "react": "^17.0.1", "test-for-installing-npm-package-locally": "file:../test-for-installing-npm-package-locally" }
devDependencies
如果一个npm包的源码不会被使用方打包到发布的源码中去的话,那么这个npm包应该添加到devDependencies
对象中去。更具体来说,如果一个npm包只是用于构建,测试源码的话,那么这中npm包(比如webpack,babel等等)就应该放在devDependencies
对象中去。
devDependencies
json对象中key的value的取值跟dependencies
是一样的,在此就不赘述了。
peerDependencies
如果我们在开发一个npm包的时候,不会在代码层面去require
一个npm包,但是实际上又是间接地依托在它的能力之上。那么我们一般把当前包称为“plugin”,而作支撑的那个包被称为“host tool”或者“host library”。这种场景下,我们往往是需要表达当前我们开发的这个包是需要某个宿主环境的某个以上的版本去支撑的,这个时候我们就会用到peerDependencies
字段。
同样,peerDependencies
字段也是一个json对象。对象key的value值也是跟dependencies
对象key的value值的取值是一样的,支持四种类型的值,在此就不赘述饿了。
现在为了解释peer Dependencies到底是意味着啥,我们假设我们现在有这样的package.json:
{
"name": "tea-latte",
"version": "1.3.5",
"peerDependencies": {
"tea": "2.x"
}
}
那么tea
这个npm包被当作peer Dependencies意味着:
- 传达【我当前的这个包
tea-latte
需要宿主包tea
2.0以上的版本来兼容】这个信息。 - 当用户执行
$ npm install tea-latte
的时候,如果当前用户已经安装了符合版本要求的tea
宿主包的话,npm什么都不会做。它将会产出这样的依赖图:
但是如果用户没有安装├── tea-latte@1.3.5 └── tea@2.2.0
tea
宿主包或者安装的tea
宿主包不符合版本要求的时候,那么npm就会在控制台给出相应的警告。
peerDependenciesMeta
当npm在控制台给出相应的警告的时候,它需要用到开发者给它的相关信息。它的最常用的一个用途就是将一个peer Dependencie包标志为可选。
{
"name": "tea-latte",
"version": "1.3.5",
"peerDependencies": {
"tea": "2.x",
"soy-milk": "1.2"
},
"peerDependenciesMeta": {
"soy-milk": {
"optional": true
}
}
}
一个peer Dependencie包一旦被标志为可选的话,那么当用户当前没有安装的话,npm也不会给出警告。
bundledDependencies
如果一个npm包同时存在于host package的dependencies
和bundledDependencies
字段中,那么这个包会在开发者执行$ npm pack
命令的时候被打包到.tgz
的压缩包里面。这样的一来,那么当使用者执行$ npm install <host-package>
的时候,这个npm包也会被一同下载了。比如说,当前我们正在开发一个叫awesome-web-framework
的包,它的package.json文件如下:
{
"name": "awesome-web-framework",
"version": "1.0.0",
"bundledDependencies": [
"renderized",
"super-streams"
]
}
在当前包的根目录下执行$ npm pack
,那么打包出来个压缩包会包含了renderized
和super-streams
这两个包;用使用者执行npm install awesome-web-framework
的时候,renderized
和super-streams
这两个包会伴随着awesome-web-framework
包的安装而被安装到本地项目里面。
optionalDependencies
通过这个字段,我们可以把某个开发依赖包来指定为可选的。optionalDependencies
字段的值跟dependencies
字段的取值是一样。但是optionalDependencies
字段的优先级会更高。也就是如果一个npm包同时出现在optionalDependencies
json对象和dependencies
json对象的时候,这个包就变成了一个可选的开发依赖包。
一个包一旦成为可选的开发依赖包,那么npm在安装过程即使是对它安装失败也不会影响整一个安装进程。
script
script字段是一个很重要的字段,适合新开一个篇章去深入探索。这里立个flag~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!