管理器

  • 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

1
npm i -g pnpm

详解

安装时

执行命令后,首先会构建依赖树,然后针对每个节点下的包,会经历下面四个步骤:

  1. 将依赖包的版本区间解析为某个具体的版本号
  2. 下载对应版本依赖的 tar 包到本地离线镜像(能够在无网环境下安装,npm 5+ 才抄袭过来)
  3. 将依赖从离线镜像解压到本地缓存
  4. 将依赖从缓存拷贝到当前目录的 node_modules 目录

然后,对应的包就会到达项目的node_modules当中。

速度

很明显 npm 是最慢的。
为什么慢,因为 node_modules,文件小而多,磁盘 I/O 的特别慢,而且重复下载的文件也会在有多。

包管理方式

依赖版本

  • npm包管理工具都是通过 package.json 中对各个依赖包的描述去下载对应的依赖包的。
  • 但 package.json 只能规定大版本号。这样就会导致每个时期下载的依赖包都是不一样的,很容易出现兼容性等各种问题。例如:
    1
    2
    3
    "dependencies": {
    "vue": "^3.2.6"
    }
  • 字符 ^ 告诉 NPM 检查在 3.X.X 范围内是否有较新版本,如果有,则进行安装。类似地,~ 字符只会出现在热修复程序或 3.2.X 上。
  • 这样导致项目每次安装的时候版本不一致,可能引起一些相关错误。
  • 而 yarn 率先发明了 lockfiles(已被 npm 5+ 抄袭)。
  • 规定了具体每个依赖包的版本号和对应的下载路径,保证我们下次在重新安装依赖时,能跟上次一模一样。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 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+ 抄袭)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # ① 假设项目依赖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 是一门运行时的语言有关)。
  • 接下来在看两种情况:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # ② 现在假设在①的基础上,根项目依赖了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出现了重复
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
      .
    └── 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 机制的以下优点:
    1. 摆脱了 node_modules:
      • 时间上: 相比较在热缓存(hot cache)环境下运行yarn install节省 70%的时间。
      • 空间上: pnp 模式下,所有 npm 模块都会存放在全局的缓存目录下,依赖树扁平化,避免拷贝和重复。
    2. 提高模块加载效率,Node 为了查找模块,需要调用大量的 stat 和 readdir 系统调用。. pnp 通过 Yarn 获取或者模块信息,直接定位模块。
    3. 不再受限于 node_modules 同名模块不同版本不能在同一目录。
  • 使用 pnp 机制的以下缺点
    1. 因为 node 依赖解析的目录 node_modules 没了,不能直接使用 node xxx.js。
    2. 由于还是不够成熟(2018.9面世),前端社区其他工具链支持度还不够,从官方看已有下列的工具有条件支持(某版本起或插件支持)
  • 但 pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,他只会安装一次,磁盘中只有一个地方写入(rails 也类似这样),后面再次使用都会直接使用 hardlink(硬链接),这种方法几乎就解决了上面的所有问题。
  • 例如我们安装一个依赖:pnpm init -y && pnmp i react
1
2
3
4
5
6
7
.
└── node_modules
│ ├── .pnpm
│ ├── react
│ ├── .modules.yaml
└── package.json
└── pnpm-lock.yaml # lockfiles
  • 如此整洁、干净,我们直接就看到了 react,但值得注意的是,这里仅仅只是一个软链接,那么它真正的位置在哪呢?
  • .pnpm 当中寻找:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .
    └── 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 文件不同)。