在开发会被其他项目依赖的 npm 包时,有时需要在其他项目下安装测试。为了方便测试,我们通常会将包本地安装或者发布到 npm 仓库下再安装。但是实际上前者会有很多 npm 处理依赖的坑,而后者即便可以用 canary
通道,某些精神洁癖的同学还是会不太乐意。
那么我们可以换种思路,为何我们不在本地自建一个 npm registry?这样可以随意发布测试,不会踩到本地安装 npm 的坑,还不会影响到线上包。Verdaccio 就是一个随开随用甚至无需配置的 npm 仓库,用户不需要自己搭建数据库,只需要一行指令即可在本地启动一个 npm 仓库。
太长不看版
12345 npm i -g verdaccioverdaccionpm adduser --registry http://127.0.0.1:4873npm publish --registry http://127.0.0.1:4873
本地安装的坑
或许你会好奇,npm 是支持本地安装包的,原理就是在 node_modules
下添加一个 symlink,指向对应的目录:
1 2 |
npm i ../path/to/my/package |
同时刷新 package.json
的依赖:
1 2 3 4 5 6 |
{ "dependencies": { "my-package": "file:../path/to/my/package" } } |
这样安装在一般情况下是没问题的,node 在运行时能找到对应的包。问题在于这样的操作只是添加一个 symlink 而已,而这个包里的依赖不一定会同步复制到当前的 node_modules
下。如果这个包里的依赖只是在这个包用上的话,一般不需要考虑这个问题。但是如果这个包下的依赖同时是项目下的其他依赖需要的依赖,那么就会遇到找不到依赖的问题。
具体到实际的例子,我们正在维护一份包含 ESLint 等 lint 规则的包,这个包会包含 lint 规则所需的所有依赖(如 eslint-plugin-react
、eslint-plugin-import
等),这样用户不需要手动安装这些依赖。然而由于本地安装不会处理本地包所使用的依赖,也就是不会复制到 node_modules
目录下,而这些依赖是被项目的 ESLint 等依赖使用的,最终会导致 ESLint 等会找不到对应的插件。
这种情况的解决方法只能是手动将对应的依赖复制到 node_modules
下,但是一个个手动复制未免也过于繁琐了。所幸已经有人开发了 npm-install-offline
这样的包来解决这种问题:
1 2 |
npx npm-install-offline my-package --repo ../path/to/my/package --production |
它的原理就是将项目所需要的上游依赖包复制到当前项目的 node_modules
下,这样项目在运行时就能找到这些依赖了。但是这个包在应对 lerna 这类 monorepo 时会有意想不到的 bug,lerna 在 bootstrap 后会将关联的包在 node_modules
下用 symlink 连接起来,而这个包无法处理 symlink,导致复制对应的依赖时会卡住。虽然这种情况下 npm-install-offline
还可以指定 --symlink
参数也使用 symlink 连接:
1 2 |
npx npm-install-offline my-package --repo ../path/to/my/package --production --symlink |
这种情况下依赖就能顺利链接到 node_modules
下了,一般也能解决不少问题。然而对于之前提到的实际中的例子,包里的依赖其实不仅仅是被项目依赖如 ESLint 调用的,同时自身也需要调用项目中的依赖,例如 eslint-plugin-import
就需要 require('eslint')
。然而 symlink 的依赖在寻找上游依赖时可能就犯了难,会抛出 Cannot find module 'eslint'
的错误,盲猜是依赖执行时并不是 symlink 后的项目所在的 node_modules
下寻找,而是在原来的目录下寻找,最终导致找不到依赖。
总之 npm 处理本地包的依赖大概就是一坨💩,如果你的包也是这样复杂,建议尽早放弃本地安装的思路,否则可能还会有更多的坑。
自建 npm 仓库
既然本地安装目录不靠谱,那么 npm pack
呢?不好意思,npm pack
并不会打包 node_modules
,安装后依赖还是从远程仓库上拉的,对于 monorepo 并不友好。所以此时更好的方法应该是直接发包并安装测试,可是这样有可能会影响已经发布的包版本。那么我们何不自己搭建一个 npm 仓库?
自建 npm 仓库是可行的,最常见的方案就是 cnpm,而自己搭建 cnpm 则还需要准备数据库等环境依赖。那么有更快更简单的方法吗?当然有,比如这次要用上的 Verdaccio,它是一个开源轻量免配置的 npm 私服,用户不需要准备本地的使用环境,只需要安装即可使用:
1 2 3 4 5 6 7 |
npm i -g verdaccio verdaccio # 或者也可以使用 docker docker pull verdaccio/verdaccio docker run -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio |
启动后 Verdaccio 会默认运行在 127.0.0.1:4873
上,你可以打开浏览器确认:
此时我们只需要将 npm 的 registry 指向本地,就可以开始操作了。
1 2 3 4 5 6 7 8 9 10 11 12 |
# 设置全局 registry npm set registry http://localhost:4873/ # 或者设置环境变量 NPM_CONFIG_REGISTRY=http://localhost:4873 # 或者在测试的项目下设置 .npmrc 文件并填写 registry registry=http://localhost:4873 # 或者在每次执行 npm 时指定 registry 参数 npm --registry http://localhost:4873 |
首先我们需要登录本地仓库的 registry,用户名密码其实可以随意:
1 2 |
npm adduser --registry http://localhost:4873 |
然后我们就可以将包发布到本地的仓库下了:
1 2 3 4 5 |
npm publish --registry http://localhost:4873 # 如果这是一个 lerna monorepo npx lerna publish from-package --registry http://localhost:4873 |
如果不出意外,你可以在刚才的 web 页面上看到刚刚发布的包了。
在其他项目下测试时,只需要在安装时指定 registry 即可:
1 2 |
npm i my-package --registry http://localhost:4873 |
这样你就在本地搭建了一个运行在本地的 npm 仓库,现在你可以随意折腾你的包并随意发布了,本地发布到 Verdaccio 的包是不会发布到上游的。
进阶配置
Verdaccio 的配置文件默认放在用户目录下,一般是取 $XDG_DATA_HOME
环境变量,Windows 是取 %APPDATA%
环境变量。此外在启动 Verdaccio 时也会输出配置文件的位置,个人实践的配置文件位置如下:
1 2 3 4 5 |
# Ubuntu 18 ~/.config/verdaccio/config.yaml # Windows %APPDATA%\verdaccio\config.yaml |
开放公网访问
Verdaccio 默认是绑定在 127.0.0.1:4873
下的,如果需要绑定到公网 IP 可以改为监听 0.0.0.0
,此外也可以修改端口。
1 2 3 4 5 6 7 8 |
listen: - localhost:4873 # 默认配置 - http://localhost:4873 # 同上 - 0.0.0.0:4873 # 监听所有 ipv4 地址 - https://example.org:4873 # 如果需要使用 https - "[::1]:4873" # ipv6 - unix:/tmp/verdaccio.sock # unix socket |
自动代理上游的包
如果你是采用全局配置 npm registry 的话,如果使用了公司内部的包,那么本地安装上游的包时可能会遇到找不到依赖的问题,因为 Verdaccio 虽然默认会代理上游 npmjs 的包,但是私有仓库是不支持的。此时你需要在安装原始项目依赖时用默认的 registry,然后安装测试的依赖时指定 registry。其实我们可以通过配置上行链路让 Verdaccio 自动查找上游的包,这样就只需要将 registry 指向本地即可。
Verdaccio 的默认上行链路配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
uplinks: npmjs: url: https://registry.npmjs.org/ packages: '@*/*': # scoped packages access: $all publish: $authenticated unpublish: $authenticated proxy: npmjs '**': # allow all users (including non-authenticated users) to read and # publish all packages # # you can specify usernames/groupnames (depending on your auth plugin) # and three keywords: "$all", "$anonymous", "$authenticated" access: $all # allow all known users to publish/publish packages # (anyone can register by default, remember?) publish: $authenticated unpublish: $authenticated # if package is not available locally, proxy requests to 'npmjs' registry proxy: npmjs |
我们可以在 uplinks
里指定上行链路,然后在 packages
里指定对应的 scope 走对应的链路。例如我们可以将公司的私有 scope @foo
指向公司的 cnpm,然后默认的依赖走淘宝的中转,配置可以写成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
uplinks: npmjs: url: https://registry.npm.taobao.org/ company: url: https://registry.my.company.com/ packages: '@foo/*': access: $all publish: $authenticated unpublish: $authenticated proxy: company '@*/*': access: $all publish: $authenticated unpublish: $authenticated proxy: npmjs '**': access: $all publish: $authenticated unpublish: $authenticated proxy: npmjs |
然后我们可以在需要测试的项目下删除原有的 .npmrc
,然后安装依赖时直接使用本地的 Verdaccio,或者直接将 .npmrc
指向本地的 Verdaccio,它会帮我们代理好上游的包,不再需要来回切换 registry 了:
1 2 |
npm i --registry http://localhost:4873 |