前言npm(全称NodePackageManager,或“nodepackagemanager”)是Node.js预设的一个用JavaScript编写的包管理工具。虽然是Node.js中的一个工具,但现在更多的是用来配合前端构建工具进行前端包管理。作为包管理器,最重要的是管理依赖关系。对于复杂的依赖树,npm的处理机制不同于其他包管理器。本文将详细介绍这些细节。npm2和npm3+版本处理依赖的方式不同,但是现在很少有使用npm3及以下版本的项目,本文所有介绍均基于npm3+及以上版本的npm依赖管理机制。一般来说,npm与其他包管理器类似。都是包依赖包,版本号就是用来声明这些依赖包的。SemanticVersionNumbernpm使用语义版本来控制依赖版本的包的版本,比如范围符号^~>=<,不过本文的版本号的解析方法不是重点,你只需要知道如果你使用范围版本号,Npm将安装范围内可用的最新版本**这里我想抱怨一下npm文档。找了好久才找到这个范围内的版本号使用的具体版本策略。我在页面上找到了一点介绍如果app的package.json包含:"dependencies":{"dep1":"^1.1.1"}那么npmupdate会安装dep1@1.2.2,因为1.2.2是最新的,1.2.2满足^1.1.1.npm这个range版本的设计理念相当先进。通过范围版本号,用户可以及时自动更新小版本。升级后,可能会修复一些BUG,但也会有很多因更新带来的风险。毕竟版本号是人为控制的,人为控制也有可能出现失误。例如,在修改版本号的更新中删除了一些API,导致不兼容。个人觉得这个版本号范围的包管理机制危害比较大是的,风险太高了。如果你在服务器端场景中偷偷改成(更小的)版本而不做任何更改,很可能会发生一些严重的事故。一般来说,任何改动都需要进行测试,尤其是这种依赖包升级,是一件相当冒险的事情。如果是那种普通的基础包,风险就更大了。引用过多,容易出现一些不兼容的情况。依赖树和传递依赖npm会默认将传递依赖的包以flat的形式安装到node_modules的根目录下。比如有一个模块A依赖模块B:版本冲突现在添加一个模块C,它也依赖B,但是C依赖B的高版本V2.0,此时npm的处理是有点不同;因为C依赖的B模块版本和A依赖的B版本不兼容,npm会先把模块A依赖的B1.0替换到根目录下,然后安装B2.0,即C依赖,进入C自己的node_modules,如下图|————mod-A@1.0|————mod-B@1.0|————mod-C@1.0|————mod-B@2.0对于版本不兼容的依赖树,npm的处理是先检查版本是否兼容,如果兼容则不再重复安装。如果传递的依赖包版本不兼容,则将依赖包安装到当前引用包的node_modules**npm的包版本冲突解决方案可能会带来包文件的冗余,但是可以很好的解决冲突问题。这个版本冲突解决机制真的很完善吗?从上面的介绍我们可以看出,当出现版本不兼容的时候,npm会把依赖包安装到当前包的node_modules下,也就是submodule的意思,但是并不是真的万无一失,还是有可能会出现共存的情况的多个版本。冲突。还是以上面A/B/C的三个依赖模块为例。比如Bv1.0向window对象注册了一个属性,Bv2.0也向window注册了一个属性。由于Bv1.0和v2.0有很大的不同,虽然注册的是同一个对象,但是属性和作用却有很大的不同,当一个页面同时导入A和C模块时,Bv1.0和Bv2.0将被加载,可能会有一些意外错误。用户是不能接受的。上面的例子可能不是很合适,因为注册窗口是有一定风险的。现在想象另一个常见的场景。例如,在Angular(2)中,两个基于Angular的组件依赖于不同的Angular(Core)主要版本。那么当一个页面同时使用了两个组件,并且这两个组件需要和当前页面交互的时候,比如赋值或者函数调用,就很容易出现上图的问题。这种问题虽然在Java生态的包管理中也存在,但形式会有所不同:在Maven(Java生态的包管理工具)中,虽然依赖是树状结构,但构建后的结果其实是平(flat)的。如果有多个版本的jar包,一般都是在运行时加载所有的jar包;但是由于JAVA中ClassLoader的父委托机制,同一个Class只会被加载一次,接下来N个Jar包中同名的类(包名+类名)将被忽略。这样做的好处是简单。如果有版本冲突,也清晰可见。冲突问题需要用户自行处理。MavenBuild处理多版本的包(转)依赖,如下图:npm也提供了解决这种可能的版本冲突问题的方法:peerDependenciespeerDependenciespeerDependencies和maven中的providescope很相似,当一个依赖模块X被定义时在peerDependencies而不是devDependencies或dependencies中,依赖模块的项目不会自动下载依赖项。项目需要直接或间接声明满足版本的依赖。直接依赖是指直接在devDependencies或dependencies中声明。间接依赖是指当前项目所依赖的其他模块依赖了满足X的版本范围的模块,如果都不满足,npminstall时会有告警,例如:npmWARNhidash@0.2.0requiresapeeroflodash@~1.3.1但没有安装。必须自己安装peerdependenciesnpm&webpack现在很多项目都会使用webpack作为项目构建工具,但是不像java中的maven,webpack和npm是两个独立的工具,构建和包管理是分开的。也就是说,即使npm将冲突的包安装为“子模块”在当前包中,但webpack并不一定能识别ABC的上述三个模块。比如模块A的代码从B模块导入了BObj,那么webpack构建完成后,A会引用B的哪个版本呢?v1.0还是v2.0?这个场景比较复杂,本文就不介绍了。有一篇文章详细介绍了webpack下的处理方式和测试场景:《Finding and fixing duplicates in webpack with Inspectpack》总结npm包管理的设计理念虽然很好,但并不是适合所有场景,比如这种submodule模式是行不通的在java中,子模块模式还是有一定风险的,只是降低了风险。一旦多个依赖代码同时在一个页面上工作或交互,就很容易出错。无论包管理工具如何,最安全的做法是避免重复。添加新依赖或创建新项目后,使用一些依赖分析和检查工具来检查和修复重复/冲突的依赖。参考FindingandfixingduplicatesinwebpackwithInspectpackhttps://github.com/formidablelabs/inspectpackUnderstandingthenpmdependencymodelhttps://www.reddit.com/r/haskell/comments/4zc6y3/why_doesnt_cabal_use_a_model_like_that_of_npm/?ref=share&ref_source=linkhttps://stackoverflow.com/questions/25268545/why-does-npms-policy-of-duplicated-dependencies-workHownpm3Workshttps://nodejs.org/es/blog/npm/peer-dependencies/https://docs.npmjs.com/packages-and-modules/npm依赖管理中被忽略的那些细节npm如何处理包版本冲突?
