管理器
- npm,太慢了,总会有莫名的bug,需要删除 node_modules,在次安装以后才能解决。
- cnpm,已经淘汰。
- yarn / yarnPnP,比 npm 快很多,比 npm 做了很多的优化,但是还存有和 npm 一样的其他问题。
- pnpm,更快,且解决了 npm/yarn 内部潜在的 bug,并且极大了地优化了性能,扩展了使用场景。
npm
node 自带
为什么 node 要选择 npm?
在远古时代,前端是通过网址来共享代码,比如你想使用 jQuery,那么你点击 jQuery 网站上提供的链接就可以下载 jQuery,放到自己的网站上使用。
但是当项目的依赖越来越多的时候,这是一件很麻烦的事情,去 jQuery 官网下载 jQuery,去 BootStrap 官网下载 BootStrap...等等。
程序员 Isaac Z. Schlueter 给出一个解决方案:用一个工具把这些代码集中到一起来管理吧!毕竟懒才是程序员的第一生产力。
NPM 的思路大概是这样的:
- 买个服务器作为代码仓库(registry),在里面放所有需要被共享的代码
- 发邮件通知 jQuery、Bootstrap、Underscore 作者使用 npm publish 把代码提交到 registry 上,分别取名 jquery、bootstrap 和 underscore(注意大小写)
- 社区里的其他人如果想使用这些代码,就把 jquery、bootstrap 和 underscore 写到 package.json 里,然后运行 npm install ,npm 就会帮他们下载代码
- 下载完的代码出现在 node_modules 目录里,可以随意使用了。
发展
- Isaaz 通知 jQuery 作者 John Resig,他会答应吗?这事儿不一定啊,对不对。
- 只有社区里的人都觉得 「npm 是个宝」的时候,John Resig 才会考虑使用 npm。
- 那么 npm 是怎么火的呢?
- npm 的发展是跟 Node.js 的发展相辅相成的。
- Node.js 是由一个在德国工作的美国程序员 Ryan Dahl 写的。他写了 Node.js,但是 Node.js 缺少一个包管理器,于是他和 npm 的作者一拍即合、抱团取暖,最终 Node.js 内置了 npm(现在来看为什么 npm 这么烂,node 还要选择它,当时的 node 觉得自己 i/o 很快,且当时的程序还没有这么复杂)。后来的事情大家都知道,Node.js 火了。
- 所以说一门技术想要流行就得攀附、组合,NPM 全称 node package manager。(类似 LAMP,之前在编程历史中讲过)
yarn
安装 yarn
首先不推荐使用 npm 安装
为什么?
- Yarn 团队认为 npm 不安全且不可靠,根据Yarn项目维护者的说法,通过npm安装Yarn违反了项目目标,可能会引起问题,并且通常比特定于平台的安装方法更糟糕。
- 一般不推荐通过 npm 安装 Yarn。使用 npm 安装 Yarn 是不确定的,包没有签名,唯一执行的完整性检查是基本的 SHA1 哈希,这在安装系统范围的应用程序时是一个安全风险。
- 通过 npm 运行 Yarn,它是一个单独的包管理器实用程序,可能会导致边缘问题(请参阅issue 2072)
- 通过系统包管理器安装将 Yarn 与 npm 分离,允许您在没有 npm 的情况下运行 Yarn
- 系统包管理器通常会定期运行,保持 Yarn 更新
- 通过 npm 安装 Yarn 很慢
但是从 Yarn 2.x 开始,Yarn 团队改变了他们的建议,现在建议通过 npm 安装该工具。此建议围绕锁定每个项目使用的 Yarn 版本的优势。这使项目能够适应不同版本的 Yarn 的变化。
pnpm(performant npm)
安装 pnpm
npm i -g pnpm
详解
安装时
执行命令后,首先会构建依赖树,然后针对每个节点下的包,会经历下面四个步骤:
- 将依赖包的版本区间解析为某个具体的版本号
- 下载对应版本依赖的 tar 包到本地离线镜像(能够在无网环境下安装,npm 5+ 才抄袭过来)
- 将依赖从离线镜像解压到本地缓存
- 将依赖从缓存拷贝到当前目录的 node_modules 目录
然后,对应的包就会到达项目的node_modules当中。
速度
很明显 npm 是最慢的。
为什么慢,因为 node_modules,文件小而多,磁盘 I/O 的特别慢,而且重复下载的文件也会在有多。
包管理方式
依赖版本
- npm包管理工具都是通过 package.json 中对各个依赖包的描述去下载对应的依赖包的。
- 但 package.json 只能规定大版本号。这样就会导致每个时期下载的依赖包都是不一样的,很容易出现兼容性等各种问题。例如:
"dependencies": {
"vue": "^3.2.6"
}
- 字符
^
告诉 NPM 检查在3.X.X
范围内是否有较新版本,如果有,则进行安装。类似地,~
字符只会出现在热修复程序或3.2.X
上。 - 这样导致项目每次安装的时候版本不一致,可能引起一些相关错误。
- 而 yarn 率先发明了 lockfiles(已被 npm 5+ 抄袭)。
- 规定了具体每个依赖包的版本号和对应的下载路径,保证我们下次在重新安装依赖时,能跟上次一模一样。
// package-lock.json,npm 是 json 文件,方便看得懂
"vue": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.6.tgz", //下载路径
"integrity": "sha512-Zlb3LMemQS3Xxa6xPsecu45bNjr1hxO8Bh5FUmE0Dr6Ot0znZBKiM47rK6O7FTcakxOnvVN+NTXWJF6u8ajpCQ==",
"requires": {
"@vue/compiler-dom": "3.2.6",
"@vue/runtime-dom": "3.2.6",
"@vue/shared": "3.2.6"
}
}
目录结构
- 如果一个项目有 100 个依赖,并且这些依赖的依赖都有 lodash。
- 在 npm 中 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。所以这能不能优化呢?
- 而在 yarn 中会实行的是扁平结构(已被 npm 3+ 抄袭)。
# ① 假设项目依赖a,b,c三个模块,依赖树为:
# +- a
# +- react@15
# +- b
# +- react@16
# +- c
# +- react@16
# yarn安装时会按照项目被依赖的次数作为权重,将依赖提升(hoisting),
# 安装后的node_modules结构为:
.
└── node_modules
├── a
│ ├── index.js
│ ├── node_modules
│ │ └── react # @15
│ └── package.json
├── b
│ ├── index.js
│ └── package.json
├── c
│ ├── index.js
│ └── package.json
└── react # @16 被依赖了两次,所以进行提升
- 这样一来,重复的包将会大量减少,但是由于“提升”,当你只安装一个依赖的时候,会发现 node_modules 下多了很多的你没有安装的目录(因为重复的被提升了),node_modules 目录将会变得很丑。
- Q:为什么不一起把 react15 也一起提升了?
- A:因为 node_modules 不能有效地处理重复的包. 两个名称相同但是不同版本的包是不能在一个目录下共存的。
-而且这里还有一个潜在的问题,即:如果 A 依赖 B, B 依赖 C,由于提升了那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖,因此会出现这种非法访问的情况(其中也有因为Node没有包的概念的关系,我猜测可能和 js 是一门运行时的语言有关)。 - 接下来在看两种情况:
# ② 现在假设在①的基础上,根项目依赖了react@15,对于项目自己的依赖肯定是要放在node_modules根目录的,
# 由于一个目录下不能存在同名目录,所以react@16没有的提升机会.
# 安装后node_moduels结构为
.
└── node_modules
├── a
│ ├── index.js
│ └── package.json # react@15 提升
├── b
│ ├── index.js
│ ├── node_modules
│ │ └── react # @16
│ └── package.json
├── c
│ ├── index.js
│ ├── node_modules
│ │ └── react # @16
│ └── package.json
└── react # @15
# 上面的结果可以看出,react@16出现了重复
.
└── node_modules
├── a
│ ├── index.js
│ ├── node_modules
│ │ └── react # @16
│ └── package.json
├── b
│ ├── index.js
│ ├── node_modules
│ │ └── react # @15
│ └── package.json
└── react # @15 or 16?
# 答案是: 都有可能。取决于 a 和 b 在 package.json中的位置,如果 a 声明在前面,那么就是提升的就是 react@16,否则是react@15。
- 所以这种扁平化结构存在的缺点有:一、依赖结构的不确定性。二:扁平化算法本身的复杂性很高,耗时较长。三:项目中仍然可以非法访问没有声明过依赖的包。四:在某些情况下没有很好的解决重复问题。
- 因此 yarn 又做出了改进,加入的 PnP(Plug'n'Play) 功能,1.12 版本开始默认包含,2.0 版本开始默认开启。
- 基本原理:Yarn 作为一个包管理器,它知道你的项目的依赖树. 那能不能让 Yarn 告诉 Node? 让它直接到某个目录去加载模块。这样即可以提高 Node 模块的查找效率,也可以减少 node_modules 文件的拷贝。
- 在 pnp 模式下,Yarn 不会创建 node_modules 目录,取而代之的是 .yarn 目录和 .pnp.js文件。
-
.pnp.js
文件,这个文件包含了项目的依赖树信息,模块查找算法,也包含了模块查找器的 patch 代码(在 Node 环境,覆盖 Module._load 方法),简单来说就是项目的npm模块解析规则。 - .yarn 目录存放了项目中下载的所有依赖的zip包。
- 使用 pnp 机制的以下优点:
- 摆脱了 node_modules:
- 时间上: 相比较在热缓存(hot cache)环境下运行yarn install节省 70%的时间。
- 空间上: pnp 模式下,所有 npm 模块都会存放在全局的缓存目录下,依赖树扁平化,避免拷贝和重复。
- 提高模块加载效率,Node 为了查找模块,需要调用大量的 stat 和 readdir 系统调用。. pnp 通过 Yarn 获取或者模块信息,直接定位模块。
- 不再受限于 node_modules 同名模块不同版本不能在同一目录。
- 摆脱了 node_modules:
- 使用 pnp 机制的以下缺点
- 因为 node 依赖解析的目录 node_modules 没了,不能直接使用 node xxx.js。
- 由于还是不够成熟(2018.9面世),前端社区其他工具链支持度还不够,从官方看已有下列的工具有条件支持(某版本起或插件支持)。
- 但 pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,他只会安装一次,磁盘中只有一个地方写入(rails 也类似这样),后面再次使用都会直接使用 hardlink(硬链接),这种方法几乎就解决了上面的所有问题。
- 例如我们安装一个依赖:
pnpm init -y
&&pnmp i react
.
└── node_modules
│ ├── .pnpm
│ ├── react
│ ├── .modules.yaml
└── package.json
└── pnpm-lock.yaml # lockfiles
- 如此整洁、干净,我们直接就看到了 react,但值得注意的是,这里仅仅只是一个软链接,那么它真正的位置在哪呢?
- .pnpm 当中寻找:
.
└── node_modules
├── .pnpm
├ node_modules
├ registry.npmjs.org+@js+tokens@4.0.0
├ .......
├ registry.npmjs.org+react@17.0.2
├ node_modules
├ loose-envify (软链接)
├ ... (软链接)
├ react
├ cjs
├ umd
├ ......
- 好家伙!竟然在
.pnpm/egistry.npmjs.org+react@17.0.2/node_modules/react
下面找到了!同级其他的依赖也都是软链接。 - 再看看.pnpm,.pnpm目录下虽然呈现的是扁平的目录结构,但仔细想想,顺着软链接慢慢展开,其实就是嵌套的结构!
- 将包本身和依赖放在同一个node_module下面,与原生 Node 完全兼容,又能将 package 与相关的依赖很好地组织到一起,设计十分精妙,也解决了 yarn PnP 没有 node_modules 的问题。
- 这么好的东西为什么没有人用呢?
- 兼容问题,像 hard link 和 symlink 这种方式在所有的系统上都是兼容的吗?实际上 hard link 在主流系统上(Unix/Win)使用都是没有问题的,但是 symlink 即软连接的方式可能会在 windows 存在一些兼容的问题,但是针对这个问题,pnpm 也提供了对应的解决方案:在 win 系统上使用一个叫做 junctions 的特性来替代软连接,这个方案在 win 上的兼容性要好于 symlink。
- 或许你也会好奇为啥 pnpm 要使用 hard links 而不是全都用 symlink 来去实现。
- 实际上存在 store 目录里面的依赖也是可以通过软连接去找到的,nodejs 本身有提供一个叫做 --preserve-symlinks 的参数来支持 symlink,但实际上这个参数实际上对于 symlink 的支持并不好导致作者放弃了该方案从而采用 hard links 的方式。具体可以参考该 issue。
monorepo
- 只有 pnpm 支持。
- npm(npm 7+ 抄袭)/yarn workspace
- 随着前端工程的日益复杂,越来越多的项目开始使用 monorepo。之前对于多个项目的管理,我们一般都是使用多个 git 仓库,但 monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的packages目录下,那么一个子项目就代表一个package。如果你之前没接触过 monorepo 的概念,建议仔细看看这篇文章(https://www.perforce.com/blog/vcs/what-monorepo)以及开源的 monorepo 管理工具
lerna
(https://github.com/lerna/lerna#readme),项目目录结构可以参考一下babel 仓库
(https://github.com/babel/babel)。 - pnpm 体现在各个子命令的功能上,比如在根目录下
pnpm add A -r
,那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter 字段来对 package 进行过滤。
目前我们为什么使用 yarn
- 因为 npm 太慢了。
- 从目前主流的开源项目来看,几乎没有使用 npm 的仓库,一般都是 yarn。
- 团队内需要统一,如果不一致可能会出现依赖问题(npm 和 yarn 的 lock 文件不同)。
网友评论