如何锁定并自动切换Node.js版本?

1,582 阅读7分钟

大家好,我是老纪。

团队协作中,一个令人困扰的问题是:如何锁定项目的Node.js版本?

随着Node.js的不断迭代升级,我们开发者往往跟不上官方发布的速度,这导致旧的项目可能因为升级而无法正常运行,又或者团队内部有人用版本v20好好的,而有人用版本v18就有Bug了。如果是启动阶段能够发现问题,那还简单些,最怕是项目在运行时出现了不兼容或错误。所以团队需要采取措施来确保项目中使用的Node.js版本和相关依赖的稳定性和一致性,以避免可能出现的生产环境中的灾难性后果。

因此,在日常开发中,就会衍生两方面的需求:

  1. 锁定项目Node.js版本
  2. 快速切换Node.js版本

以上两点是老生常谈的内容,本文在上面的基础上提出更高的需求:如何锁定并自动切换Node.js版本?

下面将带你一一解答。

锁定项目Node.js版本

如果仅仅是锁定当前项目的Node.js的版本,npm官方就有一个配置项,在package.json中设置engines属性来指定版本范围:

{
  "engines": {
    "node": ">=12.10.3 <15"
  }
}

也可以固定为指定版本:

{
  "engines": {
    "node": "18.19.1"
  }
}

再搭配.npmrc,加个配置项:

engine-strict=true

这样,当你安装依赖时(npm、yarn、pnpm都实现了这个配置),如果检测到Node.js版本与配置的不匹配,就会失败:

jw aa % npm i axios
npm ERR! code EBADENGINE
npm ERR! engine Unsupported engine
npm ERR! engine Not compatible with your version of node/npm: aa@1.0.0
npm ERR! notsup Not compatible with your version of node/npm: aa@1.0.0
npm ERR! notsup Required: {"node":"18.19.1"}
npm ERR! notsup Actual:   {"npm":"10.5.0","node":"v20.12.0"}

但是,这个方案的缺点是,只会在用户安装依赖时生效,如果用户直接执行node index.js,是不会校验版本的。当然,对于大部分项目而言,必然是要用到npm包的,尤其是前端项目。

此外,它只会强制报错,并不能帮助我们进行版本的切换。这时,需要用到快速切换工具。

快速切换Node.js版本

nvm

为了解决在不同项目中需要使用不同Node.js版本的问题,nvm(Node Version Manager)应运而生,使用上很简单:

$ nvm use 16
Now using node v16.9.1 (npm v7.21.1)
$ node -v
v16.9.1
$ nvm use 14
Now using node v14.18.0 (npm v6.14.15)
$ node -v
v14.18.0
$ nvm install 12
Now using node v12.22.6 (npm v6.14.5)
$ node -v
v12.22.6

需要注意的是,它的安装并不是npm i -g nvm,初学者不看文档这样安装就踩坑了。MacOS和Linux用户可以使用命令安装,而Windows用户可以在这个仓库地址下载安装文件。详情不再赘述。

fnm

nvm并非是切换工具的唯一选择,还有nvsn(可使用npm i -g n安装,但不支持Windows)等大量成熟的工具。更有追求极致的大佬们,使用Rust开发了fnmvolta,性能必然要比nvm好很多,有兴趣的同学可以尝试。

本文只介绍fnm,以下是官方的动态效果图: fnm.svg

pnpm

看过我这篇文章《乾坤挪移:为什么推荐你使用pnpm?》的同学应该都知道,pnpm也可以管理Node.js版本。如果你是pnpm的重度用户,不妨像我一样使用:

pnpm env use --global lts
pnpm env use --global 16

# 移除
pnpm env remove --global 14.0.0

# 列表
pnpm env list
pnpm env list --remote
pnpm env list --remote 16

需要注意的是,不同的管理工具间必然有个优先级的关系,当你决定使用一个管理工具后,请先将其它的移除,或者在脚本中将相关环境变量注释掉,否则可能会不生效。

自动切换Node.js版本

我们的理想情景是这样的:打开工程a,有个工具或者脚本可以自动检测到当前工程锁定的Node.js版本,自动切换。

这个功能本身并不复杂,常见的这些管理工具都提供了相关的解决方案。

nvm

以下是个nvm的Linux环境(注意,MacOS的不一样)的配置,在配置文件$HOME/.bashrc中添加以下内容:

cdnvm() {
    command cd "$@" || return $?
    nvm_path="$(nvm_find_up .nvmrc | command tr -d '\n')"

    # If there are no .nvmrc file, use the default nvm version
    if [[ ! $nvm_path = *[^[:space:]]* ]]; then

        declare default_version
        default_version="$(nvm version default)"

        # If there is no default version, set it to `node`
        # This will use the latest version on your machine
        if [ $default_version = 'N/A' ]; then
            nvm alias default node
            default_version=$(nvm version default)
        fi

        # If the current version is not the default version, set it to use the default version
        if [ "$(nvm current)" != "${default_version}" ]; then
            nvm use default
        fi
    elif [[ -s "${nvm_path}/.nvmrc" && -r "${nvm_path}/.nvmrc" ]]; then
        declare nvm_version
        nvm_version=$(<"${nvm_path}"/.nvmrc)

        declare locally_resolved_nvm_version
        # `nvm ls` will check all locally-available versions
        # If there are multiple matching versions, take the latest one
        # Remove the `->` and `*` characters and spaces
        # `locally_resolved_nvm_version` will be `N/A` if no local versions are found
        locally_resolved_nvm_version=$(nvm ls --no-colors "${nvm_version}" | command tail -1 | command tr -d '\->*' | command tr -d '[:space:]')

        # If it is not already installed, install it
        # `nvm install` will implicitly use the newly-installed version
        if [ "${locally_resolved_nvm_version}" = 'N/A' ]; then
            nvm install "${nvm_version}";
        elif [ "$(nvm current)" != "${locally_resolved_nvm_version}" ]; then
            nvm use "${nvm_version}";
        fi
    fi
}

alias cd='cdnvm'
cdnvm "$PWD" || exit

假设当前项目存在一个.nvmrc文件,内容如下:

v18.17.0

当用户在终端里进入这个工程时,就会自动执行上述脚本,切换Node.js版本,而离开了这个工程,又会还原回默认的版本:

# 进入
root@debian:/wk/nodejs# cd test
Now using node v18.17.0 (npm v9.6.7)

# 查看node路径
root@debian:/wk/nodejs/test# which node
/run/user/0/fnm_multishells/1281376_1711505241894/bin/node

# 离开
root@debian:/wk/nodejs/test# cd ..
Now using node v16.16.0 (npm v8.11.0)

fnm

fnm同样可以做到这点,区别在于它除了兼容.nvmrc外,还额外支持配置文件.node-version文件:

v18.17.0

以下是切换示例,稍微有些区别的是,在离开了当前工程后,fnm没有切换回原始版本(测试版本为fnm 1.35.1),当然,这个并不重要:

# 外部一个版本
root@debian:/wk/nodejs# fnm use 18
Using Node v18.19.1

# 进入
root@debian:/wk/nodejs# cd test
# Can't find an installed Node version matching v18.17.0.
# Do you want to install it? answer [y/N]: y
Installing Node v18.17.0 (x64)
Using Node v18.17.0

root@debian:/wk/nodejs/test# node -v
v18.17.0

# 离开
root@debian:/wk/nodejs/test# cd ..
root@debian:/wk/nodejs# node -v
v18.17.0

另外,值得一提的是,fnm可以只配置主版本号,如v20

# Can't find an installed Node version matching v20.x.x.
# Do you want to install it? answer [y/N]: y
Installing Node v20.12.0 (x64)
Using Node v20.12.0

pnpm

上面nvmfnm的功能大同小异,pnpm的思路则不太一样。它并不会主动切换当前工程的Node.js版本,而是在用户使用pnpm命令时切换运行时的Node.js版本。

我们在.npmrc文件中强制指定Node.js版本与国内镜像地址:

use-node-version=18.19.1
node-mirror:release=https://npmmirror.com/mirrors/node/
node-mirror:rc=https://npmmirror.com/mirrors/node-rc/
node-mirror:nightly=https://npmmirror.com/mirrors/node-nightly/

scripts中配置:

"dev": "node index.js"

index.js的内容如下:

console.log("Node:", process.version);

只有使用pnpm dev时才会使用配置的版本18.19.1image.png

如果你的团队将包管理工具确定为pnpm,那么这无疑也是一个好的选择,毕竟可以省掉另一个管理工具的安装和学习成本。缺点是团队成员需要理解pnpm仅会切换运行时的Node.js版本。

总结

本文探讨了团队项目中锁定和管理 Node.js 版本的方法,以及如何快速切换和自动切换 Node.js 版本。

  1. 锁定项目的 Node.js 版本:在 package.json 文件中使用 engines 属性来指定 Node.js 的版本范围或固定版本,并配合 .npmrc 配置项来强制执行版本检查。
  2. 快速切换 Node.js 版本:介绍了几种常用的工具,包括 nvmfnmpnpm,以及它们的使用方法。
  3. 自动切换 Node.js 版本:讨论了如何实现自动切换 Node.js 版本的功能。对于 nvmfnm,可以通过配置环境变量或自定义函数来实现在进入项目目录时自动切换 Node.js 版本。而对于 pnpm,可以在 .npmrc 文件中指定使用的 Node.js 版本,并在执行 pnpm 命令时自动切换。

至于包管理工具,关乎项目依赖的稳定性和一致性,同样至关重要,只是不在本文讨论的范畴,团队可以考虑约定某个包管理工具(推荐pnpm),也可以考虑结合Corepack,有必要的话我再写一篇详细介绍。