前言看完本文,你会对依赖版本锁定的原理,package-lock.json或yarn.lock的重要性有一个整体的认识。首先先说最近两起npm安装package.json依赖包相关的事件,由于依赖包版本更新bug导致项目出错。事件一:新版本依赖包本身的bug。项目本地打包正常,线上使用Jenkins完成DevOps交付流水线打包报错。报如下错误:**17:15:32**ERRORin./node_modules/clipboard/dist/clipboard.js**17:15:32**Modulebuildfailed(from./node_modules/babel-loader/lib/index.js):**17:15:32**Error:Couldn'tfindpreset"@babel/env"relativetodirectory"/app/workspace/SIT/node_modules/clipboard"错误的原因是剪贴板插件没有Install@babel/env预设。很明显这是插件的问题。去官方库剪贴板查看源码,发现该库的依赖包非常少,而且大部分都是原生实现。让我们看看其他人是否在这个问题上有同样的问题。到目前为止,还没有人提出来。由此推断,可能是插件本身的“问题”。但是我本地的项目是正常打包的,在线报错可能是本地版本和在线版本不一致导致的(小版本的bug)。通过查看剪贴板:package.json中配置的“^2.0.4”,网上实际安装的版本是2.0.7,而我本地实际安装的版本是2.0.6,所以2.0.7出现的“问题”位于。由于是插件本身的“问题”,我的临时解决方案是锁定到2.0.4版本,也就是clipboard:“2.0.4”,后面是package-lock.json。打破砂锅问到底既然“问题”已经定位到2.0.7版本,通过进一步比较该版本提交文件内容的差异,发现.babelrc文件中使用的preset是环境。2.0.7版本使用@bable/env,并将babel更新为7!问题基本找到了,这里顺便给作者提个issue。事件二:依赖包的新插件bug已经正常使用。Braft-editor是一款优秀的富文本编辑器插件。最近把项目重新部署到其他朋友的电脑上或者我本地的电脑上。启动后,发现使用toHtml()方法获取富文本html内容总是空的!历史版本正常,猜测可能是版本更新导致的。同样的,去官方库brat-editor看看有没有人遇到同样的问题。果然,这一次,原因是它的依赖包draft-js更新了。有关详细信息,请参阅此问题。这是因为插件的依赖包更新有问题。直接锁定当前插件没有效果,不会对其依赖包进行约束(依赖包仍会下载最新版本的包)。我的临时解决方案是尝试将版本回滚到更高版本并锁定。之所以这样是因为回滚版本的依赖包版本肯定会低于当前版本,之前的版本是正常的。经验教训其实这两个事件的诱因是一样的:当前项目依赖树模块的版本没有被锁定。下面我们来探讨下依赖包的版本管理。语义版本(semver)package.json主要用于前端工程中记录依赖包名、版本、运行指令等信息字段。其中dependencies字段指定了项目所依赖的模块,devDependencies指定了项目开发所需的模块。他们都指向一个对象。对象的每个成员由模块名称和对应的版本要求组成,表示所依赖的模块及其版本范围。可以在对应的版本上添加各种限制,主要有以下几类:指定版本:如1.2.2,按照“主版本.次版本.次版本”的格式,安装时只安装指定的版本.Tilde(波浪号)+指定版本:比如~1.2.2表示安装最新版本的1.2.x(不低于1.2.2),但不安装1.3.x,即主版本number和次要版本号。Caret(脱字符)+指定版本:例如^1.2.2表示安装最新版本的1.x.x(不低于1.2.2),但不安装2.x.x,表示主版本号不会在安装过程中更改。需要注意的是,如果主版本号为0,则插入符的行为与波浪号相同。这是因为此时正处于开发阶段,即使小版本号发生变化也可能导致程序不兼容。最新:安装最新版本。当我们使用例如安装依赖包时npminstallpackage-save,版本以插入符形式显示。这样每次重新安装依赖包npminstall时,“小版本”和“小版本”都会拉取最新的。一般如果主版本不变,核心功能不会有变化,API要兼容旧版本,但这在开源世界是很难控制的,尤其是复杂项目的依赖包很多,它不可避免地会引入一些意想不到的错误。npm-shrinkwrap&&package-locknpm-shrinkwrap正是因为每次重装和依赖树模块版本的不确定性,所以有相应的锁定版本机制。在npm5之前,这可以通过npmshrinkwrap实现。通过运行npmshrinkwrap,会在当前目录生成一个npm-shrinkwrap.json文件,里面是一个很大的列表,里面列出了package.json中的各个依赖,应该安装的具体版本,模块的位置(URI),验证模块完整性的散列、它需要的包列表以及依赖项列表。运行npminstall时,会先使用npm-shrinkwrap.json进行安装,如果没有,则使用package.json进行安装。package-lock在npm5版本之后,我们运行npminstall时发现会生成一个新的文件package-lock.json,其内容和上面提到的npm-shrinkwrap.json基本一致。“vue-loader”:{“版本”:“14.2.4”,“已解决”:“https://registry.npmjs.org/vue-loader/-/vue-loader-14.2.4.tgz”,“完整性":"sha512-bub2/rcTMJ3etEbbeehdH2Em3G2F5vZIjMK7ZUePj5UtgmZSTtOX1xVVawDpDsy021s3vQpO6VpWJ3z3nO8dDw==","dev":true,"requires":{"consolidate":"^0.14.0","hash-sum".2:"^14.0","hash-sum",2-utils":"^1.1.0","lru-cache":"^4.1.1","postcss":"^6.0.8","postcss-load-config":"^1.1.0","postcss-selector-parser":"^2.0.0","prettier":"^1.16.0","resolve":"^1.4.0","source-map":"^0.6.1","vue-hot-reload-api":"^2.2.0","vue-style-loader":"^4.0.1","vue-template-es2015-compiler":"^1.6.0"},“依赖项”:{“postcss-load-config”:{“版本”:“1.2.0”,“已解决”:“https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz","完整性":"sha1-U56a/J3chiASHR+djDZz4M5Q0oo=","dev":true,"requires":{"cosmiconfig":"^2.1.0","object-assign":"^4.1.0","postcss-load-options":"^1.2.0","postcss-load-plugins":"^2.3.0"}},}},当project中已经有一个package-lock.json文件。安装项目依赖时,会使用该文件来解析安装指定版本的依赖包,而不是使用package.json来解析安装模块,因为package-lock是针对每个模块的,它的每个依赖都指定了一个版本,位置,和完整性哈希,因此它每次都会创建相同的安装。不管你使用什么设备,或者将来安装它,它每次都会给你相同的结果。npm5版本下的安装规则npm从一开始就没有按照现有的规则制定。5.0.x版本:无论package.json中的依赖是否更新,npminstall都会根据package-lock.json进行下载。针对这种安装策略,有人提出了这个issue,然后在5.1.0版本之后演变成一个rule。5.1.0版本后:当package.json中有新版本的依赖时,npminstall会忽略package-lock.json去下载新版本的依赖并更新package-lock.json。针对这种安装策略,有人提了一个issue,参考了npmcontributoriarna的评论,得出了5.4.2版本之后的规则。5.4.2版本后:如果只有一个package.json文件,运行npminstall会生成一个基于它的package-lock.json文件。此文件相当于此安装的快照。它不仅记录了直接依赖的版本,还记录了间接依赖的版本。如果package.json的semver-range版本与package-lock.json中的版本兼容(package-lock.json版本在package.json指定的版本范围内),即使package.json中有新版本。此时执行npminstall还是会根据package-lock.json下载json。如果package.json的版本范围被手动修改,与package-lock.json中的版本不兼容,则在执行npminstall时,package-lock.json将更新为与package.json兼容的版本。yarnyarn出现的主要目标是解决上述由语义版本控制导致的npm安装的非确定性问题。虽然可以使用npmshrinkwrap来实现可预测的依赖树,但这不是默认选项,所有开发人员都必须了解并启用此选项。纱线采用不同的方法。每个yarn安装都会生成一个类似于npm-shrinkwrap.json的yarn.lock文件,它是默认创建的。除了一般信息外,yarn.lock文件还包含正在安装的内容的校验和,以确保使用相同版本的库。yarn的主要优化yarn的出现主要做了以下优化:并行安装:无论是npm还是yarn执行包的安装,都会执行一系列的任务。npm是按照队列来执行每个包的,也就是说必须先完成当前包的安装,才能继续进行后续的安装。另一方面,Yarn同步执行所有任务,从而提高性能。离线模式:如果之前安装过某个软件包,用yarn重新安装时会从缓存中获取,不需要像npm那样从网络下载。统一安装版本:为了防止不同版本被拉取,yarn有一个锁文件(lockfile),记录了恰好安装的模块的版本号。每次添加新模块时,yarn都会创建(或更新)yarn.lock文件。这确保每次拉取相同的项目依赖项时都使用相同的模块版本。更好的语义:yarn改变了一些npm命令的名称,例如yarnadd/remove,这比npm原来的install/uninstall更清晰。安装依赖树过程执行项目自己的预安装。如果当前npm项目定义了preinstallhook,此时会执行。确定一级依赖关系。一个模块首先要做的是确定项目中的一级依赖,即在dependencies和devDependencies属性中直接指定的模块(假设此时没有添加npminstall参数)。项目本身就是整个依赖树的根节点。每个一级依赖模块是根节点下的子树。npm会启动多个进程,逐步从每个一级依赖模块中寻找更深层次的节点。获取模块。获取模块是一个递归的过程,分为以下几个步骤:获取模块信息。下载一个模块之前,首先要确定它的版本,因为package.json往往是语义版本(semver,semanticversion)。此时如果版本描述文件(npm-shrinkwrap.json或package-lock.json)中有模块信息,可以直接获取,没有则从仓库中获取。比如package.json中某个包的版本是^1.1.0,npm会去仓库获取最新版本,形式为1.x.x。获取模块实体。在上一步中,会得到模块的压缩包地址(resolved字段),npm会通过这个地址去查看本地缓存,如果缓存中有,则直接取,如果没有,它将从仓库下载。找到模块依赖,如果有依赖则返回步骤1,如果没有则停止。模块扁平化(重复数据删除)。上一步得到的是一个完整的依赖树,其中可能包含大量重复的模块。比如模块A依赖loadsh,模块B也依赖lodash。npm3之前会严格按照依赖树的结构安装,所以会造成模块冗余。yarn和npm5默认添加了dedupe进程。它遍历所有节点,将模块逐个放置在根节点下,这是第一层节点模块。当发现重复模块时,它们将被丢弃。这里需要一个重复模块的定义,指的是同名模块和semver兼容性。每个semver对应一个版本允许范围。如果两个模块的版本允许范围重叠,则可以获得兼容的版本而不必具有相同的版本号。这允许在重复数据删除过程中删除更多冗余模块。.安装模块。此步骤将更新项目中的node_modules并执行模块中的生命周期函数(按照预安装、安装、安装后的顺序)。执行项目自身的生命周期。如果当前npm项目定义了hook,此时会执行(按照install、postinstall、prepublish、prepare的顺序)。比如插件htmlparser2@^3.10.1和dom-serializer@^0.2.2都使用了entities依赖包,但是使用的版本不一样,我们自己安装一个版本的entities包。详情如下:--htmlparser2@^3.10.1|--entities@^1.1.1--dom-serializer@^0.2.2|--entities@^2.0.0--entities@^2.1.0通过npminstall安装后,生成的package-lock.json文件内容及其node_modules目录结构:可以找到:dom-serializer@^0.2.2的依赖包entities@^2.0.0和我们自己安装的entities@^2.1.0其实安装为entities@^2.2.0,放在node_modules的第一层。因为这两个版本的semverranges相同,先遍历,所以都会合并安装在第一层;htmlparser2@^3.10.1的依赖包entities@^1.1.1实际上是放在dom-serializer包的node_modules中,并与package-lock.json描述结构保持一致。通过yarn安装后,生成的yarn.lock文件内容及其node_modules目录结构:可以发现,与npminstall不同的是,yarn.lock中的所有依赖描述都是平的,即没有嵌套关系依赖描述;在yarn.lock中,如果semver范围相同,则合并同名不同版本号的依赖包,否则会有多个版本描述。注意cnpm不支持package-lock。使用cnpminstall时,不会生成package-lock.json文件。cnpm安装时,即使你的项目中有package-lock.json文件,cnpm也不会识别,依然会按照package.json进行安装。所以这就是为什么你之前用npm安装package-lock.json,后来人家用cnpm安装,可能和你安装的依赖包不一致。因此尽量不要直接使用cnpminstall来安装项目依赖。但是为了解决直接使用npminstall速度慢的问题,可以设置npmproxy来解决。//设置淘宝镜像代理npmconfigsetregistryhttps://registry.npm.taobao.org//查看设置的代理npmconfiggetregistry当然你也可以通过nrm工具和快捷操作来设置代理。全局安装$npminstall-gnrm查看安装的代理列表$nrmls*npm-----https://registry.npmjs.org/yarn-----https://registry.yarnpkg.comcnpm----http://r.cnpmjs.org/taobao--https://registry.npm.taobao.org/nj------https://registry.nodejitsu.com/skimdb--https://skimdb.npmjs.com/registryswitchproxy$nrmusecnpm//switchregistrytocnpm*注册表已设置为:http://r.cnpmjs.org/speednrmtestcnpm*cnpm---618ms但是,设置这些全局代理可能不足以下载一些特定的依赖包(在没有VPN的情况下),例如:node-sass、puppeteer、chromedriver、electron等。具体依赖包的国内镜像可以通过.npmrc设置文件。该文件会在项目npminstall时加载读取,优先级高于npm全局设置。registry=https://registry.npm.taobao.org/sass_binary_site=http://npm.taobao.org/mirrors/node-sasschromedriver_cdnurl=http://npm.taobao.org/mirrors/chromedriverelectron_mirror=http://npm.taobao.org/mirrors/electron/puppeteer_download_host=http://npm.taobao.org/mirrors/chromium-browser-snapshots/Summary以后会重新构建项目。由于依赖树中的版本更新,导致意外是不可能避免的,原因是整个依赖树版本没有被锁定。解决方案分为以下四种:package.json中固定版本。注意:只能锁定当前依赖包版本,不能控制整个依赖树版本。npm+npm-shrinkwrap.json。npm+package-lock.json。纱线+纱线锁.json。根据自己的情况选择。本人知识有限,欢迎指正,谢谢大家的好评,over~
