四、包管理工具进阶指南

1、前言

承上:Node 包管理

至此,你已经可以快乐的玩耍 npm 了,但是,这还不够,等待你来挑战的是更进一步的包管理工具:yarn、pnpm

哎呀,我已经学会了 npm 了,已经够用了,还要学那些新的干嘛?

这是人之常情

人们总是害怕那些他们不能理解的事;对于没有见过的东西,人会排斥,因为内心会恐惧;对于新的东西,最好的办法就是先了解它,然后再通透它

喝完这碗鸡汤,路还是要赶的,当然,如果可以,我推荐你坐车;就像掌握这些进阶工具,让你对包管理更加得心应手;不仅让你站在巨人的肩膀上,更如臂使指,指哪打哪

2、yarn

首先,这个玩意怎么读?百度上的英式和美式发音都是 (所以当初学习的时候选择了这个读音),但身边同事有的读 ,还有的读 ;这些读法都是哪来的?趣意横生~话说大家都是怎么读的?

yarn 是由 Facebook、Google、Exponent 和 Tilde 共同开发的,旨在提供更快、更可靠的软件包安装和依赖关系管理;说直白点,就是为了解决 npm 乱七八糟问题的

yarn 一经发布,就瞬间大火,有取 npm 而代之的趋势;为什么?因为天下苦 npm 久矣~

npm 的诞生为我们打开了连接整个 JavaScript 天才世界的一扇大门;它允许我们分享借鉴来自各大洲的开发者使用的包,是具有跨时代意义的~但 npm 同时又存在许多问题,给开发者带来许多困扰

2.1、npm 的问题

2.1.1、嵌套问题(至今仍存在)

npm 发布最初(v1),采用的是嵌套结构;上图很好的诠释了 npm 的嵌套结构问题,早期的 npm 是嵌套部署依赖的 dependencies 的,这将造成依赖重复下载和嵌套层次过深的问题;npm 在 v3 版本(此时 yarn 发布了),采用扁平化结构,一定程度上解决了嵌套问题

至今为止(npm v9),npm 仍然没有彻底解决嵌套问题,同一依赖版本不一致时还是需要嵌套

2.1.2、下载速度

npm 发布最初(v1),采用的是同步下载,下载速度慢;v3 启用多线程并行下载;v5 借助 lock 文件缓存依赖信息实现缓存加载而非重新下载;但在速度上,始终还是略逊一筹

2.1.3、lock 文件

早期的 npm 没有 lock 文件,导致依赖结构不确定,给程序带来不可预知的问题。

package-lock.json 是 npm 5 版本开始,借鉴 yarn.lock 引入的一个文件,用于记录当前安装的每个包的确切版本号以及其依赖关系的锁定状态。

为什么需要 package-lock.json ?我们通过 package.json 来管理项目的依赖,默认情况下,都是锁定主版本,如以下形式

json 复制代码
"dependencies": {
    "tiange-npm": "^1.0.8"
}

前面说过,^1.. 会安装主版本号为 1 的最新依赖,如果长时间不执行 npm install,不同团队成员将使用不同版本的依赖,package-lock.json 就是为了解决此类问题

  • 锁定依赖关系:确保每次安装相同的软件包时都会得到相同的版本。这是非常重要的,因为不同版本之间可能存在兼容性问题。
  • 更快速的安装:当 package-lock.json 文件存在时,npm 会使用其中记录的下载地址直接从缓存中获取软件包,而无需重新下载。
  • 精确控制:package-lock.json 文件中包含了软件包所需的所有子依赖项及其版本号,可以确保项目在复制或部署到其他环境时能够精确。

注意,package-lock.json 文件应该被纳入源代码管理工具(如Git)进行版本控制,从而确保团队成员、构建服务器或其他人在构建和部署项目时都使用相同的软件包版本。

锁定版本更新

遥想当年刚学 npm 时,被这样一个问题困扰:既然我已经锁定了版本,为啥我有时改了 package.json 版本,锁定版本会更新,但有时候又不会更新?

没错,是否能锁定住版本,和 package.json 的依赖版本设置息息相关

  • 1、npm init -y 初始化 package.json;然后 npm i tiange 安装最新依赖,此时会 ^1.0.8
  • 2、修改版本 ^1.0.8 为 ^1.0.1;然后 npm i 安装全部依赖;你会发现并没有安装 1.0.1 版本的依赖
  • 3、采用具体版本,将 ^1.0.1 修改为 1.0.1;然后 npm i 安装全部依赖;依赖更新为 1.0.1
  • 4、将 1.0.1 修改为 ^1.0.1;然后 npm i 安装全部依赖;依赖并没有更新到最新

上述步骤中,第 2、3 步并不难理解,前面讲过,^x.y.z 只会锁定主版本,安装最新依赖;而具体版本后则按指定更新

但第 4 步,却没有安装预期安装主版本的最新版本,坏了啊,lock 并没有生效~

机制: package.json 与 package-lock.json 两者,如果版本号值相同(1.0.4 与 ^1.0.4 相同,不比较符号),则按 lock 文件为准;如果版本号值不一致,则以版本值大的文件为准(不是以大的版本号)

lock 锁定 1.0.4 版本

  • 1、package.json 版本号 ^1.0.1,以版本值大的文件为准,以 lock 为准,安装 1.0.4
  • 2、package.json 版本号 ^1.0.6,以版本值大的文件为准,以 package.json 为准,^1.0.6 即最新 1.y.z 版本,安装 1.0.8

这是我在任何文档上都没有看到提及的,让我困惑已久~

另外补充一句:依赖更新不会删除本地 node_modules 历史缓存依赖;

如 tian-npm 1.0.2 版本 dependencies 包含 moment.js;而 tiange-npm 1.0.8 版本 dependencies 则使用了 dayjs 替代;

如果你先安装了 1.0.2 后安装了 1.0.8,那么你本地会有 moment.js 和 dayjs 两个依赖;这就将可能导致你依赖安装产生冲突(当然不用担心其他同事,因为他们是通过 package.json 和 package-lock.json 来安装)

2.1.4、伴生依赖问题

什么是伴生依赖?

以我们之前发布的 tiange-npm 包为例,其中依赖了 dayjs;我们在使用项目中安装 tiange-npm;根据 npm 扁平化结构,node_modules 根目录下会安装 tiange-npm 和 dayjs 两个依赖

此时,我们可以直接在项目中使用 dayjs 的功能而不报错,我们把 dayjs 称之为伴生依赖(或幻影依赖);这是很不安全的,一旦我们移除 tiange-npm;那么 dayjs 就无法找到,从而引发报错!

2.1.5、相同依赖下载多次

我们发布两个包,tiange-npm、 和 tiange-npm2,两者均依赖于最新的 dayjs;我们在使用项目中安装 dayjs 1.10.8、tiange-npm、tiange-npm2

此时 tange-npm、tiange-npm2 下的 node_modules 都会下载一份 dayjs 的最新版本依赖;那如果我项目里 100 个依赖都使用 dayjs 同一版本,这将极大的占用我们的磁盘空间

2.2、yarn 优缺点

正是为了解决 npm 的上述问题,Facebook 才在 2016 发布了 yarn,它具有以下优点(npm 后续也借鉴了 yarn 的 lock、缓存)

  • 缓存,yarn 会缓存所有安装的包
  • 并行下载,有效的提升下载速度
  • 锁定依赖版本,package.json 配合 yarn.lock 文件确保不同机器安装相同依赖
  • 扁平化结构(也正因为此,yarn 也存在伴生依赖问题)

发展至今,yarn 比 npm 最大的优势可能就是安装快点,由于同样采用了扁平化,yarn 同样存在 npm 的关键缺点

  • 相同版本依赖下载多次
  • 伴生依赖问题

2.3、yarn 命令

我们可以通过 npm 命令 npm install yarn -g 来安装 yarn;然后就可以在终端使用 yarn CLI 了

yarn 的命令与命令参数基本与 npm 一致,只是对部分命令进行了更改,更偏向口语化

功能 npm 命令 yarn 命令
全局安装指定依赖 npm install nvm -g yarn global add nvm
安装全部依赖 npm install yarn install
安装指定依赖 npm install dayjs yarn add dayjs
安装指定开发依赖 npm install dayjs -D yarn add dayjs -D
删除指定依赖 npm uninstall dayjs yarn remove dayjs
升级指定依赖 npm update dayjs yarn upgrade dayjs
查看包信息 npm view dayjs yarn info dayjs
运行脚本 npm run dev yarn run dev
发布相关 npm login/logout/adduser/publish yarn login/logout/adduser/publish

2.3.1、区别对比

yarn 的命令对比 npm,主要是更加偏向口语化,使用时千万不要弄混了~

1、安装指定依赖命令及参数

less 复制代码
yarn add [package] --dev 或 yarn add [package] -D // 加到 devDependencies
yarn add [package] --peer 或 yarn add [package] -P // 加到 peerDependencies
yarn add [package] --optional 或 yarn add [package] -O // 加到 optionalDependencies

yarn add [package] // 会自动安装最新版本,会覆盖指定版本号
yarn add [package] [package] [package] // 一次性添加多个包
yarn add [package]@[version] // 添加指定版本的包
yarn add [package]@[tag] // 安装某个tag(比如beta,next或者latest)

安装命令的参数与 npm 一致,同样支持小写;只是命令修改为 add 了;PS:npm add 同样是可用命令,同 npm install

2、取消了大部分缩写形式 上述 yarn add 命令参数仍然支持缩写;但像全局 global 参数,yarn install(不能写成 yarn i)等缩写形式均取消

3、命令名变化

  • 安装指定依赖 install 修改为 add
  • 查看包信息 view 修改为 info
  • 升级包 update 修改为 upgrade
  • 删除包 uninstall 修改为 remove

4、安装全部依赖命令及参数

scss 复制代码
yarn 或 yarn install // 安装所有依赖(直接执行 npm 则是显示相关帮助;直接执行 yarn 则是安装全部依赖)
yarn install --flat // 安装一个包的单一版本
yarn install --force // 强制重新下载所有包
yarn install --production // 只安装生产环境依赖,不安装开发依赖(npm 做不到)

2.4、yarn2 简单介绍

上述所有介绍,均为 Classic 版本及 1.y.z 版本;通过 npm install yarn -g 命令安装的形式默认也是安装的 yarn1 最新版;yarn2 对 yarn1 进行了颠覆式的升级。

有以下核心特性,更多内容前往 yarn2 官方文档 查看

  • 零依赖安装:不再存在node_modules文件夹,所有的依赖都会被压缩成很小的文件,跟随 git 上传,使得部署更稳定,再不会存在本地运行没问题,发布线上环境的时候挂掉了的问题
  • yarn dlx: 可以 yarn dlx 在临时环境中执行包,而无需先安装它。

yarn2 彻底颠覆了传统的包管理系统,很好的解决了 npm、yarn1 扁平化结构带来的核心问题;但也正因为过于颠覆性,出于兼容性考虑,大部分项目并没有进行 yarn2 的升级;因此绝大部分项目依然无法解决相同依赖、伴生依赖等问题

3、pnpm

没错,解决 npm、yarn1 扁平化结构核心问题的新一代包管理工具来了

pnpm:Performance Node Package Manager;顾名思义,高性能 npm;具有以下优点

  • 节省磁盘空间:相同版本的包不再重复安装
  • 安装速度快:安装过的依赖会复用缓存,甚至包版本升级带来的变化都只 diff;同时相同版本的包不再重复安装
  • 权限严格:非扁平结构,因此代码无法对任意软件包进行访问
  • pnpm store:内置了仓库,包含多个软件包的支持

无图言屌!我们分别通过 pnpm(需先安装,可以 npm install pnpm -g 全局安装) 和 yarn(或 npm)安装 nodemon 来看一下它们的区别,给你们看一下什么叫 遥遥领先

yarn add nodemon -D node_modules 结构图

pnpm add nodemon -D node_modules 结构图

两者区别一目了然,这就是 pnpm,相比于其他包管理工具

markdown 复制代码
            遥遥领先!
        遥遥领先!遥遥领先!
    遥遥领先!遥遥领先!遥遥领先!
遥遥领先!遥遥领先!遥遥领先!遥遥领先!

node_modules 根目录下只有一个 nodemon 的依赖,且是一个软连接(注意标红处的箭头),可以理解为 Window 的快捷方式;这也是为什么 pnpm 的依赖不再有伴生依赖的问题,因为 node_modules 根目录下已经没有那些伴生依赖

这时,会有同学开始疑惑了,根目录的 node_modules 没有伴生依赖, 那么 node_modules 所需的依赖不就缺失了吗?答案是所有的依赖都保存在 pnpm 目录下

那么这种模式是怎么工作的呢?一切都要从 pnpm 的原理: symlinks 说起

3.1 pnpm 原理

在理解 pnpm 原理之前,先理解几个核心概念:

  • store:pnpm 的依赖仓库,内置了许多软件包的支持(可以理解为类似 npm 包私服,我们安装的包都是从这里下载)
  • 扁平化结构:即 npm、yarn 的 node_modules 依赖存储结构,仍然存在嵌套
  • 平铺式结构:pnpm 的 node_modules 依赖结构,所有依赖平铺在 .pnpm 目录下
  • 软链接:symlinkssymbolic links 的缩写,即象征性链接;symlinks 翻译为符号链接或软链接(可以理解为 Window 的快捷方式),用来指向 pnpm 下的某个依赖
  • 硬链接:hard link,指向 store 中的对应依赖

Symlinked node_modules structure

bash 复制代码
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       ├── bar -> <store>/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo -> <store>/foo
    │       ├── bar -> ../../bar@1.0.0/node_modules/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    └── qar@2.0.0
        └── node_modules
            └── qar -> <store>/qar

上述即 pnpm 的原理图

  • node_modules 根目录下只保留直接依赖的软链接;而将所有依赖放到 pnpm 目录下
  • 直接依赖以软链接的形式链接到 pnpm 下的对应依赖,而这个依赖以硬链接的形式指向 pnpm store
  • 伴生依赖只安装一次,以硬链接的形式指向 pnpm store
  • 当多个依赖用共同版本的伴生依赖时,以软链接的形式链接到安装的伴生依赖

更详细的解读,参考官方文档

3.2 pnpm 命令

基本上保持和 npm 类似,官网有非常详细的说明文档

  • pnpm install 或 pnpm i 安装全部依赖
  • pnpm add xx -g 安装全局依赖
  • pnpm add xx 安装指定依赖到 dependencies
  • pnpm add xx -D 安装指定依赖到 devDependencies
  • pnpm add xx -O 安装指定依赖到 optionDependencies
  • pnpm update xx 更新依赖
  • pnpm remove xx 删除依赖

3.3、npm、yarn 到 pnpm 的丝滑升级

pnpm 以软链接形式的 node_modules 结构(所有依赖平铺在 .pnpm 目录下)解决 npm、yarn 扁平化 node_modules 的重复依赖、伴生依赖的两大核心问题;同时下载速度更快。

为什么推荐 pnpm?诸多优点,无疑很是让人心动,但是,如果升级过于麻烦,那我想仍会劝退绝大部分的开发者

实际上,npm/yarn 到 pnpm 的升级过程非常丝滑

  • 1、删除 node_modules 和 lock 文件
  • 2、执行 pnpm install 安装依赖
  • 3、根据警告安装同伴依赖 peerDependencies; 执行 pnpm add -D packageA packageB 命令一一安装即可
  • 4、执行 pnpm run dev 运行项目

当然,如果你的项目中有使用伴生依赖的场景(即项目 package.json 并没有记录,但实际使用了),就需要根据错误提示一一安装,执行命令 pnpm add xxx

你还在等什么?

4、相关问题整理

TODO: 整理日常开发遇到的问题

跨平台设置脚本参数

windows 不支持 NODE_ENV=development 的脚本参数设置方式,使用 cross-env 则可以跨平台设置

项目安装 npm install cross-env -D

配置脚本时即可使用,如: "dev": "cross-env NODE_ENV=development node index.js"

快速删除 node_modules

有时候安装依赖报错或者需要清除无用依赖(如你在 package.json 去除了一个依赖,重新安装,本地 node_modules 会仍然保存着这个依赖)

全局安装工具 rimraf: npm install rimraf -g

快速删除 rimraf node_modules

删除无用依赖

利用 depcheck 工具

  • 安装,npm install depcheck -g
  • 执行 depcheck 命令,然后删除无用依赖即可

5、结束语

话说,土豆都可以 gap 一年,人为什么不可以呢?

因为,我们是韭菜,嘿嘿!韭菜总是一茬接一茬,不歇的~

有没有被伤到的说?没事的,韭而韭之韭习惯啦~

相关推荐
奔跑吧邓邓子16 分钟前
npm包管理深度探索:从基础到进阶全面教程!
前端·npm·node.js
前端李易安36 分钟前
ajax的原理,使用场景以及如何实现
前端·ajax·okhttp
汪子熙1 小时前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
Envyᥫᩣ1 小时前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
Мартин.5 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。7 小时前
案例-表白墙简单实现
前端·javascript·css
数云界7 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd7 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常7 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer7 小时前
Vite:为什么选 Vite
前端