It's all about
connecting the dots

浅谈项目中node和依赖包的版本管理

 

这是最近才注意的一个问题。之前在上一家公司的时候,有个项目组的项目,经常发生有的人的电脑里安装项目依赖后无法启动项目的问题,然后每每都是让他从别的同事那里把node_modules文件夹打包拷贝过来,我刚去那个项目组的时候也碰到了这个问题,也是从别的同事那里拷贝了一份node_modules文件夹才能正常跑项目的,后来经过尝试发现好像如果我将npm命令的版本降低到2+再安装项目依赖的话就能正常跑了,但是这个显然是个很奇葩的现象,另外需要说的是他们那个项目也有package-lock.json这个文件的,这个项目因为不是一开始就在跟的项目,我也搞不拎清究竟是哪里出了问题。但是node包的版本管理依旧很重要,不管理的话更容易发生类似这样的问题。

一、node的版本管理

1.1、建议使用LTS版本

推荐使用标记为LTS的版本,这种版本官方会用较长的时间进行维护,所谓LTS,就是Long-Term-Support长期支持的意思。

1.2、同项目里不同开发者应统一node版本

团队合作开发的项目中,大家各自电脑里的node版本号也需要进行统一,如果不统一,比如甲本地node –version 为9.2.1,乙本地使用node –version 为10.0.0,那么可能会有两种问题:

1、如果一个程序的版本遵循语义化版本的规定,那么a.b.c这样的版本号中,a的变动表示的是较大的版本升级(可能会有不向前兼容的新API等)b的变动表示的是较小的、向前兼容的功能更新,c的变动表示的是一些bug修复。但是我们不能保证第三方程序都严格按照这个语义化版本的规定进行版本号的命令,另外也无法确定只是更新了版本号的c部分的是否只是修复了已知bug而没有再次引入新的bug,所以我个人是建议直接把node版本号进行统一,而不是给出一个允许使用的node版本号区间

2、这里还有一个问题,比如乙在本地添加了一个依赖包,该依赖包要求node版本号10.0.0及以上,由于乙本地node版本号为10.0.0所以可以正常安装该依赖了,但是甲本地的node版本号为9.2.1,就无法安装该依赖包了,团队合作开发的项目,我们当然是希望大家都可以在本地正常安装项目依赖并运行项目的。

1.3、如何限制同项目开发者之间node版本的统一

直接上代码(参考自vue-cli v2):

const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')

function exec (cmd) {
    return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
    {
        name: 'node',
        currentVersion: semver.clean(process.version),
        versionRequirement: packageConfig.engines.node
    },
]

module.exports = function () {
    const warnings = []
    for (let i = 0; i < versionRequirements.length; i++) {
        const mod = versionRequirements[i]
        if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
            warnings.push(mod.name + ': ' +
                chalk.red('当前版本为:' + mod.currentVersion) + ',应为 ' +
                chalk.green(mod.versionRequirement)
            )
        }
    }

    if (warnings.length) {
        console.log('')
        console.log(chalk.yellow('要开发本项目, 需要更新/降级以下模块:'))
        console.log()
        for (let i = 0; i < warnings.length; i++) {
            const warning = warnings[i]
            console.log('  ' + warning)
        }
        console.log()
        process.exit(1)
    }
}

对应地,package.json文件里需要有要求的node版本信息:

{
    // ...
    "engines": {
        "node": "9.2.1"
    },
    // ...
}

1.4、多项目不同node版本共存问题

前面我们提到了单个项目里要尽量统一node版本。但是如果是多个项目的话,就会涉及到多个node版本了。要让多个node版本在一台及其里和平共处,有两种方案。

一种方案是使用现成的第三那方命令行工具,比如nvm(安装方法参考见其仓库地址:https://github.com/nvm-sh/nvm)。nvm的安装及使用都是非常简单的。下面是一些常用命令:

# 通过nvm安装指定版本的node
nvm install v9.2.1

# 临时切换为指定版本的node环境
nvm use v9.2.1

# 设置默认情况下的node版本
nvm alias default v9.2.1

# 列出可安装的node版本
nvm ls-remote

# 列出本地安装了的node版本以及当前正在使用的node版本
nvm ls

举个例子,在安装nvm成功后,如果我们执行nvm install v10.12.0nvm install v14.15.5之后,本机就会有两个版本的node了(v10.12.0和v14.15.5)。然后构建A项目时,执行nvm use v10.12.0 && npm run build,构建B项目时,执行nvm use v14.15.5 && npm run build。就可以多node版本项目在一个机器里和平共处了。当然实际落地时,建议各自项目的package.json里定义下具体要用的node版本版,部署平台编译时从package.json里读取node版本,判断有对应node版本后执行对应的nvm use [version]命令,如果不存在则先nvm install [version]。这样部署平台的处理逻辑就可以尽量保持统一。

还有一种方案会比上面的稍微麻烦一点点点点。其实就是node安装由通过nvm操作变成手动安装多个node版本(放到同一个大目录下便于查找判断是否已有对应版本的node)。然后编译某个项目时直接找到对应版本node的bin路径,然后执行类似下面这样的命令即可。

/path/versions/node/v10.12.0/bin/node /path/versions/node/v10.12.0/lib/node_modules/npm/bin/npm-cli.js run build --scripts-prepend-node-path=auto

二、npm包的版本管理

2.1、如何增删依赖包

①、通过npm增删包

# 添加项目依赖
npm i -S [package-name]

# 添加开发依赖
npm i -D [package-name]

# 删除项目依赖
npm uninstall -S [package-name]

# 删除开发依赖
npm uninstall -D [package-name]

②、通过yarn增删包

# 通过yarn添加项目依赖
yarn add [package-name]

# 通过yarn添加开发依赖
yarn add --dev [package-name]

# 通过yarn删除依赖
yarn remove [package-name]

通过这些命令去更新项目依赖信息的同时,不仅会自动更新package.json文件,还会同步更新package-lock.json文件(如果用的是npm)或yarn.lock文件(如果用的是yarn)。而且这里有个特点,通过这种方式更新的package.json文件里的项目依赖都是将依赖包按包名字母顺序排列的,这样比较好找包的版本信息。如果你打开一个项目的package.json文件发现里面的包名排列没有顺序乱七八糟的,那就说明这个项目package.json里包的信息是某个开发直接手改上去的。

切记,如果要增删项目的依赖或者修改其版本号,不要手动直接去修改package-lock.json/yarn.lock文件,而要通过上面提到的对应的npm命令或yarn命令操作来自动生成。

2.2、避免同项目里混用npm和yarn

假设你通过npm i -S a 命令添加并安装了node包a,并且实际安装的版本是1.0.1,这时候具体的版本信息是记录在package-lock.json文件里的,过了一个月另外一个同事通过yarn install 命令安装包a时,由于具体的版本信息在yarn.lock文件里是不存在的,所以完全有可能这位同事实际安装的是1.1.2版本,这样就会造成包的版本管理混乱的问题,反过来也是一样。

如何进行约束限制呢?这里假定我们要求使用版本1.3.2(含)~2(不含)的yarn,并且要禁止开发者使用npm。

先修改package.json文件:

{
    // ...
    "engines": {
        "yarn": ">= 1.3.2 < 2.0.0"
    },
    // ...
}

然后执行下面这样的脚本,就可以达到限制yarn版本的作用。

const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')

function exec (cmd) {
    return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
    {
        name: 'yarn',
        currentVersion: exec('yarn --version'),
        versionRequirement: packageConfig.engines.yarn
    }
]

module.exports = function () {
    const warnings = []
    for (let i = 0; i < versionRequirements.length; i++) {
        const mod = versionRequirements[i]
        if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
            warnings.push(mod.name + ': ' +
                chalk.red('当前版本为:' + mod.currentVersion) + ',应为 ' +
                chalk.green(mod.versionRequirement)
            )
        }
    }

    if (warnings.length) {
        console.log('')
        console.log(chalk.yellow('要开发本项目, 需要更新/降级以下模块:'))
        console.log()
        for (let i = 0; i < warnings.length; i++) {
            const warning = warnings[i]
            console.log('  ' + warning)
        }
        console.log()
        process.exit(1)
    }
}

接下来我们需要限制开发者使用yarn,避免他们用npm(原理是检测项目根目录下是否存在运行npm install时生成的package-lock.json文件,npm-shrinkwrap.json是作向前兼容的,不是很常见):

const path = require('path')
const chalk = require('chalk')
const fse = require('fs-extra')

const unnecessaryFiles = [
    'package-lock.json',
    'npm-shrinkwrap.json'
]

const necessaryFiles = [
    'yarn-lock.json'
]

module.exports = async function () {
    const warnings = []
    for (let fileName of unnecessaryFiles) {
        const targetPath = path.resolve(__dirname, `../${fileName}`)
        try {
            const isExisted = await fse.pathExists(targetPath)
            if (isExisted) {
                warnings.push(
                    chalk.red(`检测到文件:${fileName}。`) +
                    chalk.green(`请将其删除后使用yarn重新安装依赖。`)
                )
            }
        } catch (e) {
            console.log(chalk.red(`  判断文件${targetPath}是否存在时出错了。\n`))
            console.log(e)
            console.log()
            process.exit(1)
        }
    }

    for (let fileName of necessaryFiles) {
        const targetPath = path.resolve(__dirname, `../${fileName}`)
        try {
            const isExisted = await fse.pathExists(targetPath)
            if (!isExisted) {
                warnings.push(
                    chalk.red(`未检测到文件:${fileName}。`) +
                    chalk.green(`请先使用yarn安装项目依赖后再执行其他操作。`)
                )
            }
        } catch (e) {
            console.log(chalk.red(`  判断文件${targetPath}是否存在时出错了。\n`))
            console.log(e)
            console.log()
            process.exit(1)
        }
    }

    if (warnings.length) {
        console.log('')
        console.log(chalk.yellow('本项目中请统一使用yarn而非npm进行依赖管理,不要同时混用yarn和npm,这样会带来依赖版本混乱的问题:'))
        console.log()
        for (let i = 0; i < warnings.length; i++) {
            const warning = warnings[i]
            console.log('  ' + warning)
        }
        console.log()
        process.exit(1)
    }
}

2.3、提交package-lock.json/yarn.lock文件

在通过npm和yarn增删项目依赖时,会同步生成/更新package-lock.json文件(如果用的是npm)或yarn.lock文件(如果用的是yarn)。这些lock文件里包含了所有依赖的具体版本号信息。通过提交这些lock文件,就可以达到锁定依赖包版本号的目的。

切记,如果要增删项目的依赖或者修改其版本号,不要手动直接去修改package-lock.json/yarn.lock文件,而应通过npm/yarn的对应命令来自动更新。

为什么要锁定依赖包的版本号呢?因为:

  • 你不能信任第三方包都完全按照语义化版本号规则去更新版本号;
  • 你不能信任按语义化版本号更新版本号的第三方包在修复bug或添加新功能时不会引入新的bug;
  • 第三方包在更新版本号的时候可能会更新它对应的依赖包的版本号,第三方包依赖的第三方包的版本号变动又可能引发前面几点及本条提到的问题。

三、锁定node和npm包版本带来的问题

前文所述的将node和npm包的版本号固定的处理方案是一种保守方案。node和npm包版本固定后会导致一些版本更新可以带来的bugfix(包括安全相关的)、优化点(比如node某个API的性能提升等)被忽略掉了,需要依靠人工主动更新。当然这是个双刃剑,也规避了一些版本更新带来的潜在的新bug,以及一些性能的降低。

npm包带来的运行时的性能提升一般是个优先级不高的事情,一般你不太会因为一个版本的问题,用户界面就由非常卡变得非常流畅了(有这问题的话,开发阶段就发现了)。但是npm和node版本的更新,有时候带来的对编译时的性能提升有时候是非常明显的。bugfix如果我们的项目没触发到这个bug,其实优先级也不高(触发了并影响到实际功能的话,开发测试阶段应该就发现了)。但是安全问题是绕不开的。

先说node的版本问题。还是建议尽量统一,鉴于node的版本号本来就不会自动升级,升级操作只能人工促成。团队里不同同事用不同node版本开发同一个项目,版本有高有低时,除了本文开头提到的问题之外,并不会导致部署平台服务器上node版本的升级。所以要升级就统一升级,可以定一个较长的时间来周期性去尝试升级一下(要考虑下升级成本的大小,如果会需要改动很多npm包的版本,影响比较大的话要回归下主要功能)。

对npm包的版本考虑,则会有两种方案。

一种方案是不固定npm包的版本号。还是上面双刃剑的事,碰到问题时再去修复。如果用这个方案,要注意首先不能提交package-lock.json文件,否则版本号还是会被固定了。另外平时尽量少用小众的库,热门库一般如果出bug后可以预期官方修复会比较快。不过即便是package.json里我们不固定版本号,用了^之类的符号,版本号的变动也是有范围限制的,在这个限制之外的版本升级还是要人工促成。所以这个方案很大程度上是减少了上面说的问题。但是要要解决这方面的问题,还是要有个别的方案。可以参考下面一段文字里的方案。

还有一种方案是固定版本号的前提下,手动判断是否要升级。如果公司有安全部门会扫这些版本,他们那边应该会有个渠道可以查哪些包的哪些版本是有安全风险的,我们可以本地开发时在npm run dev时去调他们的接口然后根据风险级别和升级成本等判断是否要将现有的npm包升级一下。如果你在github上有一个自己的项目,你可以发现当你登录后打开你的项目,github会在项目页面较靠前的地方给出一个只要开发者本人才能看到的提示,提示里会告诉你哪些包有安全风险建议升级之类的,所以github上多半也有此类api可以供我们使用。这块我没试过,只是想到的方案,可行性待定。

另外,npm官方的建议也可以作为我们对这个事情具体结论的一个参考。早期npm是没有锁文件的,后面加了需要开发者主动生成的npm-shrinkwrap.json文件,到后面会主动生成package-lock.json文件,并且会在终端里提示你类似下面这样的文案(You should commit this file)(重现方式:删除项目下的package-lock.json文件然后执行npm install即可),是可以明显看到官方的倾向的——应该提交。

赞(0) 打赏
版权声明:非商业用途转载请注明文章链接,商业用途转载请联系邮箱获取授权。
文章名称:《浅谈项目中node和依赖包的版本管理》
文章链接:https://www.orzzone.com/version-management-node-package.html
商业联系:yakima.public@gmail.com
本站内容仅供个人学习交流,不做为任何投资、建议的参考依据,因此产生的问题需自行承担。

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏