NPM 包管理机制
解析 package.json
我们知道当运行 npm install
命令的时候,会根据 package.json
文件中的配置自动下载所需的模块,也就是配置项目所需的运行和开发环境。
package.json
文件中必填选项只有两个:name
和 version
,代表项目名称和版本号。
name & version
name
和 version
字段是必填项,它俩一起构成一个标识符,如果作为一个 NPM 包发布的话,这个标识符被认为是唯一的。如果更改包内部的代码,就要同步更改版本号。
name
命名规范:必须小于 214 个字符,不能以 .
或 _
开头,不能有大写字母。因为名称最终成为 URL 的一部分因此不能包含任何非 URL 安全字符。若包名称中存在一些符号,将符号去除后不得与现有包名重复。
如果你的包名与现有的包名太相近导致你不能发布这个包,那么推荐将这个包发布到你的作用域下。用户名 conard
,那么作用域为 @conard
,发布的包可以是@conard/react
。
可以使用 npm view 包名
查看一个包的信息。
version
一般的格式是 x.x.x
, 并且需要遵循 SemVer 规范。
一些不太重要的属性
description
:一个字符串,用于编写描述信息。有助于人们在 NPM 库中搜索的时候发现你的模块。keywords
:一个字符串组成的数组,有助于人们在 NPM 库中搜索的时候发现你的模块。bugs
:用于项目问题的反馈issue
地址或者一个邮箱。homepage
:项目的主页地址。license
:当前项目的协议,让用户知道他们有何权限来使用你的模块,以及使用该模块有哪些限制。author
&contributors
:author
是具体一个人,contributors
表示一群人,他们都表示当前项目的共享者。同时每个人都是一个对象。具有name
字段和可选的url
及email
字段。man
:用来指定当前模块的man
文档的位置。directories
:制定一些方法来描述模块的结构, 用于告诉用户每个目录在什么位置。repository
:指定一个代码存放地址,对想要为你的项目贡献代码的人有帮助。bundledDependencies
:指定发布的时候会被一起打包的模块。optionalDependencies
:如果一个依赖模块可以被使用, 同时你也希望在该模块找不到或无法获取时 NPM 继续运行,你可以把这个模块依赖放到optionalDependencies
配置中。engines
:指明了该模块运行的平台,比如 Node 或者 npm 的某个版本或者浏览器。os
:指定你的模块只能在哪个操作系统上运行。cpu
:限制模块只能在某种架构的 cpu 下运行。private
:如果这个属性被设置为 true,npm 将拒绝发布它,这是为了防止一个私有模块被无意间发布出去。publishConfig
:这个配置是会在模块发布时生效,用于设置发布用到的一些值的集合。如果你不想模块被默认标记为最新的,或者默认发布到公共仓库,可以在这里配置tag
或仓库地址。preferGlobal
:布尔值,表示当用户不将该模块安装为全局模块时(即不用–global 参数),要不要显示警告,表示该模块的本意就是安装为全局模块。browser
:指定该模板供浏览器使用的版本。
files
一个数组,内容是模块下文件名或者文件夹名,如果是文件夹名,则文件夹下所有的文件也会被包含进来(除非文件被另一些配置排除了)。
可以在模块根目录下创建一个 .npmignore
文件,写在这个文件里边的文件即便被写在 files
属性里边也会被排除在外,这个文件的写法与 .gitignore
类似。
main
入口文件加载路径,require
导入的时候就会加载这个文件。这个字段的默认值是模块根目录下面的 index.js
。
main
的值应该是一个相对于根目录的文件路径。
bin
指定每个内部命令对应的可执行文件的位置。如果你编写的是一个 node
工具的时候一定会用到 bin
字段。
当我们编写一个 cli
工具的时候,需要指定工具的运行命令,比如常用的 webpack
模块,他的运行命令就是 webpack
。
"bin": {
"webpack": "bin/index.js",
}
当我们执行 webpack
命令的时候就会执行 bin/index.js
文件中的代码。
在模块以依赖的方式被安装,如果存在 bin
选项。在 node_modules/.bin/
生成对应的文件,NPM
会寻找这个文件,在 node_modules/.bin/
目录下建立符号链接。由于 node_modules/.bin/
目录会在运行时加入系统的 PATH
变量,因此在运行 NPM
时,就可以不带路径,直接通过命令来调用这些脚本。
所有 node_modules/.bin/
目录下的命令,都可以用 npm run [命令]
的格式运行。在命令行下,键入 npm run
,然后按 tab
键,就会显示所有可以使用的命令。
scripts
指定了运行脚本命令的 npm
命令行缩写,比如 start
指定了运行 npm run start
时,所要执行的命令。
scripts
可以直接使用 node_modules
中安装的模块,这区别于直接运行需要使用 npx
命令。
"scripts": {
"build": "webpack"
}
// npm run build
// npx webpack
config
config
字段用于添加命令行的环境变量。
"config" : { "port" : "8080" }
然后,在 server.js
脚本就可以引用 config
字段的值。
console.log(process.env.npm_package_config_port) // 8080
用户可以通过 npm config set
来修改。
dependencies & devDependencies
dependencies
指定项目运行所依赖的模块,devDependencies
指定项目开发所依赖的模块。
它们的值都是一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。
当安装依赖的时候使用 --save
参数表示将该模块写入 dependencies
属性,--save-dev
表示将该模块写入 devDependencies
属性。
版本说明:
~5.38.1
表示安装5.38.x
的最新版本(不低于5.38.1
),但是不安装5.39.x
。ˆ5.38.1
表示安装5.x.x
的最新版本(不低于5.38.1
),但是不安装6.x.x
。latest
表示安装最新版本。
peerDependencies
当我们开发一个模块的时候,如果当前模块与所依赖的模块同时依赖一个第三方模块,并且依赖的是两个不兼容的版本时就会出现问题。
比如,你的项目依赖 A 模块和 B 模块的 1.0 版,而 A 模块本身又依赖 B 模块的 2.0 版。大多数情况下,这不构成问题,B 模块的两个版本可以并存,同时运行。但是,有一种情况,会出现问题,就是这种依赖关系将暴露给用户。
因此,需要一种机制,在模板安装的时候提醒用户,如果 A 和 B 一起安装,那么 B 必须是 2.0 模块。
peerDependencies
字段,就是用来供插件指定其所需要的主工具的版本。
npm install 做了什么?
早期的 npm
早期的 npm 安装依赖的方式非常粗暴,递归通过 package.json
的结构以及子依赖包的 package.json
结构将依赖安装到它们各自的 node_modules
文件夹中,直到这个依赖包不再依赖其它包。
但这样有个问题,如果你引用了许多高层级依赖的包(这里指的是那些依赖了很多包,这些包也同时依赖了很多包的情况),这样一层一层会变得非常复杂,需要安装很多依赖,如果许多包同时引用了相同的依赖,那么 npm 就要重复安装很多次这些依赖。
在 Windows 系统中,文件路径最大长度为 260 个字符,嵌套层级过深可能导致不可预知的问题。
扁平化依赖结构
npm 3.x
版本将早期的嵌套结构改成了扁平结构。
安装模块时,不管是直接依赖还是依赖的依赖都优先安装到 node_modules
根目录。
当依赖中包含版本不同的相同模块时,npm 会判断已经安装的模块版本是否符合新版本的模块范围,符合就跳过,不符合就安装该模块。
比如项目本身依赖于 1.0 版本的 B,现在要安装一个依赖 A,而 依赖 A 又依赖于 2.0 版本的 B,假设 B2.0 不兼容 B1.0 版本,那么最后 node_modules
的结构会是这样:
node_modules
├─B@1.0
└─A@1.0
└─node_modules
└─B@2.0
这样做有一个问题:
如果我的项目又引入了 C1.0,而 C 也依赖于 B2.0 呢?
node_modules
的结构会是这样:
node_modules
├─B@1.0
├─A@1.0
├ └─node_modules
├ └─B@2.0
└─C@1.0
└─node_modules
└─B@2.0
这样看来,似乎 npm 3.x
仍然没有解决包冗余的问题。
假如项目同时依赖了 1.0 版本的 A 和 1.0 版本的 C,他们又分别依赖于 1.0 版本的 B 和 2.0 版本的 B。那么这个时候 package.json
依赖的顺序就决定了 node_modules
的结构。
"dependencies": {
"A": "1.0.0",
"C": "1.0.0"
}
对应结构:
node_modules
├─B@1.0
├─A@1.0
└─C@1.0
└─node_modules
└─B@2.0
"dependencies": {
"C": "1.0.0",
"A": "1.0.0"
}
对应结构:
node_modules
├─B@2.0
├─C@1.0
└─A@1.0
└─node_modules
└─B@1.0
为了让开发者在安全的前提下使用最新的依赖包,我们在 package.json
通常只会锁定大版本(版本号前使用 ^
前缀),这意味着在某些依赖包小版本更新后,同样可能造成依赖结构的改动,依赖结构的不确定性可能会给程序带来不可预知的问题。
我们可以使用 npm dedupe
去除冗余模块。把所有二级的依赖模块 B@1.0 重定向到一级目录下(前提是所依赖的 B@1.0 模块升级到了 B@2.0)。
package-lock.json 文件
为了解决 npm install
的不确定性问题,在 npm 5.x
版本新增了 package-lock.json
文件,而安装方式还沿用了 npm 3.x
的扁平化的方式。
package-lock.json
的作用是锁定依赖结构,即只要你目录下有 package-lock.json
文件,那么你每次执行 npm install
后生成的 node_modules
目录结构一定是完全相同的。项目中使用了 package-lock.json
可以显著加速依赖安装时间。package-lock.json
中已经缓存了每个包的具体版本和下载链接,不需要再去远程仓库进行查询,然后直接进入文件完整性校验环节,减少了大量网络请求。
npm 缓存
在执行 npm install
或 npm update
命令下载依赖后,除了将依赖包安装在 node_modules
目录下外,还会在本地的缓存目录缓存一份。
在命令行使用 npm config get cache
可以查询到缓存的目录。
.npmrc
.npmrc
文件,就是 npm
的配置文件所在位置。当然,寻找这个文件的目的,多数是为了修改 .npmrc
文件内容。但 npm
提供了方便快捷的修改方式,不知道这个文件的位置,其实也是可以修改的。在命令行输入 npm config edit
。
整体流程
- 检查
.npmrc
文件:优先级为:项目级的.npmrc
文件 > 用户级的.npmrc
文件> 全局级的.npmrc
文件 >npm
内置的.npmrc
文件。 - 检查项目中有无
lock
文件。- 无
lock
文件:- 从
npm
远程仓库获取包信息。 - 根据
package.json
构建依赖树,构建过程:- 构建依赖树时,不管其是直接依赖还是子依赖的依赖,优先将其放置在
node_modules
根目录。 - 当遇到相同模块时,判断已放置在依赖树的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的
node_modules
下放置该模块。 - 注意这一步只是确定逻辑上的依赖树,并非真正的安装,后面会根据这个依赖结构去下载或拿到缓存中的依赖包。
- 构建依赖树时,不管其是直接依赖还是子依赖的依赖,优先将其放置在
- 在缓存中依次查找依赖树中的每个包。
- 不存在缓存:
- 从
npm
远程仓库下载包。 - 校验包的完整性。
- 校验不通过:
- 重新下载。
- 校验通过:
- 将下载的包复制到
npm
缓存目录。 - 将下载的包按照依赖结构解压到
node_modules
。
- 将下载的包复制到
- 从
- 存在缓存:将缓存按照依赖结构解压到
node_modules
。
- 不存在缓存:
- 将包解压到
node_modules
。 - 生成
lock
文件。
- 从
- 检查
package.json
中的依赖版本是否和package-lock.json
中的依赖有冲突。- 有冲突,重新远程获取包的信息,构建依赖树,在缓存中查找包信息,后续过程相同。
- 没有冲突,跳过获取包信息、构建依赖树过程,直接开始在缓存中查找包信息,后续过程相同。
- 无