你可能不知道的 npm 小规律
本文会介绍一些容易被忽视、但是非常实用的 npm 小知识点,带你从独特的角度认识每天手边常用的 npm 工具。
- 容易被忽视的
dist tag
- 耳熟能详的npm install
- node_modules 的目录组织规律
- 踩坑指南
1. 容易被忽视的 dist tag
dist tag
是一个特殊的版本标记指针
模块发布:每个版本号在发布时都会有一个 dist tag
指向它
- 自定义
dist tag
:发布时可以指定dist tag
,它可以为任何字符串(如:’alpha’,’beta’),自定义dist tag
通常用于发布内部版本 - 默认
dist tag
为 ‘latest’:发布时不指定dist tag
时会默认dist tag
为 ‘latest’,对外发布的正式版本dist tag
一定是 ‘latest’
模块安装:
- 自定义
dist tag
:- 自定义
dist tag
的版本必须通过指定 tag name 才能安装到,形如
npm install modulename@tagname
- 默认
dist tag
:模块安装不指定dist tag
时默认拉取dist tag
为 ‘latest’ 的版本
- 自定义
举一只🌰:
模块发布:
我们发布 PackageA 的 1.0.0-alpha 时,执行
npm publish --tag alpha
来发布 alpha 版本;修改版本号后,再次执行 npm publish –tag alpha 时,则 alpha 版本会指向新的版本号。模块安装:
通过 npm i PackageA@alpha 来安装这个 alpha 版本 ( npm i PackageA 不能安装到该 alpha 版本)。提示:npm install PackageA 与 npm install PackageA@latest 等价。
gy有三个dist tag
表示不同版本:latest 、docker、alpha,详情可以参考 gy 发版规范2. 耳熟能详的npm install
我们每天都在用 npm install 命令,下面分几个场景讲一下你可能会忽视的细节,让你有更清晰的认识。
姿势:除了 install modulename,你还可以:
- install git-url:接 git 地址,较常见
- install folder:接本地文件目录,类似于 link 原理
- install tarball-file:接本地压缩包,支持.tar, .tar.gz, .tgz
版本:
npm install 安装模块时,默认安装最晚发布的版本号,而不是最大的版本号,所以不要通过发布比线上更低的版本号来进行内部测试,请使用 dist tag
。
性能:
该命令大部分耗时都在解析模块依赖树上,只有很小部分时间花在下载压缩包上(正常网速下),所以只是缓存下载的 zip 包或持久化 npm 的本地缓存并不能节省很多时间。
场景一
- 用法: npm install
- 适用场景:用于安装本地项目工程的所有依赖。
- 规则:
- 命令会安装 dependencies 和 devDependencies,如果不想安装devDependencies,执行 npm install –production 即可
- 该命令执行时会匹配 package-lock.json 信息、 package.json 信息、远程 registry信息是否一致, 会分析已有 node_modules 目录内的模块状态,按照 lock 文件中锁定的版本进行依赖安装。
场景二
- 用法: npm install PackageA
- 适用场景:安装特定的包 PackageA 到本地工程。
- 规则:
- 该命令只安装 PackageA 的 dependencies ,而不会安装 PackageA 的 devDependencies。
- 该命令无视工程目录中 package-lock.json 中锁定的版本,直接安装(或升级) PackageA 到 package.json 中允许的最新版(有时候并不是最大版本号),同时会向 package.json 和 package-lock.json 文件写入该模块的最新版。
场景三
- 用法: npm install -g PackageA
- 适用场景:安装特定的包 PackageA 到全局。
- 规则:
- 该命令只安装 PackageA 的 dependencies ,而不会安装 devDependencies
- 使用 n 管理 node 版本的情况,packageA 模块是安装到系统全局目录中
- 优点:纯 js 模块在不同 nodejs 版本下无需再次安装
- 缺点:模块如果含有一些 c 模块,nodejs 版本切换时可能需要重新安装该模块,如 node-sass。
- 对于 nvm 管理的模块,会把 packageA 安装到当前 nodejs 版本所在目录中,优缺点跟 n 正好相反。
3. node_modules 的目录组织规律
新版的 npm ,工程的所有依赖 (包括依赖的下级依赖)都会被安装到 node_modules 根目录下以最大可能复用模块。
但是,如有项目工程目录的 package.json 如下:
// package.json
{
“name”: “project”,
“dependencies”: {
“B”: “0.0.3” // B中存在依赖 A@0.0.1
},
“devDependencies”: {
“A”: “0.0.2”
}
}
- 模块 B 的 package.json 如下:
{
}"name": "B", "dependencies": { "A": "0.0.1" }
工程根目录执行 npm install 时,得到如下目录1
2
3
4
5
6
7project
├──package.json
├──node_modules // 第一级
├── A@0.0.2
├── B
├── node_modules // 第二级
├── A@0.0.1
可以看到模块 A 同时被 project 的 devDependencies和 B 模块的 dependencies引用,且引用的版本不同,所以会安装两份 A ,project 依赖的 A@0.0.2 安装在工程的 node_moduels 根目录中,B 模块依赖的 A@0.0.1 安装在自己目录下层的 node_modules 中。
工程根目录执行 npm install –production 对比结果:1
2
3
4
5
6project
├──package.json
├──node_modules // 第一级
├── B
├── node_modules // 第二级
├── A@0.0.1
可以看到,虽然通过 --production
指令,在安装 project 的依赖时过滤了 devDependencies 中模块 A 的安装,但是 B 模块中的依赖 A 仍旧安装在的模块 B 目录下的 node_modules 内,并没有提到项目根目录中。
由此发现了 npm 一个非常不人性化的地方:npm install 时目录的组织只会考虑 package.json 文件本身,而不会兼顾命令行使用姿势造成的 devDependencies 是否安装的影响。 这个特点大多情况下并没有什么副作用,但总存在例外的情况,可以查看下面的章节
4. node_modules 目录组织与 peerDependencies 的坑
peerDependencies 有什么用?
考虑下面几个模块的关系:
- babel-plugins 与 babel
- eslint-plugins 与 eslint
以 babel 为例,其中 babel-plugin 会调用 babel ,但是针对每一个 babel-plugin,是不可能都把 babel 作为 dependency 单独安装一遍的。而 babel-plugin 的不同版本可能会对 babel 版本有要求,在这种情况 babel 会作为 babel-plugin 的 peerDependency 出现。
在 npm install babel-plugin-A 时,若工程中没有安装对应版本的 babel,则会发出警告,但不会主动安装 babel。
类似的情况还有 antd 之于 react1
2
3
4
5
6
7{
"name": "antd",
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
}
坑来了
项目中碰到过如下情况,在开发 @scopt/gy-lint 模块时,@scopt/gy-lint 的 package.json 如下:1
2
3
4
5
6
7{
"name": "@scopt/gy-lint",
"dependencies": {
"eslint": "5.9.0",
"eslint-plugin-import": "^2.14.0"
}
}
gy 主框架模块的 package.json 如下1
2
3
4
5
6
7
8
9{
"name": "@scopt/gy",
"dependencies": {
"@scopt/gy-lint": "^1.0.0"
},
"devDependencies": {
"eslint": "5.8.0",
}
}
我们执行 npm install -g @scopt/gy
时,得到的目录如下:1
2
3
4
5
6
7gy
├──package.json
├──node_modules // 第一级
├── eslint-plugin-import@2.14.0
├── @scopt/gy-lint
├── node_modules // 第二级
├── eslint
解释:因为 gy 主框架模块中的 devDepenencies 中存在 eslint 库,即使安装 gy 时并没有安装该 eslint@5.8.0 ,npm 分析 gy-lint 的依赖时依旧会认为外层 eslint@5.8.0 存在 ,从而把 gy-lint 依赖的 eslint@5.9.0 安装到了 gy-lint 模块内部的 node_modules 中。
而 gy-lint 的另外一个依赖 eslint-plugin-import@2.14.0 会被默认安装到 node_modules 根目录中。
如此导致:外层的 eslint-plugin-import 插件在 require(‘eslint’) 时会找不到模块 eslint 而报错,因为 eslint 被安装到了 @scopt/gy-lint 内部。
可以通过统一外层 gy 主框架与 gy-lint 的 eslint 版本以回避这个问题。
5. npm ci
上面提到,npm install 大部分时间都消耗在依赖树计算分析上,使用 npm ci 可以优化这个过程。使用 npm ci 需要了解以下几点:
- 该项目必须有一个package-lock.json 或 npm-shrinkwrap.json。
- 如果程序包锁中的依赖项与其中的依赖项不匹配package.json,npm ci则将退出并显示错误,而不是更新程序包锁。
- npm ci 只能一次安装整个项目:使用此命令无法添加单个依赖项。
- 如果 node_modules已经存在,它将在npm ci开始安装之前自动删除。
- 它永远不会写入package.json或任何包锁:安装基本上是冻结的。
不满足于 npm ci ,想要安装更快?可以了解一下 gy fastinstall 插件 依赖缓存方案设计
6.npm deprecate
开发工具库的同学可以关注一下这个命令,命令用于标记历史的某版本已作废过期,如
npm deprecate modulename@"<2.0.0" "小于 2.0.0 的版本已经过期,请升级"