重新认识包管理器

包管理器: 包管理器或包管理系统是一系列软件工具的集合, 这些软件工具用和电脑操作系统一致的方式, 使应用的安装, 升级, 配置和删除软件包的过程自动化, 它通常维护一个数据库软件的依赖和版本信息, 防止软件不匹配和无法跟踪 --维基百科

当我们知道上面👆所说的包管理器的含义,那么我们聚焦于前端领域,看看前端的包管理器以及工具:

1、常用包管理器

目前,常用的包管理工具有 npm/yarn/pnpm 三种:

  • npm npm 是由 node.js 官方推出的包管理器,于 2010 年首次发布,旨在解决 node.js 项目中的依赖管理问题,后续也被前端项目所广泛使用
  • yarn yarn 是由于 2016 年推出的另一个包管理工具,为了解决当时 npm 的一些性能以及稳定性问题,使用一种全新的算法来优化依赖关系的解析和安装流程。即将当时 npm 的 node_modules 嵌套改为平铺(目前 npm 也是平铺策略)
  • pnpm pnpm 采用了一种全新的依赖解决方案,它使用硬链接和符号链接 结合方式显著减小了硬盘空间的占用。最简单的一个示例是:当你拥有 100 个依赖 lodash 的项目,使用 pnpm 磁盘将仅仅占用一份 lodash 多的体积大小

三者功能对比:链接

Feature Comparison

Feature pnpm Yarn npm
Workspace support ✔️ ✔️ ✔️
Isolated node_modules ✔️ - The default ✔️ ✔️
Hoisted node_modules ✔️ ✔️ ✔️ - The default
Autoinstalling peers ✔️ ✔️
Plug'n'Play ✔️ ✔️ - The default
Zero-Installs ✔️
Patching dependencies ✔️ ✔️
Managing Node.js versions ✔️
Has a lockfile ✔️ - pnpm-lock.yaml ✔️ - yarn.lock ✔️ - package-lock.json
Overrides support ✔️ ✔️ - Via resolutions ✔️
Content-addressable storage ✔️
Dynamic package execution ✔️ - Via pnpm dlx ✔️ - Via yarn dlx ✔️ - Via npx
Side-effects cache ✔️
Listing licenses ✔️ - Via pnpm licenses list ✔️ - Via a plugin

性能对比 链接

2、模块解析时resolve算法

为了保证模块的正确加载,实现了额外的依赖查找算法resolve算法。比如:当我们引入一个模块时,我们将会在当前路径node_modules 中寻找该 package,如果找不到则递归上级目录node_modules 寻找,直至根路径。

ini 复制代码
const lodash = require('lodash')

正是因为有了 resolve 算法,不管我们使用的 npm/yarn 目前的 node_modules 扁平安装方式,还是 pnpm 的软硬链接结合方式,装包所需要做到的就是在符合 resolve 算法的同时,使得安装速度加快,安装体积减小。

除此之外monorepo 也正是基于 resolve 算法,使其解决如何依赖其它其它子包,以及如何减少整体安装体积。

3、项目中存在多个lockfile,怎么办?

如果一个项目中同时存在多个 lockfile,则表明该项目包管理器管理混乱,此时应该只保留正确包管理器的 lockfile并将其它包管理器对应的 lockfile 置于 .gitignore

那此时如何确定正确的包管理器呢?此时可以通过以下操作来确定:

  • 比较多个 lockfile 的上次修改时间(lastModified),以最后修改为准
  • 查看是否有 CI/CD,如果有跟着 CI/CD 中的包管理工具确认
  • 查看是否有 Dockerfile,如果有跟着 Dockerfile 确认
  • 查看是否有文档,如果有跟着文档走

除此可以借助antfu开源的 @antfu/ni

shell 复制代码
npm i -g @antfu/ni

如果您的项目中同时存在多个锁文件,@antfu/ni 将按照以下优先级进行选择:Yarn > pnpm > npm > bun。也就是说,如果同时存在 yarn.lockpackage-lock.json@antfu/ni 将优先选择 Yarn

npm link可以帮助我们模拟包安装后的状态,它会在系统中做一个快捷方式映射,让本地的包就好像install过一样,可以直接使用,这样就便于我们本地开发时调试未发布的包!

由于 yarn/npm link 的原理相同,此处使用 yarn link 软连接rollup 为例说明使用方式以及原理

  • rollup 源码目录,通过 npm run watch 进行构建,此时会生成带有 source-map 的构建文件。
  • rollup 源码目录,执行 yarn link,它会自动寻找当前目录的 package.json 中的 name 字段,并创建全局目录(~/.config/yarn/link)软链接至该项目
  • 在自己项目,执行 yarn link rollup,将会替换 node_modules/rollup,其软链接至全局目录

简单来说yarn/npm link 的原理:

1、yarn link:将当前 package 软链接至 ~/.config/yarn/link,其名为 package 的名称,即 package.json 中的 name 字段

2、yarn link rollup:将当前项目,即需调试项目目录中的 node_modulels/rollup 软链接到 ~/.config/yarn/link/rollup

5、node_modules 拓扑结构

1、 npm v2: 嵌套结构

直接依赖会平铺在 node_modules 下,子依赖嵌套在直接依赖的 node_modules 中。

比如项目依赖了A 和 C,而 A 和 C 依赖了不同版本的 B@1.0 和 B@2.0,node_modules 结构如下:

kotlin 复制代码
node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
└── C@1.0.0
    └── node_modules
        └── B@2.0.0

如果 D 也依赖 B@1.0,会生成如下的嵌套结构:

kotlin 复制代码
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

可以看到同版本的 B 分别被 A 和 D 安装了两次。

2、npm v3: 平铺结构

为了将嵌套的依赖尽量打平,避免过深的依赖树和包冗余,npm v3 将子依赖「提升」(hoist),采用扁平的 node_modules 结构,子依赖会尽量平铺安装在主依赖项所在的目录中。

kotlin 复制代码
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
    └── node_modules
        └── B@2.0.0

可以看到 A 的子依赖的 B@1.0 不再放在 A 的 node_modules 下了,而是与 A 同层级。

而 C 依赖的 B@2.0 因为版本号原因还是嵌套在 C 的 node_modules 下。

这样不会造成大量包的重复安装,依赖的层级也不会太深,解决了依赖地狱问题,但也形成了新的问题(幽灵依赖)。

比如上方的示例其实我们只安装了 A 和 C:

css 复制代码
{
  "dependencies": {
    "A": "^1.0.0",
    "C": "^1.0.0"
  }
}

由于 B 在安装时被提升到了和 A 同样的层级,所以在项目中引用 B 还是能正常工作的

6、'臭名昭著'的幽灵依赖

如果一个npm包没有在package.json中声明而直接被项目所依赖,那么这个 npm 包就是幽灵依赖(Phantom Denendency)

The Dependency Hell

比如下面这个场景:

如果使用 npm/yarn 来作为包管理器时,那我们可以在项目中直接使用 loose-envify 而无需手动 npm install。假设此时项目中需要使用到该 npm 包,但没有手动安装。

ini 复制代码
const envify = require('loose-envify')

因为对于 npm/yarn 而言,它会将 loose-envify 该依赖提升至 node_modules/loose-envify 目录下,根据 npm 包进行 resolve 的算法而言,此时可以直接使用它。

但是随着时间的推移,假设 React 发布了 20.0.0 版本,并在内部实现中废弃掉了 loose-envify,但此时我们的项目因为依赖 loose-envify 而又无法找到,因此报错。

注:React 在 17 版本时依赖 object-assign 与 loose-envify 两个直接依赖,但是在 React 18 版本废弃掉了 object-assign 依赖

1、它会导致什么问题

简单来说:

  • 依赖缺失
  • 兼容性问题
  • 增加项目的体积
2、解决措施

1、 pnpm 解决了该问题,它的 node_modules 目录中仅包含 package.json 声明的 npm 包

pnpm三层寻址的设计:

第一层处理直接依赖关系。第一层寻找依赖是 nodejs 或 webpack 等运行环境/打包工具进行的,他们在 node_modules 文件夹寻找依赖,并遵循就近原则。所以第一层依赖文件势必写在node_modules/package-a下,一方面遵循依赖寻找路径,一方面没有将依赖都拎到上级目录,也没有将依赖打平,目的就是还原最语义化的 package.json。同时每个包的子依赖也从该包内寻找,解决了多版本管理的问题,同时也使 node_modules 拥有一个稳定的结构,即该目录组织算法仅与 package.json 定义有关,而与包安装顺序无关。

第二层处理符号链接依赖项。解决 npm@2.x 设计带来的问题,主要是包复用的问题。利用软链接解决了代码重复引用的问题。相比 npm@3 将包打平的设计,软链接可以保持包结构的稳定,同时用文件指针解决重复占用硬盘空间的问题。

bash 复制代码
(bar 将被符号链接到 foo@1.0.0/node_modules 文件夹)
node_modules
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>/bar
    └── foo@1.0.0
        └── node_modules
            ├── foo -> <store>/foo
            └── bar -> ../../bar@1.0.0/node_modules/bar

第三层是硬链接寻址,脱离当前项目路径指向一个全局统一管理路径,这正是跨项目复用的必然选择,解决了多个项目对于同一个包的多份拷贝过于浪费问题。

2、但还有一种更难以解决的幻影依赖问题,即用户在 Monorepo 项目根目录安装了某个包,这npx/dlx个包可能被某个子 Package 内的代码寻址到,要彻底解决这个问题,需要配合使用 Rush,在工程上通过依赖问题检测来彻底解决。

原文链接:blog.csdn.net/qq_45225759...

7、npx/dlx

简而言之: npxnode package execute 的意思,dlxdownload and execute

  • npx: 可以通过 npx 直接在命令行执行项目中命令,若不存在则安装它

除了 npm,在 pnpm/yarn 上也有对应的工具,如下所示:

1、 npx,实际上是 node package execute 的简写 2、 yarn dlx 3、 pnpx,实际上是 pnpm dlx 的简写

基本功能也是一样:

1、直接执行 node_modules/.bin 递归目录(如果当前 node_modules/.bin 目录下无法找到,则去上一级 node_modules/.bin 目录下寻找)下的可执行文件 2、直接执行全局 npm 命令行工具,不存在则下载

那当它们不存在则下载时,下载了哪里,又是如何管理全局命令行工具的?

  • 对于 npx 而言,如果该 npm 包不存在,则会下载到 npx 全局目录 ~/.npm/``._npx,而不会直接下载到全局可执行目录污染 $PATH

或者这样理解:

npx 首先会执行 ./node_packages/.bin 下面的命令,如果没有找到才会去下载一个 npm package 并执行。

这会造成一个问题,如果你本地有这个 package,那么执行的命令可能不是最新的版本

所以 yarn 和 pnpm 将这个命令拆分成两个,一个是执行本地,另一个是下载最新的package并执行:

  • yarn exec , yarn dlx
  • pnpm exec , pnpm dlx
相关推荐
正小安21 分钟前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch2 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光2 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   2 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   2 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web2 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常2 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇3 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr3 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho4 小时前
【TypeScript】知识点梳理(三)
前端·typescript