NPM/Yarn包管理策略与最佳实践

July 25, 2021

NPM

NPM 安装机制

执行 npm install :

  1. 读取 npm 配置文件 .npmrc

    配置文件优先级: 项目级 > 用户级 > 全局 > npm 内置

  2. 检查是否存在 package-lock.json 文件:

    • 若不存在, 则根据 package.json 递归构建依赖树并从缓存或远程仓库下载相关资源, 并生成 package-lock.json 文件;
    • 若存在, 则对比 package-lock.jsonpackage.json 文件中声明的依赖规范是否一致:
    • 若一致, 直接使用 package-lock.json 中的信息从缓存或远程仓库加载依赖文件;
    • 若不一致, NPM v5.4.2+ 会按照 package.json 文件安装依赖并更新 package-lock.json 文件(NPM 旧版本的处理方式有所不同, 详见下文).
  3. 构建扁平化依赖树: 无论是直接依赖还是嵌套依赖, 都优先将依赖包放在 node_modules 根目录.

    当遇到相同模块且版本冲突时, 将后者放在当前模块的 node_modules 目录下.

NPM 不同版本对于 package-lock.json 文件处理方式的不同, 可能导致安装的依赖包版本不一致, 所以团队内应保持 NPM 版本一致.

NPM 缓存机制

获取 NPM 本地缓存路径: npm config get cache , 默认为 ~/.npm .

缓存路径下的 _cacache 目录下有三个子目录:

  • content-v2 二进制文件, 将此目录扩展名改为 .tgz 并解压则可以得到依赖包资源
  • index-v5 同样可通过修改扩展名解压, 是 content-v2 资源的索引文件
  • tmp 临时文件

NPM 缓存策略: 执行 npm install 时, 若缓存中存在依赖包, 则通过 pacote 将依赖包解压到对应的 node_modules 目录下; 若缓存中不存在, 则先从远程仓库下载依赖包到缓存中, 再从缓存解压到项目 node_modules 中.

在安装资源时, 根据 package-lock.json 中依赖包的 integrity nameversion 信息生成一个唯一的 key, 根据这个 keyindex-v5 目录中寻找缓存资源的 hash , 如果找到了就可以从 content-v2 中找到对应的 tar 包, 通过 pacote 将二进制文件解压到项目的 node_modules 中.

NPM v5 之前的缓存文件是以 {cache}/{name}/{version} 的形式直接存储在 .npm 目录下.

npm link 为目标模块创建软连接, 将其链接到全局安装路径中, 可用于在模块发布之前在本地调试和使用.

npx 的两个作用:

  • 可直接执行 node_modules/.bin 和 环境变量 PATH 中的命令, 在之前只能通过在 package.json 中定义 script 或手动定位到命令所在目录来执行;
  • 执行一个需要安装依赖的命令, 在临时目录中安装依赖, 并在执行完成后删除相关依赖, 避免在全局安装模块. 可用于使用脚手架生成项目: npx create-react-app my-app .

package-lock.json 中的依赖

package-lock.json 文件中的 dependency 主要有以下属性:

  • Version:版本号
  • Resolved:安装源(下载地址)
  • Integrity:表明包完整性的 Hash 值
  • Dev:该依赖是否为顶级模块的开发依赖或者是传递依赖
  • requires:需要的所有依赖项, 对应该依赖包 package.jsondependencies 中的依赖项
  • dependencies:依赖包 node_modules 中依赖的包, 当子依赖的依赖与当前根目录中的依赖冲突后才有此属性

lockfiles 与 NPM 版本问题总结

  • 早期 NPM 使用 npm-shrinkwrap.json 锁定版本, 与 package-lock.json 不同点在于:NPM 包发布时默认将 npm-shrinkwrap.json 同时发布;
  • package-lock.json 是 NPM v5.x 版本新增特性, 而 v5.6 以上才逐步稳定, 在 5.0 - 5.6 之间对 package-lock.json 的处理逻辑进行过几次更新:

    • 在 v5.0.x 中, npm install 时根据 package-lock.json 文件下载, 不考虑 package.json 内容;
    • v5.1.0 - v5.4.2, npm install 会无视 package-lock.json 文件而下载最新的依赖包并更新 package-lock.json ;
    • v5.4.2 后:
    • 如果项目只有 package.json 文件, npm install 后会根据它生成一个 package-lock.json 文件;
    • 如果存在 package.jsonpackage-lock.json 文件,同时 package.jsonsemver-range 版本 和 package-lock.json 中版本兼容, npm install 会根据 package-lock.json 下载;
    • 如果存在 package.jsonpackage-lock.jsonpackage.jsonsemver-range 版本和 package-lock.json 中版本不兼容, npm installpackage-lock.json 将会更新到兼容 package.json 的版本;
    • 如果 package-lock.jsonnpm-shrinkwrap.json 都存在于项目根目录, package-lock.json 将会被忽略.

xxxDependencies 声明总结

NPM 中共有 5 中依赖声明:

  • dependencies 项目依赖, NPM 包被下载时, 它的项目依赖会被一起下载.
  • devDependencies 开发依赖, 只在开发阶段或开发环境用到的依赖. 在实际业务中只是一个规范, 依赖是否被打包完全取决于项目中是否引入了该模块, 开发依赖在 npm install 时也会被下载.
  • peerDependencies 同版本依赖, 一般用于在基于某个框架或核心库做扩展库或中间件时, 来声明宿主环境, 如开发基于 React 的 UI 组件库时就可以声明 "peerDependencies": { "react": "^17.0.0" } .
  • bundledDependencies 捆绑依赖, 在 npm pack 时会在压缩包中包含捆绑依赖中声明的安装包; 业务方使用 npm install xx 安装压缩包时也会安装捆绑依赖中声明的包. 需要注意的是, 此包必须在 dependenciesdevDependencies 中声明过.
  • optionalDependencies 可选依赖, 表示安装失败也不影响整个过程, 一般不建议使用.

CI 环境中的 NPM 优化

使用 npm ci 代替 npm install

npm ci 是专用于 CI 环境的安装命令, 它与 npm install 的主要不同有:

  • 项目中必须存在 package-lock.jsonnpm-shrinkwrap.json ;
  • npm ci 完全根据 package-lock.json 安装依赖, 保证整个团队的依赖包完全一致, 同时安装过程也更迅速;
  • npm ci 执行安装时会删除现有的 node_modules 并重新安装;
  • npm ci 只能一次安装整个依赖包, 无法单独安装;
  • package-lock.jsonpackage.json 冲突会直接报错;
  • npm ci 不会修改 package.jsonpackage-lock.json .

基于 npm ci 命令的特性, 可得出以下 CI 环境中的 NPM 优化方法:

  • 提交 package-lock.json 到仓库;
  • 缓存 node_modules 文件;

适用于团队的 NPM 最佳实践

  • 使用 NPM v5.4.2 以上的版本, 最好统一 Node.js 版本.
  • 项目初次搭建时使用 npm install 安装依赖, 并提交 package.json、package-lock.json, 不提交 node_modules 目录.
  • 其他成员首次 clone/checkout 项目后, 执行 npm install 安装依赖包.
  • 升级依赖包:

    • 使用 npm update 升级到新的小版本;
    • 使用 npm install @ 升级大版本;
    • 也可以手动修改 package.json 中的版本号并执行 npm install 升级版本;
    • 本地验证升级后无问题再提交新的 package.json、package-lock.json 文件.
  • 降级依赖包:执行 npm install @ 命令,验证没问题后提交新的 package.json、package-lock.json 文件.
  • 删除依赖包:

    • 执行 npm uninstall xx 命令, 验证没问题后提交新的 package.json、package-lock.json 文件;
    • 或手动更改 package.json 删除依赖并执行 npm install 命令, 验证没问题后提交新的 package.json、package-lock.json 文件.
  • 任何成员更新 package.json、package-lock.json 后, 其他成员在拉取代码后执行 npm install 更新依赖.
  • 禁止修改 package-lock.json.
  • 若 package-lock.json 出现冲突或问题,建议删除本地 package-lock.json 文件, 引入远程的 package-lock.json 和 package.json 文件, 再执行 npm install .

Yarn

Yarn 是在 NPM 处于 v3 时为了解决 NPM 在依赖包的版本确定性(无 lock 文件)、扁平化安装、网络性能、缓存机制等方面的问题而出现的包管理器.

目前 NPM 也吸收了 Yarn 的很多优势特点, 改进了本身存在的诸多问题, 所以二者目前并没有明显的优劣之分.

yarn.lockpackage-lock.json 相比, 除了文件格式不同, 另一个显著区别是其中的 子依赖 的版本号不是固定版本, 而是类似于 ^4.0.1 这样的版本规则, 这意味着 yarn.lock 必须和 package.json 文件相配合才能确定 node_modules 目录结构.

Yarn 和 NPM 的另一个显著区别是: Yarn 默认使用 prefer-online 模式, 即优先使用网络资源, 请求失败后才去读取缓存资源.

Yarn 安装机制

Yarn 的安装过程可分为 5 个步骤:

  1. Checking

    检查项目中是否存在 npm 相关文件如 package-lock.json 等, 若有会提示用户可能导致冲突; 也会检查当前环境的操作系统、CPU 等信息.

  2. Resolving

    解析依赖树:

    1. 获取首层依赖, 即 package.json 中的 dependencies devDependenciesoptionalDependencies 内容
    2. 遍历首层依赖, 逐个递归查找嵌套依赖: 对于每个包, 尝试从 yarn.lock 中获取版本信息, 若获取不到, 则从远程仓库获取满足版本规则的最高版本的版本信息.
    3. 通过步骤 b 最终得到所有依赖的具体版本信息和下载地址.
  3. Fetching

    检查缓存中是否存在当前依赖包, 对于不存在的包会维护一个 fetch 队列, 将依赖包下载到缓存目录.

  4. Linking

    将依赖包从缓存目录 扁平化 复制到项目 node_modules 下.

  5. Building

    对存在二进制包的依赖包进行编译.

依赖嵌套问题

早期 NPM 的树形依赖

早期 NPM 的依赖包采用自然的树形依赖: 将项目的直接依赖放到项目 node_modules 根目录, 若直接依赖 A 还依赖模块 B, 则将模块 B 放在模块 A 的 node_modules 下. 在稍复杂的项目中形成“嵌套地狱”:

  • 依赖树层级过深, 一方面难以调试, 另一方面可能出现文件路径过长导致的一些问题;
  • 依赖树的不同分支可能存在重复的依赖包, 导致安装过慢、浪费空间.

扁平化依赖

对于没有版本冲突的依赖包, 扁平化地将依赖包放在 node_modules 根目录.

存在版本冲突时则嵌套安装, 如 模块 A 依赖 模块 B-v1.0 , 模块 C 依赖 模块 B-v2.0 , 则将 模块 B-v2.0 安装在 模块 C 的 node_modules 中.

冲突模块 B 不同版本的安装路径取决于模块 A 和 C 的安装顺序, 先安装的版本放在根目录.

此时, 依赖包的安装顺序对依赖树影响很大: 若 模块 A 依赖 模块 B-v1.0 , 而 模块 C、D、E 都依赖 模块 B-2.0 , 则会导致 C、D、E 模块下都存在重复性的 模块 B-v2.0 .

执行 npm dedupe 会尝试通过将依赖关系向上移动来尽可能删除重复依赖.

Yarn 则会在安装依赖时自动执行 dedupe 命令.


Profile picture

佚树 的个人博客

关于前端、音乐与生活