npm

npm是 2010 年发布的 nodejs 依赖管理工具,在此之前,前端的依赖管理都是手动下载和管理的。

npm1、npm2的问题: 依赖嵌套

  1. node_modules
  2. ├─ foo
  3. | └─ node_modules
  4. | ├─ lodash
  5. | └─ bar
  6. └─ baz
  7. └─ node_modules
  8. └─ lodash
  1. 依赖层级太深,会存在文件路径过长的问题。尤其在windows系统下。
  2. 大量重复的包被安装,文件体积变大。比如foo和同级的baz都依赖相同版本的lodash,就会安装两次。
  3. 模块实例无法共享,不同的包引入的Vue/React不是同一个模块实例,无法共享内部变量。


npm3:依赖扁平化

npm3对上述情况进行了优化,将依赖树扁平化,将所有的依赖都拍平到node_modules目录下,
后来诞生的yarn也采用扁平化的依赖树。

  1. node_modules
  2. ├─ foo
  3. ├─ lodash
  4. ├─ bar
  5. └─ baz

image.png
根据node的require机制,当我们运行require(“xxx”)引入外部模块时

  • 如果xxx是一个node核心模块,例如fs、http等,那么返回node核心模块。
  • 如果不是,那么会判断判断当前node_modules 文件夹是否有此模块,如果有就返回,如果没有就递归往上层的node_modules目录查找,如果找到就返回,如果到根目录都没找到就报错。

这样尽可能把复用的模块提升到最上层,减少了大量的重复安装问题。

扁平化产生的问题

  • 依赖树不稳定
  • 扁平化算法复杂度高,耗时长
  • 幽灵依赖问题:即某个包没有在package.json 被依赖,但是用户却能够引用到这个包。

为什么依赖树不稳定?
image.png
假设foo和bar都依赖了同一个包的不同版本,经过npm/yarn 安装扁平化处理之后,可能产生两种依赖树结构
image.png
image.png
依赖树结构是1还是2取决于foo和bar在package.json中声明的先后顺序,如果foo声明在前,则是依赖树1的结构,如果bar声明在前,则是依赖树2的结构。
这也是lock文件诞生的原因,npm5(2017年发布)和yarn分别出现了package-lock.json和yarn.lock文件,都是为了保证install之后产生一个确定的依赖树。

npm5

由于yarn的诞生,npm在v5是进行了一系列改进优化
主要包括 安装时默认生成lock文件、重写缓存机制、下载依赖时使用强校验算法、registry策略调整等等

npm 安装机制

image.png

  • npm install 执行之后,首先,检查并获取 npm 配置,这里的优先级为:项目级的 .npmrc 文件 > 用户级的 .npmrc 文件> 全局级的 .npmrc 文件 > npm 内置的 .npmrc 文件。
  • 然后检查项目中是否有 package-lock.json 文件。
  • 如果有,则检查 package-lock.json 和 package.json 中声明的依赖是否一致:
    • 一致,直接使用 package-lock.json 中的信息,从缓存或网络资源中加载依赖;
    • 不一致,按照 npm 版本进行处理(不同 npm 版本处理会有不同,具体处理方式如图所示)。
  • 如果没有,则根据 package.json 递归构建依赖树。然后按照构建好的依赖树下载完整的依赖资源,在下载时就会检查是否存在相关资源缓存:
    • 存在,则将缓存内容解压到 node_modules 中;
    • 否则就先从 npm 远程仓库下载包,校验包的完整性,并添加到缓存,同时解压到 node_modules。
  • 最后生成 package-lock.json。

构建依赖树时,当前依赖项目不管其是直接依赖还是子依赖的依赖,都应该按照扁平化原则,优先将其放置在 node_modules 根目录(最新版本 npm 规范)。在这个过程中,遇到相同模块就判断已放置在依赖树中的模块版本是否符合新模块的版本范围,如果符合则跳过;不符合则在当前模块的 node_modules 下放置该模块(最新版本 npm 规范)。

yarn

yarn 是Facebook于2016年发布的替代npm的包管理工具,还可以作为项目管理工具,定位是快速、可靠、安全的依赖管理工具。

yarn的优势

  • 安装速度快
    • npm安装依赖是按包的顺序依次安装,yarn安装利用并行下载以最大化资源利用率。
    • 采用缓存机制,缓存每个下载过的包,再次安装时从缓存读取,可以实现离线安装,后来npm也实现了缓存机制(npm config get cache查看)。
  • 扁平性与确定性
    • yarn和npm v3使用一样的扁平化结构来管理依赖,不论安装顺序如何,通过yarn.lock文件确保相同的依赖在任何及其都以相同的方式安装。npm v5开始也像yarn一样,安装时默认生成一个package-lock.json,不同的是yarn.lock是yaml格式,package-lock.json是json格式,除此之外,yarn.lock中子依赖是不锁定版本的,所以yarn是通过lock文件和package.json结合一起决定依赖树的版本的。
  • 安全
    • lock文件每个依赖的tar包都有一个hash值可以用来校验包的完整性。
  • 重试机制
    • 重试机制确保单个请求失败并不会导致整个安装失败。


yarn的安装机制

image.png检测包
目的主要是检测项目中是否存在npm的package-lock.json文件,如果有提示用户注意:这些文件可能会存在冲突。在这一步骤中 也会检测系统OS, CPU等信息。

解析包
这一步会解析依赖树中的每一个包的信息:

  • 先获取到首层依赖:也就是我们当前所处的项目中的package.json定义的dependencies

devDependenciesoptionalDependencies的内容。

  • 紧接着会采用遍历首层依赖的方式来获取包的依赖信息,以及递归查找每个依赖下嵌套依赖的版本信息,并将解析过的包和正在进行解析包用Set数据结构进行存储,这样就可以保证同一版本范围内的包不会进行重复的解析。

image.png

获取包
首先会检查缓存中是否有当前依赖的包,同时将缓存中不存在的包下载到缓存的目录中
如何去判断缓存中有当前的依赖包呢?

其实在Yarn中会根据 cacheFolder+slug+node_modules+pkg.name 生成一个path路径,判断系统中是否存在该如果存在证明已经有缓存,不用重新下载。这个path也就是依赖包缓存的具体路径。

那么对于没有命中的缓存包呢?

在 Yarn 中存在一个Fetch队列,按照具体的规则进行网络请求。如果下载的包是一个file协议,或者是相对路径,就说明指向一个本地目录,此时会调用Fetch From Local从离线缓存中获取包,否则调用 Fetch From External 获取包,最终获取的结果使用 fs.createWriteStream 写入到缓存目录。

image.png

链接包
解析peerDepdencies,遵循扁平化规则,将依赖包拷贝到node_modules
image.png

构建包
如果依赖包中存在二进制包需要进行编译,会在这一步进行。

yarn的workspace

Yarn Workspaces(工作区)是Yarn提供的monorepo的依赖管理机制,从Yarn 1.0开始默认支持,用于在代码仓库的根目录下管理多个package的依赖。
workspace优势

  • 开发多个互相依赖的package时,workspace会自动对package的引用设置软链接(symlink),比yarn link更加方便,且链接仅局限在当前workspace中,不会对整个系统造成影响
  • 所有package的依赖会安装在最根目录的node_modules下,节省磁盘空间,且给了yarn更大的依赖优化空间
  • 所有package使用同一个yarn.lock,更少造成冲突且易于审查

如何使用workspace
根目录package.json设置:

  1. {
  2. "name": "my-app",
  3. "version": "1.0.0",
  4. "private": true,
  5. "workspaces": [
  6. "packages/*"
  7. ],
  8. }

workspaces: 声明workspace中package的路径。值是一个字符串数组,支持通配符。
其中"packages/*"是社区的常见写法,也可以枚举所有package:
"workspaces": ["package-a", "package-b"]

常用命令
假设项目中有foo 和 bar两个package

  1. my-app
  2. ├─ package.json
  3. └─ packages
  4. ├─ foo
  5. | ├─ package.json
  6. └─ bar
  7. ├─ package.json

**yarn workspace <workspace_name> <command>**
在指定的package中运行指定的命令。

  1. # 在foo中添加typescript 作为devDependencies
  2. yarn workspace foo add typescript --dev
  3. # 移除bar中的lodash依赖
  4. yarn workspace bar remove lodash
  5. # 运行bar中package.json的 test 命令
  6. yarn workspace bar run test

**yarn workspaces run <command>**
在所有package中运行指定的命令,若某个package中没有对应的命令则会报错。

  1. # 运行所有package(foo、bar)中package.json的 build 命令
  2. yarn workspaces run build

**yarn <add|remove> <package> -W**

  • -W: —ignore-workspace-root-check ,允许依赖被安装在workspace的根目录

管理根目录的依赖。

  1. # 安装eslint作为根目录的devDependencies
  2. yarn add eslint -D -W

pnpm

pnpm(performant npm:高性能npm),设计初衷是为了解决包安装速度问题以及磁盘空间利用问题。

安装pnpm

npm install -g pnpm

pnpm优势

  1. 安装速度快

以这个仓库为例,对比在npm、pnpm、yarn(正常版本和PnP版)中,install、update场景下的耗时:
alotta-files.svg

  1. 高效利用磁盘空间

当使用 npm 或 Yarn 时,如果你有 100 个项目使用了某个依赖,就会有 100 份该依赖的副本保存在硬盘上。 而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:

  1. 如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到store。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么 pnpm update 时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。
  2. pnpm 会在全局的 store 目录里存储项目 node_modules 文件的 hard links,hard link 使得用户可以通过不同的路径引用方式去找到某个文件,在引用依赖的时候则是通过symlink 去找到对应 .pnpm虚拟磁盘目录下的依赖地址。
    1. 支持monorepo

pnpm与npm/yarn 另一个不同的点就是pnpm支持monorepo,体现在各个子命令的功能上,比如在根目录下
pnpm add A -r, 那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter字段来对 package 进行过滤。

  1. 安全,不存在非法访问依赖的问题
    1. pnpm 默认创建了一个非平铺的 node_modules,被打平的依赖会被放到 .pnpm 这个虚拟磁盘目录下面去,通过代码 require 是访问不到的。

      https://pnpm.io/zh/symlinked-node-modules-structure https://pnpm.io/zh/blog/2020/05/27/flat-node-modules-is-not-the-only-way

monorepo:之前对于多个项目的管理,我们一般都是使用多个git仓库,但monorepo的宗旨是利用一个git仓库来管理多个子项目,所有的子项目都放在根目录的packages文件夹下,一个子项目就代表一个package。 优点:可以在一个仓库里维护多个package,可统一构建,跨package调试、依赖管理、版本发布都十分方便,搭配工具还能统一生成CHANGELOG; 缺点:代码仓库体积会变大,只开发其中一个package也需要安装整个项目的依赖。

其他包管理工具迁移到pnpm

输入pnpm import 命令从另一个软件包管理器的 lock 文件生成 pnpm-lock.yaml。 支持的源文件:

  • package-lock.json
  • npm-shrinkwrap.json
  • yarn.lock

请注意,如果您有要为其导入依赖项的工作区,那么在导入之前,您需要在 pnpm-workspace.yaml 文件中声明它们。

pnpm常用命令

pnpm install
pnpm update
pnpm uninstall
pnpm link

补充知识点

npm语义版本号

语义版本,就是指版本号为 a.b.c 的形式,其中 a 是大版本号, b 是小版本号, c 是补丁号。

  • 无符号:比如”axios”: “0.21.0”,表示必须安装0.21.0的版本;
  • ~符号:比如 “core-js”: “~3.6.5”, 表示安装3.6.x的最新版本(不低于3.6.5),但是不安装3.7.x,也就是说安装时不改变大版本号和小版本号
  • ^符号:比如 “antd”: “^3.1.4”,表示安装3.1.4及以上的版本,但是不安装4.0.0,也就是说安装时不改变大版本号。

    nrm

    nrm(npm registry manager )是npm的镜像源管理工具,有时候国外资源太慢,使用这个就可以快速地在 npm 源间切换

  1. $ npm install -g nrm // 全局安装nrm
  2. $ nrm ls // 查看可选的源
  3. $ nrm use xxx // 切换源 如nrm use taobao
  4. $ nrm add xxx // 添加源 如 nrm add zuoyebang http://ued.zuoyebang.cc/npm/
  5. $ nrm del xxx // 删除源
  6. $ nrm test // 测试速度

nvm

nvm是MacOS的node版本管理工具,如果需要管理 Windows 下的 node,官方推荐使用 nvmwnvm-windows

安装多版本node

  1. nvm install 14.19 // nvm 会寻找 14.19.x 中最高的版本来安装。
  2. # MacOS列出远程服务器上所有可用的版本
  3. nvm ls-remote
  4. # windows列出远程服务器上所有可用的版本
  5. nvm ls available
  6. # 列出已安装的版本
  7. nvm ls
  8. # 切换版本
  9. nvm use 12
  10. nvm use 14.19
  11. # 安装最新版node
  12. nvm install node
  13. # 安装最新不稳定版本的 Node
  14. nvm install unstable

npx

npx 由 npm v5.2 版本引入,是一个 npm 包执行器。我们可以使用 npx 来执行各种命令。
安装npm install npx -g

npx 执行模块时会优先安装依赖,但是在安装执行后便删除此依赖,这就避免了全局安装模块带来的问题。

和npm区别

  • npx侧重于执行命令,执行某个模块命令。虽然会自动安装模块,但是重在执行某个命令。
  • npm侧重于安装或者卸载某个模块。重在安装,并不具备执行某个模块的功能。

使用场景
一次性执行命令

  1. # 比如以下命令,npx 将 create-electron-app 下载到一个临时目录,使用以后再删除
  2. # 不用全局安装 create-electron-app ,运行后不会污染全局环境
  3. $ npx create-electron-app my-new-app

调用项目安装的模块

  1. # 比如,使用npm在项目内部安装了测试工具 Mocha
  2. $ npm install -d mocha
  3. # 想要调用它,只能在项目脚本和 package.json的scripts字段里面,如果想在命令行下调用
  4. # 需要在项目的根目录下执行
  5. $ node-modules/.bin/mocha --version
  6. # 用npx
  7. npx mocha --version

这是因为npx可以直接执行 node_modules/.bin 文件夹下的文件。在运行命令时,npx 可以自动去 node_modules/.bin 路径和环境变量 $PATH 里面检查命令是否存在,而不需要再在 package.json 中定义相关的 script。

强制使用某个包管理器

  1. {
  2. "scripts": {
  3. "preinstall": "npx only-allow pnpm"
  4. }
  5. }

此时如果用其他的包管理器安装包会报错,并且安装失败
image.png

参考:
https://yarn.bootcss.com/
https://juejin.cn/post/6932046455733485575
https://juejin.cn/post/7060844948316225572
https://juejin.cn/post/7001794162970361892