基础15:npm、yarn、pnpm

2023-11-18

npm2

用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。
执行 npm init, npm install express,可以看到node_modules目录如下:

在这里插入图片描述

可以看到,npm2的node_modules是嵌套的。
这种方式的优点就是模块依赖关系清晰。
缺点也比较明显:

  1. 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下,最多260多个字符。
  2. 大量重复的包被安装,文件体积超级大。比如跟 foo 同级目录下有一个baz,两者都依赖于同一个版本的lodash,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。
  3. 模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。

当时npm还没解决这些问题,社区就出来一个新的解决方案了,那就是 yarn。

yarn

yarn生成的node_modules目录如下:
在这里插入图片描述

将所有内部依赖平铺到最外面一层,解决了上述嵌套方案的缺陷。
后面npm3 + 也采用类次方案实现了。
所有的依赖都被拍平到node_modules目录下,不再有很深层次的嵌套关系。这样在安装新的包时,根据 node require 机制,会不停往上级的node_modules当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。
之前的问题是解决了,但仔细想想这种扁平化的处理方式,它真的就是无懈可击吗?并不是。它照样存在诸多问题:

  1. 依赖结构的不确定性。
  2. 扁平化算法本身的复杂性很高,耗时较长。
  3. 项目中仍然可以非法访问没有声明过依赖的包。

怎么理解第一条中的不确定性呢?
假如现在项目依赖两个包 foo 和 bar,这两个包的依赖又是这样的:

image

那么 npm/yarn install 的时候,通过扁平化处理之后,究竟是这样
image

还是这样的?
image

答案是: 都有可能。取决于 foo 和 bar 在 package.json中的位置,如果 foo 声明在前面,那么就是前面的结构,否则是后面的结构。

这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是package-lock.json(npm 5.x才出现)还是yarn.lock,都是为了保证 install 之后都产生确定的node_modules结构。

Phantom dependencies幽灵依赖

Phantom dependencies 被称之为幽灵依赖或幻影依赖,解释起来很简单,即某个包没有在package.json 被依赖,但是用户却能够引用到这个包。

比如yarn打包:
A依赖B, B依赖C,,那么 A 就算没有声明 C 的依赖,由于有依赖提升的存在,C 被装到了 A 的node_modules里面,在A里面引入C,没什么问题的。
但是,
第一, B 的版本是可能随时变化的,假如之前依赖的是C@1.0.1,现在发了新版,新版本的 B 依赖 C@2.0.1,那么在项目 A 当中 npm/yarn install 之后,装上的是 2.0.1 版本的 C,而 A 当中用的还是 C 当中旧版的 API,可能就直接报错了。
第二,如果 B 更新之后,可能不需要 C 了,那么安装依赖的时候,C 都不会装到node_modules里面,A 当中引用 C 的代码直接报错。
还有一种情况,在 monorepo 项目中,如果 A 依赖 X,B 依赖 X,还有一个 C,它不依赖 X,但它代码里面用到了 X。由于依赖提升的存在,npm/yarn 会把 X 放到根目录的 node_modules 中,这样 C 在本地是能够跑起来的,因为根据 node 的包加载机制,它能够加载到 monorepo 项目根目录下的 node_modules 中的 X。但试想一下,一旦 C 单独发包出去,用户单独安装 C,那么就找不到 X 了,执行到引用 X 的代码时就直接报错了。

npm 也有想过去解决这个问题,指定**–global-style**参数即可禁止变量提升,但这样做相当于回到了当年嵌套依赖的时代,一夜回到解放前,前面提到的嵌套依赖的缺点仍然暴露无遗。

pnpm

回想下,npm3+和yarn为什么要做node_modules 扁平划处理,不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?
那如果不复制文件,比如通过link。
打开node_modules如下:
在这里插入图片描述
可以看到,该目录结构比较清晰,只有一个我们直接依赖的包express,并没有把express需要引用的包平铺开,并且也不在express包下面嵌套。
同时还有个 .pnpm 目录,目录结构如下:

.pnpm
├── lock.yaml
├── node_modules
│   ├── .bin
│   ├── accepts
│   ├── array-flatten
│   ├── body-parser
│   ├── bytes
│   ├── call-bind
│
│  
├── registry.npmmirror.com+accepts@1.3.8
│   └── node_modules
├── registry.npmmirror.com+array-flatten@1.1.1
│   └── node_modules
├── registry.npmmirror.com+body-parser@1.20.1
│   └── node_modules
├── registry.npmmirror.com+bytes@3.1.2
│   └── node_modules
├── registry.npmmirror.com+call-bind@1.0.2
│   └── node_modules

.pnpm 以平铺的形式储存着所有的包,正常的包都可以在这种命名模式的文件夹中被找到:

.pnpm/<organization-name>+<package-name>@<version>/node_modules/<name>

// 组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称)

我们称.pnmp为虚拟存储目录,该目录通过<package-name>@<version>来实现相同模块不同版本之间隔离和复用,由于它只会根据项目中的依赖生成,并不存在提升,所以它不会存在之前提到的 幽灵依赖 问题!
那么它如何跟文件资源进行关联的呢?又如何被项目中使用呢?
答案是Store + Links!

Store

pnpm 资源在磁盘上的存储位置。

由于每个磁盘有自己的存储方式,所以Store会根据磁盘来划分。 如果磁盘上存在主目录,存储则会被创建在 /.pnpm-store;如果磁盘上没有主目录,那么将在文件系统的根目录中创建该存储。 例如,如果安装发生在挂载在 /mnt 的文件系统上,那么存储将在 /mnt/.pnpm-store 处创建。 Windows系统上也是如此。

可以在不同的磁盘上设置同一个存储,但在这种情况下,pnpm复制包 而不是 硬链接 它们,因为硬链接只能发生在同一文件系统同一分区上。

如果是 npm 或 yarn,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次。
如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。

Links(hard link & symbolic link)

pnpm 是怎么做到如此大的性能提升的呢?一部分原因是使用了计算机当中的 Hard link ,它减少了文件下载的数量,从而提升了下载和响应速度。

hard link

通过hard link, 用户可以通过不同的路径引用方式去找到某个文件,需要注意的是一般用户权限下只能硬链接到文件,不能用于目录。
pnpm 会在Store(上面的Store) 目录里存储项目 node_modules 文件的 hard links ,通过访问这些link直接访问文件资源。
举个例子,例如项目里面有个 2MB 的依赖 react,在 pnpm 中,看上去这个 react依赖同时占用了 2MB 的 node_modules 目录以及全局 store 目录 2MB 的空间(加起来是 4MB),但因为 hard link 的机制使得两个目录下相同的 2MB 空间能从两个不同位置进行 CAS寻址 直接引用到文件,因此实际上这个react依赖只用占用2MB 的空间,而不是4MB。

如何判断是否是hard link?

  1. mac和linux中,hard link的文件和普通文件无异,node甚至无法区分hard link文件;
  2. 可以通过 ls -i 列出文件信息,第一个参数就是文件的 inode 值,其中,具有相同 inode 节点的多个文件互为hard link文件;
  3. 这样 就可以通过不同项目观察同一个依赖文件是不是一样的 inode 就能确定是不是同为 hard link了。
symbolic link

由于hark link只能用于文件不能用于目录,但是pnpm的node_modules是树形目录结构,那么如何链接到文件? 通过symbolic link(也可称之为软链或者符号链接)来实现!

pnpm在全局通过Store来存储所有的node_modules依赖,并且在.pnpm/node_modules中存储项目的hard links,通过hard link来链接真实的文件资源,项目中则通过symbolic link链接到.pnpm/node_modules目录中,依赖放置在同一级别避免了循环的软链。

如何判断是否是symbolic link?
打开文件夹,显示简介,mac会有展示,如下图:
在这里插入图片描述

pnpm中是如何结合hard link和symbolic link的

官方给了一张原理图,
在这里插入图片描述
从上图可以看出,项目直接依赖 bar, bar又依赖 foo包。
pnpm下载依赖时,首先在node_modules下创建一个bar@1.0.0的symbolic link,链向图示位置,然后在.pnpm中平铺所有依赖,观察bar@1.0.0/node_modules/,里面的bar文件hard link真实文件,foo又是symbolic link到上层foo@1.0.0,这样bar就能够根据路径找到foo了。

pnpm prune

根据计数引用原理删除不需要的依赖包

npx 、 nvm

npx

首先 npx 是一个工具,旨在帮助完善使用npm注册表中的软件包的体验-npm使得超级容易安装和管理注册表中托管的依赖项,npx使得使用CLI工具和托管在该注册表中的其他可执行文件变得容易 注册表。 到目前为止,它大大简化了许多需要使用纯npm进行一些程序化的事情:

  1. 直接调用项目安装的模块

    npm install -D mocha // 项目中安装mocha测试模块
    // 一般来说,调用 Mocha ,只能在项目脚本和 package.json 的scripts字段里面,
    //  如果想在命令行下调用,必须像下面这样。
    // 项目的根目录下执行 node-modules/.bin/mocha --version
    
    // 如果使用npx
    npx mocha --version
    // npx运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
    
  2. 控制调用本地/远程模块

    • 强制使用本地模块,不下载远程模块
      npx --no-install http-server
    • 忽略本地同名模块,强制安装并使用远程模块
      npx --ignore-existing create-react-app my-react-app
  3. 执行一次性命令,避免全局安装

    $ npx create-react-app my-app 安装一个临时的 create-react-app 并且调用它,不会污染全局安装

    场景:
    比如尝试一个cli工具,可能这辈子只能用到这一次,不想这次使用完后还一直存在电脑硬盘里占用内存。
    步骤:

    • 当不在$PATH中时调用npx将自动从npm注册表安装一个具有该名称的包,并调用它。
    • npx 将create-react-app下载到一个临时目录,使用以后再删除。
    • 完成后,安装的软件包将会删除,而不会出现在globals中,不必担心全局污染。
  4. 使用不同版本的Node(同样适用于其他库的指定版本)

    npx -p node@ node -v 可以用于一次性运行node版本

  5. 执行Github源码

    # 执行 Gist 代码
    $ npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32
    # 执行仓库代码
    $ npx github:piuccio/cowsay hello
    
nvm

这是一个切换node版本的一个工具。和 n 命令类似。

  1. n 是一个需要全局安装的 npm package。所以在使用n时,必须得有一个版本的node环境。
  2. 在安装的时候,n 会先将指定版本的 node 存储下来,然后将其复制到/usr/local/bin。
  3. nvm 是一个独立软件包,需要单独使用它的安装逻辑。
  4. 在使用 nvm 安装 node 的时候,nvm 将不同的 node 版本存储到 ~/.nvm// 下,然后修改 $PATH,将指定版本的 node 路径加入,这样我们调用的 node 命令即是使用指定版本的 node。
  5. nvm 的全局模块存在于各自版本的沙箱中,切换版本后需要重新安装,不同版本间也不存在任何冲突。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基础15:npm、yarn、pnpm 的相关文章

  • 在 JSP 中从 JavaScript/jQuery 调用后端 Java 方法

    我有一个 JSP 其中有一个select包含实体种类名称的列表 当我选择一个实体类型时 我需要填充另一个实体类型select包含所选实体类型的字段名称的列表 为此 我调用了一个 JavaScript 函数onchange event 在 J
  • 如何使用 React 和 Jest 模拟 onPaste 事件?

    我正在尝试在我的反应项目上使用 JEST 测试来模拟粘贴事件 我有一个外部组件 App 其中包含带有 onPaste 事件的输入字段 我想测试过去的数据并检查输入值 it on past with small code gt Create
  • 如何在 JavaScript 中检查 IsPostBack? [复制]

    这个问题在这里已经有答案了 我需要运行 JavaScript 函数 onLoad 但仅在页面第一次加载时才执行 即不是回发的结果 基本上 我需要检查 JavaScript 中的 IsPostBack 谢谢 服务器端 写 if IsPostB
  • 错误 C2039: 'IsNearDeath': 不是 'Nan::Persistent> 的成员

    我最近升级了我的nodejs to v12 3 1 现在当我尝试跑步时npm install在我的项目存储库中 我收到上述错误 error C2059 syntax error compiling source file src custo
  • Svelte 路线给我 404

    我在 Svelte 中为我的应用程序创建了一个简单的路由器 如果我从导航栏访问链接 它就可以工作 如果我重新加载页面 它会给我 404 为什么
  • Vimeo 播放器 JS API 在 iOS 中无法运行

    我正在尝试使用 API 来播放视频 但只有在 iOS 中单击播放器中的播放按钮后它才有效 在桌面版和 Android 版 Chrome 中 它运行良好 http codepen io bdougherty pen JgDfm http co
  • 跟踪 HTML5 音频元素的播放次数?

    跟踪 HTML5 音频元素播放次数的最佳方法是什么 我们也可以使用 Google Analytics 如果这是最好的方法 HTML5 音频元素有基本的回调 https developer mozilla org En Using audio
  • 当函数定义为无参数时,为什么我可以调用带参数的函数?

    再会 我偶然发现了一些我在 JavaScript 领域从未见过的东西 但我想对于更了解该语言的人来说这是很容易解释的 下面我有以下函数 代码取自书籍 JavaScript Ninja 的秘密 function log try console
  • 没有 ssl 的 Web 加密 API

    我编写了一个用于安全消息传输的小网络应用程序 以了解有关加密的更多信息 并想向我的朋友展示它并让他们玩一下 所以我将它托管在我的小服务器上 并惊讶地发现 Web Crypto API 我竭尽全力开始工作 因为它的错误消息不是很具体 需要 S
  • Knockout JS - 如何正确绑定 observableArray

    请看一下这个例子 http jsfiddle net LdeWK 2 http jsfiddle net LdeWK 2 我想知道如何绑定可观察数组的值 我知道上面例子中的问题 就是这一行 p Editing Fruit p
  • IE 11 的 Map(iterable) 替代方案

    不幸的是我必须支持IE11 我使用以下代码创建地图 已使用 entries 的 polyfill const map new Map Object entries array 但由于IE11不支持iterable构造函数中Map 是空的 我
  • 从 onclick 属性调用 e.stopImmediatePropagation()

    如何从事件对象中获取事件对象onclick属性 我努力了 a href something html Click me a 另外 我也尝试过这个 a href something html Click me a 但控制台只显示 a 元素 我
  • 关闭模态后清除模态字段

    我有这个模式
  • 如何在javascript中解压二进制文件?

    我正在尝试将一些现有代码从 python 移植到 javascript 并且不确定如何处理以下行 var1 var2 struct unpack
  • Material UI Drawer设置背景色

    如何简单设置Material UI的背景色Drawer 尝试过这个 但不起作用
  • ES6 标记的​​模板函数如何解释它们的参数顺序?

    考虑以下代码 function f console log Array from arguments var x 2 var y 3 f before x y after 论据f将会 根据 Traceur http google githu
  • 如果使用 javascript 在 ASP.NET 中页面验证失败,如何禁用提交按钮

    如果页面上的验证失败 我需要使用 JavaScript 禁用表单上的保存按钮 如果没有 则必须使用以下代码启用它 Code
  • 如何在 i18next 中使用多个命名空间?

    我刚刚启动 i18next 我想为项目中的每个模块创建翻译文件 看起来使用命名空间是执行此操作的正确方法 该项目可以使用多个视图创建页面布局 因此我需要能够同时翻译多个命名空间中的字符串 我创建了一个具有两个命名空间的简单示例 但我只能使用
  • 如何在 Javascript 中检测网络丢失?

    我的 Web 应用程序适用于多种手持设备 例如 iPad Galaxy 选项卡等 应用程序从服务器请求图像并在客户端上呈现 现在的问题有时会发生 在图像渲染过程中网络连接会丢失 而不是在设备上显示 html 无图像图标时 我想优雅地处理这种
  • Google 地图 v3 信息窗口在地图视口外打开

    如果单击地图视口顶部附近的标记 信息窗口将加载到可视区域之外 并且必须拖动地图才能查看信息窗口内容 理想情况下 我不希望地图自动平移 有没有办法以不同的方向加载信息窗口 例如如果标记位于视口的顶部 则以向下的方向显示信息窗口 不 你不能以不

随机推荐