npm 是 Node.js 官方提供的包管理器,它上手简单,但内部却隐藏着不为人知的一面。这篇文章我想说说 npm 几个比较容易混淆的地方,然后再说点小技巧。
dependencies 和 devDependencies
绝大部分小伙伴应该平时都用过他们,字面上来看前者主要用于列出代码运行起来时所必要的依赖,而后者则是只在开发阶段需要用到的依赖。
那么问题来了,什么才算是只在“开发阶段”用到的依赖呢?其实这个范围还是比较模糊的。我认为对于一个纯前端项目来说,测试工具、编译工具、语法检查工具都可以算这一类。而对于一个 Node.js 项目来说,可能这个范围会更加模糊一些,因为基本上除了测试工具之外,上面所说的几类依赖在开发阶段和部署阶段可能都需要用到。
OK,既然我们花了很大心思把两类依赖分开,那么对于 npm 来说两者有何区别呢?这一点可能是很多人忘记的:npm 并不会在默认的 npm install
中区分,而需要我们换成:
npm install --production
或:
NODE_ENV=production npm install
如果没有上面的方法来做,你就白区分了。
peerDependencies 和 optionalDependencies
peerDependencies 什么时候用呢?当你在写一个插件的时候。
比如说你正在写一个组件库,组件库可以看作是Vue或React的插件。虽然你的组件库离了这些UI库就运行不了,但考虑到用户在使用你的组件库时都是已经安装了UI库的,你将UI库写成 peerDependencies 即可。这么做有什么好处呢?对于 npm 来说,这意味着更清晰的依赖树关系,也意味着用户不会因为你的插件自动下载重复的依赖。
那么 optionalDependencies 呢?看起来和 peerDependencies 很容易混淆,但实际它的场合是当你可能会用到一个依赖作为可选插件时。
举例来说,如果你的项目需要做图片处理,但允许某些格式的处理插件在安装时缺失,那这些插件就可以写在 optionalDependencies 里。
npm-shrinkwrap.json 和 package-lock.json
如果一个项目只有package.json
,npm install
安装依赖时总是会按照语义化版本号来尝试安装最新的依赖。对于很多人来说这种行为是预期之内的,因为很多bugfix可以在第一时间囊括。
但对于核心业务,我们可能想要的是每次部署安装完全一致的内容,这时候我们就需要生成上面两者中的一个。那么这两个文件有什么区别呢?其实主要的区别只有一个:package-lock.json
不会在 npm 包发布时包含,但npm-shrinkwrap.json
则会。如果你真的把package-lock.json
放在了 npm 包中,npm在其它项目中安装这个 npm 包时也不会理睬这个package-lock.json
。
npm官方认为绝大部分情况下你都应该用package-lock.json
,除非你需要部署的是一个全局命令行工具——全局安装的包没有上层 npm 包来锁依赖,只能靠自己了。
学会使用 npm ci
可能绝大部分小伙伴在部署npm项目时只是单纯地运行npm install
,但这其实可能会有坑:老版本的npm install
并不完全遵循package-lock.json
。
于此同时,npm install
对node_modules
目录有各种假设,执行的效率也不怎么样。一个更安全、高效的选择是:npm ci
。这个命令会先删除node_modules
,然后再按照package-lock.json
或npm-shrinkwrap.json
安装依赖,绝无二心——哪怕和package.json
相悖。
正确设置镜像
无论是作为一个拥有私有镜像的企业,还是作为一个墙内想用国内npm镜像的用户,设置镜像已经成了我们每天和npm共存的一部分。据我观察,大家常用的镜像设置方法往往如下:
alias cnpm="npm --registry=https://registry.npm.taobao.org"
又或者如下:
npm install -g cnpm --registry="https://registry.npm.taobao.org"
这两种办法都凑合能用,直到你发现你要和团队协作,而他们使用npm
而不是cnpm
。无奈的你可能选择把命令带着参数写进package.json
中。但总是有不适合用参数的场景,比如已经有上游写死了npm install
,我们该怎么办呢?
一种办法是使用npm config
来进行配置:
npm config set registry "https://registry.npm.taobao.org"
但是有没有一劳永逸、可以嵌入脚手架的办法呢?使用.npmrc
!
npm支持在项目的根路径下增加一个名为.npmrc
的文件,这个文件中可以配置 npm 在执行时的各项参数,和调用npm config set
是一个效果:
registry="https://registry.npm.taobao.org"
这样无论是你可爱的同事还是持续集成的脚本,都能正确地使用镜像了。为了保险起见,我还建议大家加上disturl
这个配置:
registry="https://registry.npm.taobao.org"
disturl="https://npm.taobao.org/dist"
这是因为某些原生模块在编译时node-gyp
可能会用它下载额外的头文件。
更优雅地运行 npm 命令行
如果你的项目依赖了某个Node.js命令行,该如何调用呢?比较粗犷的一种玩法是先全局安装,然后再使用。比如依赖 mocha 的项目可能有这样的:
npm i -g mocha
mocha --recursive "./spec/*.js"
这种方式无法锁定mocha
的版本,如果多个项目之间冲突就很尴尬了。退一步,把mocha
作为devDependencies
如何?npm 有个“隐藏规则”,如果一个 npm 包是命令行,那么它会建立一个“快捷方式”,于是就可以这样用了:
{
"devDependencies": {
"mocha": "^6.0.0"
}
}
./node_modules/.bin/mocha --recursive "./spec/*.js"
在 Linux 和 macOS 上,这种方法已经足够好了,但是一旦你的项目还有 Windows 用户参与开发,这种写法还是有问题的:因为 Windows 的目录分隔符是反斜杠。
从 npm 5.2.0 开始,npm 会新增一个命令行工具: npx。无需额外进行安装,上面的package.json
又可以直接写为老样子了,但在调用时我们这样用:
npx mocha --recursive "./spec/*.js"
舒服多了吧。
其实 npm 还有很多细节,比如 npm link 的正确使用方法以及 npm update 的实际语义等,有机会再和大家分享吧。
Photo by Sebastian Unrau on Unsplash