2009年,Ryan Dahl 写了 Node.js 以后,缺少包管理器,Isaac Z. Schlueter 苦于如何推广自己的包管理器,于是俩人一拍即合,最终 Node.js 内置了 npm 。后来,随着 Node.js 的流行, npm 也跟着流行了起来。

npm v1-v2

早期 npm 的处理依赖的设计比较简单,以递归的形式,严格按照 package.json 结构以及子依赖包的 package.json 结构将依赖安装到他们各自的 node_modules 中,直到有子依赖包不再依赖其他模块。

这样的 node_modules 结构简单明了符合预期,但是在遇到大型项目时,可能会有很多重复依赖的包,在遇到相互依赖的情况时甚至会形成“嵌套地狱”的情况。

npmp_inner

总结一下此时遇到的问题:

  1. 嵌套过深,安装太慢
  2. 多个子依赖包依赖相同的包时,会有重复安装的情况
  3. 相互依赖时会形成“嵌套地狱”的情况
  4. 目录层级太深导致文件路径太长,在 Windows 系统下删除 node_modules 文件夹会出现失败的情况

npm v3+ & yarn

2016年,yarn 诞生,从结构上解决了 npm 存在的问题:

  1. 嵌套过深,冗余安装 -》 扁平化安装依赖
  2. 安装太慢 -》 添加缓存、多线程安装

随后,在 npm v3 中,也实现了相关的功能。

扁平化

npm 官网有介绍扁平化相关的 算法

npm3_inner

扁平化后,实际需要安装的包数量大大减少,再加上相应的缓存机制,依赖的安装速度也得到了极大的提升。

冗余问题

依据 npm v3 的扁平化规则。按照上述例子,如果我们又安装了新的依赖 D 和 E,D 和 E 都依赖于 C v2.0。 结构如下:

npm3_dup_inner

为什么不能将 C v1.0 和 C v2.0 都安装在顶层目录?这是因为 C v1.0 和 C v2.0 的结构是类似的,包名是完全相同的,所以不能安装在同一目录下。

那么此时, C v2.0 依然存在了两次。那么为什么 C v1.0 出现在项目顶层 node_modules 而不是 C v2.0 出现在 node_modules 顶层呢?这取决于 A 和 D 的安装顺序,A 先安装,A 的依赖 C v1.0 就先安装,所以后面的 D 和 E 依赖的 C v2.0 只能重复安装于子目录。

同样的 package.json,不同的安装顺序可能影响 node_modules 内的文件结构。

接下来我们要升级 B 变为 B v2.0 ,B v2.0 也依赖 C v2.0 。此时,npm 会删除 C v1.0 ,安装 C v2.0 。变成下面的结构:

npm3_dup_1_inner

明显,D 和 E 下面的 C v2.0 是多余的,这时我们就可以使用 npm dedupe 来消除重复模块。

npm3_dup_2_inner

依赖锁

我们在执行 npm install 的时候,npm 会根据 package.json 中的依赖进行安装和升级。安装模块的时候,会根据 模块版本是否符合新模块的版本范围 来进行判断是否更新版本。

判断模块是否符合版本范围的标准是根据 语义化版本 约定的。

简单来说,a.b.c 代表一个版本,每个位置代表的含义如下:

  1. 主版本号:当你做了不兼容的 API 修改
  2. 次版本号:当你做了向下兼容的功能性新增
  3. 修订号:当你做了向下兼容的问题修正

先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

在 package.json 中,我们可以看到 ~ 和 ^ 这种标识,就是用来判断如何更新版本模块的。

  1. ~ 会匹配最近的小版本依赖包,比如 ~1.2.3 会匹配所有 1.2.x 版本,但是不包括 1.3.0
  2. ^ 会匹配最新的大版本依赖包,比如 ^1.2.3 会匹配所有 1.x.x 的包,包括 1.3.0 ,但是不包括 2.0.0

此时如果有个包没有遵守约定,在提供了不兼容的功能时未升级版本,那么当其他人安装依赖的时候,会导致非常大的不确定性。

于是在2016年 yarn 提出了 yarn.lock 依赖锁的概念:

每次安装依赖的时候,将依赖精确地将版本号锁定在一个值,并且在安装时通过计算哈希值校验文件一致性,从而保证每次构建使用的依赖都是完全一致的。

17年, npm 也提供了该功能,也就是 package-lock.json。

npm v5-8

npm 之后的版本也提供了很多新的特性,例如 scope, npx, workspaces 能力支持等。

常用npm命令

以下介绍比较常用的几个命令

npm config

npm config 可以管理 npm 配置文件。

常见用法:

npm config ls -l 列出当前目录所有的配置
npm config get <key> 获取单条配置信息
npm config set <key>=<value> [<key>=<value> ...] 设置配置信息

例如:

npm config set registry https://registry.npm.taobao.org 将默认的官方 npm 源 https://registry.npmjs.org/ 改为 https://registry.npm.taobao.org
npm config set user.email example@example.com

npm install

npm install 安装特定的 npm 包或者安装项目所有依赖的 npm 包。

npm_install_inner

npm init

npm init 创建 package.json 文件。

npm ls

npm ls 列出所有已安装的包。

npm dedupe

npm dedupe 减少包树的重复依赖。

npm publish

npm publish 发布一个包。

npm start

npm start 开始一个包。

npm start 将会运行 package.json 中 script 里的 start 命令,如果没有 start 命令,npm 将会运行 node server.js

npm run-script

npm run-script 将会运行任意的包脚本(在 package.json 的 script 当中定义)。

更常见的是它的别名 npm run

npx

npx 支持从远程或者本地的 npm 包里执行一个命令。npx 命令是 npm exec 的别名,如果命令在本地项目的依赖中不存在,则会将命令安装到 npm 缓存中的文件夹。

npm link 支持将某个依赖指向一个本地的 npm 包。这对于本地调试 npm 包而言非常方便,不需要发布即可以通过 npm 依赖。