热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

浅析node_modules如何解决依赖地狱问题、如何从node_modules加载package、目录结构的2种模式、版本重复及可能导致的问题、Semver规范及lock文件、pnpm解决理念介绍

最近看到涉及node_modules的问题比较多,所以决定深入学习一下,正好看到一篇文章,写的还挺详细的Ryan对于node.js的十大遗憾之一就是支持了node_modu

  最近看到涉及 node_modules 的问题比较多,所以决定深入学习一下,正好看到一篇文章,写的还挺详细的

  Ryan对于node.js的十大遗憾之一就是支持了node_modules,node_modules的设计虽然能满足大部分的场景,但是其仍然存在着种种缺陷,尤其在前端工程化领域,造成了不少的问题,本文总结下其存在的一些问题,和可能的改进方式


一、Dependency Hell 依赖地狱问题

  现在项目里有两个依赖A和C,A和C分别依赖B的不同版本,如何处理

  这里存在两个问题:

  1. 首先是B本身支持多版本共存,只要B本身没有副作用,这是很自然的,但是对于很多库如core-js会污染全局环境,本身就不支持多版本共存,因此我们需要尽早的进行报错提示(conflict的warning和运行时的conflict的check)

  2. 如果B本身支持多版本共存,那么需要保证A正确的加载到B v1.0和C正确的加载到B v2.0

  我们重点考虑第二个问题:node的解决方式是依赖的node加载模块的路径查找算法和node_modules的目录结构来配合解决的

  如何从node_modules加载package?

  核心是递归向上查找node_modules里的package,如果在 '/home/ry/projects/foo.js' 文件里调用了 require('bar.js'),则 Node.js 会按以下顺序查找:



  • /home/ry/projects/node_modules/bar.js

  • /home/ry/node_modules/bar.js

  • /home/node_modules/bar.js

  • /node_modules/bar.js

  该算法有两个核心:(1)优先读取最近的node_modules的依赖(2)递归向上查找node_modules的依赖

  该算法即简化了 Dependency hell 的解决方式,也带来了非常多的问题


二、node_modules的目录结构

1、nest mode 嵌套模式

  利用require先在最近的node_module里查找依赖的特性,我们能想到一个很简单的方式,直接在node_module维护原模块的拓扑图即可

  这样根据mod-a就近的使用mod-b的1.0版本,而mod-c就近的使用了mod-b的2.0版本

  但是这样带来了另一个问题,如果我们此时再依赖一个mod-d,该mod-d也同时依赖的mod-b的2.0版本,这时候node_modules就变成下面这样

  我们发现这里存在个问题,虽然 mod-a 和 mod-d 依赖了同一个mod-b的版本,但是mod-b却安装了两遍,如果你的应用了很多的第三方库,同时第三方库共同依赖了一些很基础的第三方库如lodash,你会发现你的node_modules里充满了各种重复版本的lodash,造成了极大的空间浪费,也导致npm install很慢,这既是臭名昭著的 node_modules hell

2、flat mode 平坦模式

  我们还可以利用向上递归查找依赖的特性,将一些公共依赖放在公共的node_module里

  根据require的查找算法:

  1. A和D首先会去自己的node_module里去查找B,发现不存在B,然后递归的向上查找,此时查找到了B的 v1.0版本,符合预期

  2. C会先查找到自己的node_module里查找到了B v2.0,符合预期

  这时我们发现了即解决了depdency hell也避免了npm2 的nest模式导致的重复依赖问题


三、doppelgangers 翻倍问题

  但是问题并没有结束,如果此时引入的D依赖的是B v2.0,而引入的E依赖的是B v1.0,我们发现无论是把Bv2.0还是Bv1.0放在top level,都会导致任何另一个版本会存在重复的问题,如这里的B的v2.0的重复问题

  你也许会说版本重复不就是浪费一点空间吗,而且这种只有出现版本冲突的时候才会碰到,似乎问题不大,事实的确如此,然而某些情况下这仍然会造成问题


四、版本重复可能导致的问题

1、全局types冲突

  虽然各个package之前的代码不会相互污染,但是他们的types仍然可以相互影响,很多的第三方库会修改全局的类型定义,典型的就是@types/react,如下是一个常见的错误

  其错误原因就在于全局的types形成了命名冲突,因此假如版本重复可能会导致全局的类型错误。

  一般的解决方式就是自己控制包含哪些加载的@types/xxx。

2、破坏单例模式

  require的缓存机制:node会对加载的模块进行缓存,第一次加载某个模块后会将结果缓存下来,后续的require调用都返回同一结果,然而node的require的缓存并非是基于module名,而是基于resolve的文件路径的,且是大小写敏感的,这意味着即使你代码里看起来加载的是同一模块的同一版本,如果解析出来的路径名不一致,那么会被视为不同的module,如果同时对该module同时进行副作用操作,就会产生问题。

3、Phantom dependency 幻影依赖

  我们发现flat mode相比nest mode节省了很多的空间,然而也带来了一个问题即phantom depdency,考察下如下的项目

  我们编写如下代码

  这里的glob和brace-expansion都不在我们的depdencies里,但是我们开发和运行时都可以正常工作(因为这个是rimraf的依赖),一旦将该库发布,因为用户安装我们的库的时候并不会安装库的devDepdency,这导致在用户的地方会运行报错。

  我们把一个库使用了不属于其depdencies里的package称之为phantom depdencies,phantom depdencies不仅会存在库里,当我们使用monorepo管理项目的情况下,问题更加严重,一个package不但可能引入DevDependency引入的phantom依赖,更很有可能引入其他package的依赖,当我们部署项目或者运行项目的时候就可能出问题。

  在基于yarn或者npm的node_modules的结构下,doppelganger和phantom dependency似乎并没有太好的解决方式。其本质是因为npm和yarn通过 node resolve算法配合node_modules的树形结构对原本depdency graph的模拟,哪有没有更好的模拟方式能够避免上述问题呢。


五、Semver 当理想遇到现实

  npm对package版本号采用语义化版本,Semver本身也是为了解决Depdency Hell而引入的解决方案,如果你的项目引入的第三方依赖越来越多,你将会面临一个困境。

  如果你为你的每一个版本都写死依赖,那么如果某个底层的依赖需要修复或者升级,你难以评估这个升级会修复的影响范围,这可能导致级联反应,与其协作的任何包都可能会挂掉,导致整个系统都需要全量的测试回归,最后的结果很大可能是整个应用彻底锁死版本,再也不敢做任何升级改动

  因此semver的提出主要是用于控制每个package的影响范围,能够实现系统的平滑升级和过渡,npm每次安装都会按照semver的限制,安装最新的符合约束的依赖。

  这样每次npm install都会安装符合"^4.0.0"约束的最新依赖,可能是4.42.0的版本。

  如果所有的库都能完美的遵守语义化版本,那么世界和平,然而现实是很多库因为种种原因并未遵守semver,原因包括:



  • 不可预知的bug,本来以为某个版本只是bugfix,发布了patch版本,但是该patch却引入了未预料的breaking change导致semver被破坏

  • semver的设计过于理想,实际上即使是最小的bugfix,如果业务方无意中依赖了这个bug,仍然会导致breaking change,bug和breaking change的界限是模糊的

  • 认为semver没有太大意义,例如Typescript官方就承认从未遵循semver语义,实际上typescript经常在minor版本引入各种breaking change.

1、lock 非灵药

  那么在现实世界该如何处理这种问题,你肯定不希望自己的代码在本地是正常运行的,但是当你上线的时候就挂了吧。

  在你的测试完成和业务上线前的gap期间,如果你的某个依赖不遵循semver,产生了breaking change,那么你可能得半夜上线查bug了。我们发现问题的根源在于如何保证测试时候的代码和上线的代码是完全一致的。

2、直接写死版本

  一个很自然的想法就是,我直接把我的第三方依赖版本都写死不就行了

  然而问题并没这么简单,虽然你锁定了webpack的版本,但是webpack的依赖却没法锁定,如果webpack的某个依赖法生产不遵循semver的breaking change,我们的应用还是会受到影响,除非我们保证所有的第三方以及第三方的依赖都是写死版本,这即意味着整个社区放弃semver,这显然是不可能的。

3、yarn lock vs npm lock

  一个更加靠谱的写法是将项目里的依赖和第三方的依赖同时锁定,yarn的lock和npm的lock都支持该功能。

  如我们的项目安装了express的依赖,我们发现express的所有依赖及其依赖的依赖的版本在lock文件里都锁定了,这样另一个用户或者环境,能够凭借lock文件复现node_modules里各个库的版本。

  然而还是有一些场景lock无法覆盖,当我们第一次安装创建项目时或者第一次安装某个依赖的时候,此时即使第三方库里含有lock文件,但是npm install|(yarn install) 并不会去读取第三方依赖的lock,这导致第一次创建项目的时候,用户还是会可能触发bug。这在全局安装cli的场景下非常常见,经常会碰到上一次安装全局cli的时候正常,但是重新安装这个版本的cli却挂了,这很有可能是该cli的版本的某个上游依赖发生了breaking change,因为不存在全局环境的lock,因此目前没有较好的解决方式。

4、库里应该提交lock文件吗

  前面提到npm和yarn在install的时候并不会读取第三方库里的lock文件,那么我们编写库的时候还有必要提供lock文件吗。

  不知道大家有没有过这种经验,某天发现了某个第三方库存在某个bug,摩拳擦掌的将该库下载下来,准备修复下发个mr,一顿npm install && npm build 操作猛如虎,然后就见到了一堆莫名其妙的编译错误,这些错误很可能个是编译工具的某个上游依赖的breaking change所致,经过一番google + stackoverflow仍然没有修复,这时候就基本上断了提mr的冲动,如果库的开发者将当前的编译环境的lock提交上来,则很大程度上可以避免该问题。


六、pnpm 介绍

1、pnpm在解决phantom depdency问题的同时,在此基础上也解决了doopelganger问题

// 考察如下代码
// package.json
{
"dependencies": {
"debug": "3",
"express": "4.0.0",
"koa": "^2.11.0"
}
}
// 使用pnpm安装相关依赖后,我们发现项目中存在debug的两个版本
dependencies:
debug
3.1.0
express
4.0.0
├── debug
0.8.1
├─┬ send
0.2.0
│ └── debug
1.0.5
└─┬ serve
-static 1.0.1
└─┬ send
0.1.4
└── debug
1.0.5
koa
2.11.0
└── debug
3.1.0%

  查看node_modules里的版本,我们发现区别于yarn,pnpm是将不同版本放在同一层级里通过软链选择加载版本,而yarn则是放在不同层级,依赖递归查找算法来选择版本

  我们发现pnpm的node_modules里包含了三个版本,并且不同的模块分别连接到了三个版本

 

  这样即使出现版本冲突,只需要将各个模块进行链接即可,并不需要每个模块再进行重复安装模块。

  我们可以发现pnpm避免直接依赖node_modules的递归查找依赖的性质,而是直接通过软链解决了phantom dependency和doppelgangers 问题。因为彻底的避免了包的重复问题,其节省了大量的空间和加快了安装速度

  以一个monorepo项目为例

  对比一下:

  pnpm: node_modules大小359M,安装耗时20s

  yarn: node_modules大小1.2G,安装耗时173s

  差别非常显著

2、global store

  pnpm不仅仅能保证一个项目里的所有package的每个版本是唯一的,甚至能保证你使得你不同的项目之间也可以公用唯一的版本(只需要公用store即可),这样可以极大的节省了磁盘空间。核心就在于pnpm不再依赖于node的递归向上查找node_modules的算法,因为该算法强依赖于node_modules的物理拓扑结构,这也是导致不同项目的项目难以复用node_modules的根源。


学习文章:https://zhuanlan.zhihu.com/p/137535779




推荐阅读
  • 从零基础到精通的前台学习路线
    随着互联网的发展,前台开发工程师成为市场上非常抢手的人才。本文介绍了从零基础到精通前台开发的学习路线,包括学习HTML、CSS、JavaScript等基础知识和常用工具的使用。通过循序渐进的学习,可以掌握前台开发的基本技能,并有能力找到一份月薪8000以上的工作。 ... [详细]
  • Windows下配置PHP5.6的方法及注意事项
    本文介绍了在Windows系统下配置PHP5.6的步骤及注意事项,包括下载PHP5.6、解压并配置IIS、添加模块映射、测试等。同时提供了一些常见问题的解决方法,如下载缺失的msvcr110.dll文件等。通过本文的指导,读者可以轻松地在Windows系统下配置PHP5.6,并解决一些常见的配置问题。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 本文介绍了PhysioNet网站提供的生理信号处理工具箱WFDB Toolbox for Matlab的安装和使用方法。通过下载并添加到Matlab路径中或直接在Matlab中输入相关内容,即可完成安装。该工具箱提供了一系列函数,可以方便地处理生理信号数据。详细的安装和使用方法可以参考本文内容。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • 本文介绍了一个适用于PHP应用快速接入TRX和TRC20数字资产的开发包,该开发包支持使用自有Tron区块链节点的应用场景,也支持基于Tron官方公共API服务的轻量级部署场景。提供的功能包括生成地址、验证地址、查询余额、交易转账、查询最新区块和查询交易信息等。详细信息可参考tron-php的Github地址:https://github.com/Fenguoz/tron-php。 ... [详细]
  • VueCLI多页分目录打包的步骤记录
    本文介绍了使用VueCLI进行多页分目录打包的步骤,包括页面目录结构、安装依赖、获取Vue CLI需要的多页对象等内容。同时还提供了自定义不同模块页面标题的方法。 ... [详细]
  • 本文介绍了Sencha Touch的学习使用心得,主要包括搭建项目框架的过程。作者强调了使用MVC模式的重要性,并提供了一个干净的引用示例。文章还介绍了Index.html页面的作用,以及如何通过链接样式表来改变全局风格。 ... [详细]
  • Android日历提醒软件开源项目分享及使用教程
    本文介绍了一款名为Android日历提醒软件的开源项目,作者分享了该项目的代码和使用教程,并提供了GitHub项目地址。文章详细介绍了该软件的主界面风格、日程信息的分类查看功能,以及添加日程提醒和查看详情的界面。同时,作者还提醒了读者在使用过程中可能遇到的Android6.0权限问题,并提供了解决方法。 ... [详细]
  • node.jsrequire和ES6导入导出的区别原 ... [详细]
author-avatar
刘刚michaelup_340
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有