node版本自动切换解决方案记录

在实际开发项目的时,我们可能会遇到这么一个场景,在不断落地新项目的过程中,很多老项目依然需要运行在比较低的node版本下,我们期待每个项目都能独立运行在自己需要的node版本下,自动切换且互不影响即当前node版本只在当前运行的命令行机器衍生的命令行中生效。

现如今存在不少成熟的node版本管理的解决方案,nvm, nvs, n, fnm等等,但都存在一些弊端,不太适用于我们上面描述的场景,比如跨平台问题,比如不能实现程序驱动自动切换版本等等,当然为了解决上面提到的问题,我们可以借助docker镜像来实现,将单个项目运行在不同的docker镜像下,似乎也能解决问题,可依然比较麻烦,我们可能需要制作很多个镜像,有没有更简单的解决方案呢?能在尽量少的改动原有项目的启动流程并实现node版本的自动管理和切换并实现跨平台。

为了迎合我们的需求,准备自己造轮子,因为我们的项目基本都运行在node下面,所以我们使用nodejs来开发,在开始之前我们需要了解一下环境变量。

不知道你有没有想过这个问题,为什么我们在命令行敲node命令的时候,电脑能正常响应,而我们随便输入一个xxx,电脑会提示没有找到此命令呢?这是因为在我们安装node的时候,安装程序将node的执行路径放入了系统环境变量PATH中,当我们在键入一个命令时,电脑会依次从PATH定义的路径中找键入的命令,找到了就运行该程序,没有找到则提示不存在,在windows中的Command命令行下,我们可以通过命令

echo %PATH%

在查看本机的PATH设置:

在linux环境或者git bash下,我们可以使用命令

echo $PATH

在查看本机的PATH设置:

而我们想要我们的命令能正常识别,简单的说只需要将命名的执行路径放入PATH变量中即可,但是问题也随着来了,window和linux查看环境变量的方式尚有不同,设置方式就更加不一样了:

Windows:

set path=node执行目录;%path%

Linux:

export PATH=node执行目录:$PATH

而且windows下还有git bash等不同的命令行工具,如果让我们的工具跨端是首先要解决的问题。

回过头来我们想想,为什么我们在package.json中定义的scripts脚本中使用的webpack等命令在通过npm执行脚本时能正常被识别,但是在相同目录下的命令行中直接键入webpack却提示找不到此命令,根据上面环境变量的理解,只能说明npm在运行脚本时改变了PATH变量,下面我们来印证一下,在项目根目录下我们新建一个文件test.js,内容如下:

console.log(process);

然后将其通过npm脚本启动:

通过npm run test命令,我们可以看到打印出了很多process相关的信息,其中在process.env.PATH下,我们可以看到如下信息:

对比之前的系统环境变量,多了很多node_modules\.bin相关的目录,而这些目录下,正是存放项目内命令比如webpack等命令执行脚本的地方。

借助npm和node,我们只需要更改process.env.PATH就可以轻松自定义命令的执行路径,并完美实现跨端。

解决了跨端的问题,接下来的过程就比较枯燥了,实现一个node版本的node版本管理器,有点绕,就是利用js实现node版本的自动下载和自动应用。这里不做完整展示,只提几个点:

1、下载,node不同版本的默认下载路径:https://nodejs.org/download/release,通过对node下载包规则的梳理,我们可以简单实现路径拼接:

function getVersionFile(version) {
    const { download_path = DEFAULT_DOWNLOAD_PATH } = require(config_path);
    const { arch, platform } = process;
    const formattedPlatform = platform.replace(/\d+/g, '');
    const suffix = formattedPlatform === 'win' ? 'zip' : 'tar.gz';
    const itemName = `node-${version}-${formattedPlatform}-${arch}`;
    const filename = `${itemName}.${suffix}`;
    return {
        itemName,
        filename,
        url: `${download_path}/${version}/${filename}`
    };
}

其中config_path为运行目录下的.node-config.json文件,代码实现如下:

const cwd = process.cwd();
const config_path = path.resolve(cwd, '.node-config.json');

内容如下:

{
    "version": "18.0.0",
    "download_path": "" // 不设置不要添加此项
}

2、存储,为了实现下载的node版本公用,我们需要将其存放在一个不同项目都能访问到且不会被当做临时文件删掉的地方,这里我们选择将其存储到用户家目录下:

function getStoragePath() {
    return path.resolve(os.homedir(), '.node');
}

3、改变环境变量实现切换node版本:

function setGlobalVariables(version) {
    let nodePath = path.resolve(storagePath, version);
    const binPath = path.resolve(nodePath, 'bin');
    if (fs.existsSync(binPath)) {
        nodePath = binPath;
    }
    process.env.PATH = `${nodePath}${path.delimiter}${process.env.PATH}`;
}

4、命令包装,这块儿我最初的想法有点岔了,一直想在运行脚本中做更改,但是那样对项目的打包流程的侵入性比较大,而且还要处理依赖库版本的重装问题,主要是系统默认的node版本和最终项目运行的node版本不一致,导致我们需要在项目运行时重新安装依赖库,而不能直接使用npm i安装的库,后面仔细想想何不直接将工具包装成一个类似npm和node的命令行工具呢,包装方式如下:

async function wrapExec(type) {
    await prepareEnv();
    const args = process.argv.slice(2);
    execSync(`${type} ${args.join(' ')}`, {
        stdio: 'inherit'
    });
}

比如我们要包装node,则:

#! /usr/bin/env node
const { wrapExec } = require('../lib/node');
wrapExec('node');

然后在package.json中定义命令:

"bin": {
    "snode": "./bin/node.js",
    "snpm": "./bin/npm.js"
  }

这里将node和npm分别包装成了snode和snpm,比如我们要运行一个app.js脚本,只需要在运行目录下定义好.node-config.json文件,并使用snode app.js即可使用我们规定的node版本运行app.js脚本,而对于npm也是一样的,我们只需要将npm i, npm run ...改成snpm i, snpm run ...即可。

最后附上运行截图:

运行脚本的内容:

const { execSync } = require('child_process');

execSync('node -v', {
    stdio: 'inherit',
});

安装方式:

npm i node-version-manager -g

具体使用方式见npm包主页链接:https://www.npmjs.com/package/node-version-manager

  • 支付宝二维码 支付宝
  • 微信二维码 微信

本文地址: /auto-manage-node-version.html

版权声明: 本文为原创文章,版权归 逐梦个人博客 所有,欢迎分享本文,转载请保留出处!

相关文章