npm是Node.JS的包管理工具,除此之外,社区有一些类似的包管理工具如yarn、pnpm和cnpm,以及集团内部使用的tnpm。我们在项目开发过程中通常使用以上主流包管理器生成node_modules目录安装依赖并进行依赖管理。本文主要探究前端包管理器的依赖管理原理,希望对读者有所帮助。
当我们执行npm install
命令后,npm会帮我们下载对应依赖包并解压到本地缓存,然后构造node_modules 目录结构,写入依赖文件。那么,对应的包在node_modules目录内部是怎样的结构呢,npm主要经历了以下几次变化。
npm最早的版本中使用了很简单的嵌套模式进行依赖管理。比如我们在项目中依赖了A模块和C模块,而A模块和C模块依赖了不同版本的B模块,此时生成的node_modules目录如下:
可以看到这种是嵌套的node_modules结构,每个模块的依赖下面还会存在一个 node_modules 目录来存放模块依赖的依赖。这种方式虽然简单明了,但存在一些比较大的问题。如果我们在项目中增加一个同样依赖2.0版本B的模块D,此时生成的node_modules目录便会如下所示。虽然模块A、D依赖同一个版本B,但B却重复下载安装了两遍,造成了重复的空间浪费。这便是依赖地狱问题。
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── D@1.0.0
└── node_modules
└── B@1.0.0
一些著名的梗图:
npm v3完成重写了依赖安装程序,npm3通过扁平化的方式将子依赖项安装在主依赖项所在的目录中(hoisting提升),以减少依赖嵌套导致的深层树和冗余。此时生成的node_modules目录如下:
为了确保模块的正确加载,npm也实现了额外的依赖查找算法,核心是递归向上查找node_modules。在安装新的包时,会不停往上级node_modules中查找。如果找到相同版本的包就不会重新安装,在遇到版本冲突时才会在模块下的 node_modules 目录下存放该模块子依赖,解决了大量包重复安装的问题,依赖的层级也不会太深。
扁平化的模式解决了依赖地狱的问题,但也带来了额外的新问题。
幽灵依赖主要发生某个包未在package.json中定义,但项目中依然可以引用到的情况下。考虑之前的案例,它的package.json如右图所示。
在index.js中我们可以直接require A,因为在package.json声明了该依赖,但是,我们require B也是可以正常工作的。
var A = require('A');
var B = require('B'); // ???
因为B是A的依赖项,在安装过程中,npm会将依赖B平铺到node_modules下,因此require函数可以查找到它。但这可能会导致意想不到的问题:
考虑在项目中继续引入的依赖2.0版本B的模块D与而1.0版本B的模块E,此时无论是把B 2.0还是1.0提升放在顶层,都会导致另一个版本存在重复的问题,比如这里重复的2.0。此时就会存在以下问题:
在前端包管理的背景下,确定性指在给定package.json下,无论在何种环境下执行npm install命令都能得到相同的node_modules目录结构。然而npm v3是不确定性的,它node_modules目录以及依赖树结构取决于用户安装的顺序。
考虑项目拥有以下依赖树结构,其npm install产生的node_modules目录结构如右图所示。
假设当用户使用npm手动升级了模块A到2.0版本,导致其依赖的模块B升级到了2.0版本,此时的依赖树结构如下。
此时完成开发,将项目部署至服务器,重新执行npm install,此时提升的子依赖B版本发生了变化,产生的node_modules目录结构将会与用户本地开发产生的结构不同,如下图所示。如果需要node_modules目录结构一致,就需要在package.json修改时删除node_modules结构并重新执行npm install。
在npm v5中新增了package-lock.json。当项目有package.json文件并首次执行npm install安装后,会自动生成一个package-lock.json文件,该文件里面记录了package.json依赖的模块,以及模块的子依赖。并且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。通过package-lock.json,保障了依赖包安装的确定性与兼容性,使得每次安装都会出现相同的结果。
考虑上文案例,初始时安装生成package-lock.json如左图所示,depedencies对象中列出的依赖都是提升的,每个依赖项中的requires对象中为子依赖项。此时更新A依赖到2.0版本,如右图所示,并不会改变提升的子依赖版本。因此重新生成的node_modules目录结构将不会发生变化。
依赖版本兼容性就不得不提到npm使用的SemVer版本规范,版本格式如下:
在使用第三方依赖时,我们通常会在package.json中指定依赖的版本范围,语义化版本范围规定:
语义化版本规则定义了一种理想的版本号更新规则,希望所有的依赖更新都能遵循这个规则,但是往往会有许多依赖不是严格遵循这些规定的。因此一些依赖模块子依赖不经意的升级,可能就会导致不兼容的问题产生。因此package-lock.json给每个模块子依赖标明了确定的版本,避免不兼容问题的产生。
Yarn 是在2016年开源的,yarn 的出现是为了解决 npm v3 中的存在的一些问题,那时 npm v5 还没发布。Yarn 被定义为快速、安全、可靠的依赖管理。
Yarn 生成的 node_modules 目录结构和 npm v5 是相同的,同时默认生成一个 yarn.lock 文件。对于上文例子,生成的yarn.lock文件如下:
A@^1.0.0:
version "1.0.0"
resolved "uri"
dependencies:
B "^1.0.0"
B@^1.0.0:
version "1.0.0"
resolved "uri"
B@^2.0.0:
version "2.0.0"
resolved "uri"
C@^2.0.0:
version "2.0.0"
resolved "uri"
dependencies:
B "^2.0.0"
D@^2.0.0:
version "2.0.0"
resolved "uri"
dependencies:
B "^2.0.0"
E@^1.0.0:
version "1.0.0"
resolved "uri"
dependencies:
B "^1.0.0"
可以看到yarn.lock使用自定义格式而不是JSON,并将所有依赖都放在顶层,给出的理由是便于阅读和审查,减少合并冲突。
npm v5 只需要 package.lock 文件就可以确定 node_modules 目录结构yarn.lock 无法确定顶层依赖,需要 package.json 和 yarn.lock 两个文件才能确定 node_modules 目录结构。node_modules 目录中 package 的位置是在 yarn 的内部计算出来的,在使用不同版本的 yarn 时可能会引起不确定性。
在Yarn 的2.x版本重点推出了Plug'n'Play(PnP)零安装模式,放弃了node_modules,更加保证依赖的可靠性,构建速度也得到更大的提升。因为Node依赖于node_modules查找依赖,node_modules的生成会涉及到下载依赖包、解压到缓存、拷贝到本地文件目录等一系列重IO的操作,包括依赖查找以及处理重复依赖都是非常耗时操作,基于node_modules的包管理器并没有很多优化的空间。
因此yarn反其道而行之,既然包管理器已经拥有了项目依赖树的结构,那也可以直接由包管理器通知解释器包在磁盘上的位置并管理依赖包版本与子依赖关系。
执行yarn --pnp
模式即可开启PnP模式。在PnP模式,yarn 会生成 .pnp.cjs 文件代替node_modules。该文件维护了依赖包到磁盘位置与子依赖项列表的映射。同时 .pnp.js 还实现了resolveRequest方法处理require请求,该方法会直接根据映射表确定依赖在文件系统中的位置,从而避免了在node_modules查找依赖的 I/O 操作。
pnp模式优缺点也非常明显:
pnpm1.0于2017年正式发布,pnpm具有安装速度快、节约磁盘空间、安全性好等优点,它的出现也是为了解决npm和yarn存在的问题。
因为在基于npm或yarn的扁平化node_modules的结构下,虽然解决了依赖地狱、一致性与兼容性的问题,但多重依赖和幽灵依赖并没有好的解决方式。因为在不考虑循环依赖的情况下,实际的依赖结构图为有向无环图(DAG),但是npm和yarn通过文件目录和node resolve算法模拟的实际上是有向无环图的一个超集(多出了很多错误祖先节点和兄弟节点之间的链接),这导致了很多的问题。pnpm也是通过硬链接与符号链接结合的方式,更加精确的模拟DAG来解决yarn和npm的问题。
硬链接可以理解为源文件的副本,使得用户可以通过不同的路径引用方式去找到某个文件,他和源文件一样的大小但是事实上却不占任何空间。pnpm 会在全局 store 目录里存储项目 node_modules 文件的硬链接。硬链接可以使得不同的项目可以从全局 store 寻找到同一个依赖,大大节省了磁盘空间。
软链接可以理解为快捷方式,pnpm在引用依赖时通过符号链接去找到对应磁盘目录(.pnpm)下的依赖地址。考虑在项目中安装依赖于foo模块的bar模块,生成的node_modules目录如下所示。
可以看到node_modules下的bar目录下并没有node_modules,这是一个符号链接,实际真正的文件位于.pnpm目录中对应的 <package-name>@version/node_modules/<package-name>
目录并硬链接到全局store中。而bar的依赖存在于.pnpm目录下<package-name>@version/node_modules
目录下,而这也是软链接到<package-name>@version/node_modules/<package-name>
目录并硬链接到全局store中。
而这种嵌套node_modules结构的好处在于只有真正在依赖项中的包才能访问,避免了使用扁平化结构时所有被提升的包都可以访问,很好地解决了幽灵依赖的问题。此外,因为依赖始终都是存在store目录下的硬链接,相同的依赖始终只会被安装一次,多重依赖的问题也得到了解决。
官网上的这张图清晰地解释了pnpm的依赖管理机制
看起来pnpm似乎很好地解决了问题,但也存在一些局限。
cnpm是由阿里维护并开源的npm国内镜像源,支持官方 npm registry 的镜像同步。tnpm是在cnpm基础之上,专为阿里巴巴经济体的同学服务,提供了私有的 npm 仓库,并沉淀了很多 Node.js 工程实践方案。
cnpm/tnpm的依赖管理是借鉴了pnpm ,通过符号链接方式创建非扁平化的node_modules结构,最大限度提高了安装速度。安装的依赖包都是在 node_modules 文件夹以包名命名,然后再做符号链接到 版本号 @包名的目录下。与pnpm不同的是,cnpm没有使用硬链接,也未把子依赖符号链接到单独目录进行隔离。
此外,tnpm新推出的rapid模式使用用户态文件系统(FUSE)对依赖管理做了一些新的优化。FUST类似于文件系统版的 ServiceWorker,通过 FUSE 可以接管一个目录的文件系统操作逻辑。基于此实现非扁平化的node_modules结构可以解决软链接的兼容性问题。限于篇幅原因这里不再详述,感兴趣可以移步真·深入浅出 tnpm rapid 模式 - 如何比 pnpm 快 10 秒。
通过上文探究的主流包管理器依赖管理机制,我们发现无论扁平化或非扁平化node_modules结构似乎都不完美,抛弃node_modules的PnP模式又不兼容当前Node的生态,无解。看起来似乎是Node与node_modules自身有点问题(?)。Node.JS作者Ryan也在JSConf上承认node_modules是他对Node的十大遗憾之一,但已经无法挽回了,随后他推荐了自己的新作Deno。那让我们看看JS的另一大运行时环境Deno是如何进行依赖管理的。
在Deno不使用npm、package.json以及node_modules,而是将引入源、包名、版本号、模块名全部塞进了 URL 里,通过URL导入依赖并进行全局统一缓存,不仅节省了磁盘空间,也优化了项目结构。
import * as log from "https://deno.land/std@0.125.0/log/mod.ts";
因此Deno中没有包管理器的概念,对于项目中的依赖管理,Deno提供了这样一种方案。由开发者创建dep.ts
,此文件中引用了所有必需的远程依赖关系,并且重新导出了所需的方法和类。本地模块从dep.ts
统一导入所需方法和类,避免单独使用URL导入外部依赖可能造成的不一致的问题。
// dep.ts
export {
assert,
assertEquals,
assertStringIncludes,
} from "https://deno.land/std@0.125.0/testing/asserts.ts";
// index.ts
import { assert } from './dep.ts';
Deno处理依赖的方式虽然解决了node_modules带来的种种问题,但目前体验也并不是很好。首先URL引入依赖的方式写法比较冗余繁琐,直接引用网络上文件的安全性也值得商榷;而且需要开发者手动维护dep.ts
文件,依赖来源不清晰,依赖变更还需要更改引入依赖的本地文件;此外,依赖包的生态也远远不及Node。
但Deno确实提供了另外一种思路,Node的包管理器似乎只是安装依赖、生成node_modules的“纯工具人”,真正查找resolve依赖的逻辑还是在Node做的,所以包管理器层面也没有太多优化的空间。Yarn的Pnp模式曾试图改变包管理器的地位,但也不敌强大的Node生态。因此Deno重启炉灶,将intall和resolve依赖过程合并,多余的node_modules与包管理器也就没什么存在的必要了。只是Deno当前的方式还不够成熟,期待后续的演进。
虽然目前还没有完美的依赖管理方案,但纵观包管理器的历史发展,是库与开发者互相学习和持续优化的过程,并且都在不断推动着前端工程化领域的发展,我们期待未来会出现更好的解决方案。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/BfijbUe017ZzlINnCIda8g
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。