npm 安装的包为什么不一致

4 min read · March 25th 2017

NPM 是什么

NPM(node package manager),通常称为 node 包管理器。顾名思义,它的主要功能就是管理 node 包,包括:安装、卸载、更新、查看、搜索、发布等,npm 将开发者从繁琐的包管理工作(版本、依赖等)中解放出来,更加专注于功能的开发。

NPM 模块复制

从 npm3 开始,npm 对包安装进行了优化,如果模块 A 和模块 C 都依赖了模块 B, 模块 B 将只安装一次。 npm-copy

在早期版本,依赖的模块是直接装在模块自己的 node_modules 下的 npm-old

这也是为什么用户在切换到 npm3 后 node_modules 下无缘无故多出那么多包的原因

安装顺序对包结构影响

生成测试包

生成包a@1.0.0, b@1.0.0,b@2.0.0, c@1.0.0,其中a@1.0.0依赖b@1.0.0, c@1.0.0依赖b@2.0.0

创建包目录
mkdir -p /tmp/repositories/{a-1.0.0, b-1.0.0, b-2.0.0, c-1.0.0}

b@1.0.0
vi /tmp/repositories/b-1.0.0/index.js
module.exports = 'b-1.0.0'

vi /tmp/repositories/b-1.0.0/package.json
{
  "name": "b",
  "version": "1.0.0",
  "main": "index.js"
}

b@2.0.0
vi /tmp/repositories/b-2.0.0/index.js
module.exports = 'b-2.0.0'

vi /tmp/repositories/a-2.0.0/package.json
{
  "name": "b",
  "version": "2.0.0",
  "main": "index.js"
}


a@1.0.0
vi /tmp/repositories/a-1.0.0/index.js
exports.a = 'a-1.0.0'
exports.b = require('b')

vi /tmp/repositories/a-1.0.0/index.js
{
  "name": "a",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "b": "file:///tmp/repositories/b-1.0.0/b-1.0.0.tgz"
  }
}


c@1.0.0
vi /tmp/repositories/c-1.0.0/index.js
exports.c = 'c-1.0.0'
exports.b = require('b')

vi /tmp/repositories/c-1.0.0/index.js
{
  "name": "c",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "b": "file:///tmp/repositories/b-2.0.0/b-2.0.0.tgz"
  }
}

测试包打包 tarball

为了方便安装,我们通过npm pack将包转换为 tgz 文件

cd /tmp/repositories/a-1.0.0 && npm pack
cd /tmp/repositories/b-1.0.0 && npm pack
cd /tmp/repositories/b-2.0.0 && npm pack
cd /tmp/repositories/c-1.0.0 && npm pack

先安装包 a, 再安装包 c

mkdir /tmp/stage1
cd /tmp/stage1
npm i /tmp/repositories/a-1.0.0/a-1.0.0.tgz
npm i /tmp/repositories/c-1.0.0/c-1.0.0.tgz

查看目录结构
tree /tmp/stage1
.
├── node_modules
│   ├── a
│   │   ├── index.js
│   │   └── package.json
│   ├── b
│   │   ├── index.js
│   │   └── package.json
│   └── c
│       ├── index.js
│       ├── node_modules
│       │   └── b
│       │       ├── index.js
│       │       └── package.json
│       └── package.json
└── README.md

查看外部包 b 的版本
cat node_modules/b/package.json | grep version #1.0.0

查看包 c 依赖包 b 的版本
cat node_modules/c/node_modules/b/package.json | grep version #2.0.0

可以看到在安装包 a 时,npm 同时将其依赖包b@1.0.0安装在最外层node_modules下,而安装包 c 时,npm 检查到包 b 已存在但是不同版本,将包 b 安装到包 c 自己的 node_modules 下 npm-a-and-c

先安装包 c, 再安装包 a

mkdir /tmp/stage2
cd /tmp/stage2
npm i /tmp/repositories/c-1.0.0/c-1.0.0.tgz
npm i /tmp/repositories/a-1.0.0/a-1.0.0.tgz

查看目录结构
tree /tmp/stage2
.
├── node_modules
│   ├── a
│   │   ├── index.js
│   │   ├── node_modules
│   │   │   └── b
│   │   │       ├── index.js
│   │   │       └── package.json
│   │   └── package.json
│   ├── b
│   │   ├── index.js
│   │   └── package.json
│   └── c
│       ├── index.js
│       └── package.json
└── README.md

查看外部包 b 的版本
cat node_modules/b/package.json | grep version #2.0.0

查看包 c 依赖包 b 的版本
cat node_modules/a/node_modules/b/package.json | grep version #1.0.0

可以看到在安装包 c 时,npm 同时将其依赖包b@2.0.0安装在最外层node_modules下,而安装包 a 时,npm 检查到包 b 已存在但是不同版本,将包 b 安装到包 a 自己的 node_modules 下 npm-c-and-a

结论

从上面的测试中可以看到,安装同样的包,因为安装顺序的不一致,会产生不一致的目录结构。如果安装顺序一致,node_modules目录结构也会是一致的。