luokuning / blogs

翻译,随笔,以及懒得整理……
81 stars 2 forks source link

如何同时使用多个不同版本的 Node.js 及 npm 来编译项目 #14

Open luokuning opened 5 years ago

luokuning commented 5 years ago

这篇文章并不是关于使用 nvm 或者 n 等版本工具的指南,而是探讨如何在系统中同时安装两个或多个不同版本的 Node.js。注意是 同时 存在,比如在终端输入 node1node2 时,可以分别执行两个不同版本的 Node.js,而不需要版本管理工具事先进行切换。

缘由

同时在系统中安装多个不同版本的 Node.js 跟 npm 听起来像是个伪需求,毕竟当我们需要某个特定版本的 Node.js 时,使用版本工具进行安装、切换都很方便。

不过前些天公司一个运维的小伙伴刚好跟我讨论这个问题,他想在机器上已经安装了一个 Node.js 的情况下,再安装一个不同版本的 Node.js (通过 node2, npm2 来调用),然后执行 npm2 run build 的时候用 node2 来编译项目,但是经过他实际测试发现 build 会失败,不清楚问题出在哪里,出于好奇的目的我打算自己试一试。

实验

为了不搞乱自己的电脑,我用 docker 启动了一个 ubuntu,在这个干净的 ubuntu 里实验。

从官网下载 v10.16.0v8.12.0 两个版本的 Node.js,解压后分别进入各自的目录执行:

./configure
make

进行编译。编译完成后分别将各自的 node, npm 可执行文件链接到 /usr/local/bin 目录下:

# v10.16.0
ln -s `pwd`/out/Release/node /usr/local/bin/node
ln -s `pwd`/deps/npm/bin/npm-cli.js /usr/local/bin/npm

# v8.12.0
ln -s `pwd`/out/Release/node /usr/local/bin/node1
ln -s `pwd`/deps/npm/bin/npm-cli.js /usr/local/bin/npm1

最后得到的结构如下所示:

/usr/local/bin/
|-- node -> /root/node-v10.16.0/out/Release/node
|-- node1 -> /root/node-v8.12.0/out/Release/node
|-- npm -> /root/node-v10.16.0/deps/npm/bin/npm-cli.js
`-- npm1 -> /root/node-v8.12.0/deps/npm/bin/npm-cli.js

我们在终端里试一下命令:

image

后来发现这张图里有个 typo,最后的 npm 本来应该为 npm1,输出的版本应该是 6.4.1,如果我没记错的话。暂时没纠正这个错图了,因为 docker container 被我删了...

可以看到目前系统里存在两个不同的命令 nodenode1,分别指向 v10.16.0v8.12.0,以及各自搭配的 npm。

编译

安装成功后,现在是时候体验一下不同版本的 Node.js, npm 实际使用效果了 。为了简化操作,我们选用 preact 项目来实验 npm run build

在正式运行 npm 命令之前,我们先查看一下 npm1 这个命令本身的代码,执行 vim /usr/local/bin/npm1 可以看到第一行 (shebang) 是:

#! /usr/bin/env node

这一行的意思是,找到当前系统中叫 node 的命令来执行此文件。回想一下我们之前的链接操作,node 是指向 v10.16.0 版本的,也就是说我们直接执行 npm1 -v 的时候,其实调用的是 v10.16.0 版本的 node 来运行 npm1 。为了匹配正确的 node 版本,这里我们需要把 npm1 的 shebang 改成:

#! /usr/local/bin/node1

改完保存并且克隆 preact 的项目后,我们执行 npm1 install,这一步显示成功。

继续执行 npm1 run build,编译成功。不过有个 warning 引起了我的注意:

npm WARN lifecycle The node binary used for scripts is /usr/local/bin/node but npm is using /root/node-v8.12.0/out/Release/node itself. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.

这里的意思是,如果我们想让运行 run-script 命令时,用的 node 版本与运行当前 npm 的版本一致的话,应该要传递一个 --scripts-prepend-node-path 参数。事实上,通过查看调试 npm 自身的代码可以发现,这个参数会使当前执行 npm 的 node 的可执行文件所在的目录,优先前置到 run-script 开启的子进程的 PATH 环境变量中,这样在实际执行 build 命令时,用的版本跟执行 npm1 的版本保持一致。可以看下 /usr/local/lib/node_modules/npm/node_modules/npm-lifecycle/index.js 中第 118 行的代码:

  if (shouldPrependCurrentNodeDirToPATH(opts)) {
    // prefer current node interpreter in child scripts
    pathArr.push(path.dirname(process.execPath))
  }

既然了解了这个参数的作用,这次我们执行:

npm1 run build --scripts-prepend-node-path

warning 的确没了,但是出现了一个报错:

Error: The module '/root/preact/node_modules/iltorb/build/bindings/iltorb.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 64. This version of Node.js requires
NODE_MODULE_VERSION 57. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).

这里提示说项目的依赖包在安装时候使用的 Node.js 版本与执行 build 的时候的版本不一致。我们最开始安装依赖的时候直接执行的是 npm1 run install,并没有带上 --scripts-prepend-node-path 参数,所以实际安装用的版本是 v10.16.0,而不是想要的 v8.12.0。我们重新带上参数安装并 build 试试:

npm1 install --scripts-prepend-node-path
npm1 run build --scripts-prepend-node-path

可以发现这次成功了,而且用的版本是 npm1 对应的 Node.js v8.12.0

(其实还有一种情况就是,当 npm 版本与 Node.js 版本不兼容时,直接运行 npm 也会报错)

结论

可以看到为了让 npm 能识别指定版本的 Node.js,我们需要改动到 npm-cli.js 中的 shebang。另外当多个版本的 Node.js, npm 同时共存时,还需要注意 npm 的配置文件需要隔离多份并且有些配置不能重复,比如用来放置全局依赖包的 prefix 文件夹路径不能相同,否则也会引起一些问题。鉴于这些问题,如果没有特殊需求,并不推荐同时安装多个不同版本的 Node.js,建议还是使用版本管理工具比较好。如果一定要保持现有的 Node.js 存在的情况下,需要使用另一个版本的 Node.js 来做一些事情,建议利用 docker 的 Node.js 镜像来实现,这样既方便又不怕搞乱现有的环境。

zhuhuiyuan commented 3 years ago

👍