包管理工具

Posted by CodingWithAlice on August 24, 2022

包管理工具

参考文章:JavaScript package managers compared: npm, Yarn, or pnpm?

看到另一篇主要讲 pnpm 实现的博客,也可以了解下 2022年了,你还没用pnpm吗?

背景:在工作遇到

  • (node14)npm install + npm run build 会打包静态资源失败
  • yarn + yarn build 成功

:::tip 之后尝试过切换到 node 16 ,npm i + npm run build 可以成功 :::

在我看来,yarn buildnpm run build 都是执行 package.json 里面的 build 指令串,那么差异就落在了 install 依赖的时候,yarn 和 npm 是有差异的。

  • 我看到工程里面,存在 yarn.lock 文件,而不存在 package-lock.json 文件,认为 yarn 安装的时候,lock 文件限制了包版本不是 package 中的最高版本,所以不会报错;
  • 但是我同事认为,是 yarn 和 npm 安装依赖的方式不同 -> 也就是 引入文件存储的路径不同,导致打包的时候报一些引入文件的错误(I need evidence)

我在网上查 yarn 和 npm 有什么区别,说的最多的是 speed,yarn 比 npm4.0 快很多,但是现在 npm 的版本已经升很高了,一些语义化指令也都支持了,速度也会快一些(还有些疑问:具体快多少?在 yarn 出现后 npm 是不是也做了优化?)

同时我也很想知道两种打包方式,引入包的路径究竟是不是有差异的,我遇到的这个问题,究竟是不是包路径的问题?

贴一下我用到的 node 版本 + 对应的 npm 版本:

image-20220824212246863

在此基础上,我开始啃国外的文章(国内真的好千篇一律啊啊啊啊啊),参考文章写于 2022.02,还算比较新的。

以下为关键信息笔记

当今包管理器领域存在三个主要参与者:npm、Yarn(v1、v2)、pnpm(高性能 npm)

共同点

功能对等;npm 和 Yarn 都将依赖安装在一个平面的 node_modules 中(pnpm不同,引入了一些新概念)

差异点

主要是安装速度、存储消耗、和项目工作流结匹配程度

简要了解每个工具出现的原因
  • npm

    解决的问题是,当时项目依赖是手动下载管理的(手动管理年代)

  • Yarn v1(又名 Yarn Classic)

    是基于 npm 出现的,并行化操作 加快了安装过程,针对的是 npm 早期版本的主要痛点

  • pnpm 是 npm 的替代品,解决 npm 和 Yarn 在跨项目使用的 依赖项冗余存储
    • (npm 和 yarn v1使用的是相同的依赖解析方法,都是 使用提升来扁平化 node_modules),pnpm 引入「内容可寻址存储」,生成一个嵌套 node_modules,保存在全局存储 ~/.pnpm-store/ 中,实现了依赖的每一个版本仅在该文件中进行一次物理存储 -> 构成单一来源,节省磁盘空间
  • Yarn v2(又名 Yarn Berry)主要创新是 即插即用(PnP方法,一个解决 node_modules 的策略)

    缺点:需要更新,不兼容 v1(添加一个配置开启 node_modules plugin 才可以使用传统的 node_modules 方法)

    • 不创建 node_modules,而是生成一个带有依赖查找表的文件 .pnp.cjs(执行更快,因为单一文件快于嵌套的文件夹结构)

    • 此外在 .yarn/cache/ 文件夹中,每个包都是 zip 文件,占用磁盘空间较少

每种工具的安装
  • npm

    和 Node.js 捆绑,没有额外操作步骤

  • Yarn Classic 和 Yarn Berry

    npm i -g yarn 或者 corepack prepare yarn@3.1.1 --activate

    Corepack - 在 Node > v16.9.0 后不必单独安装,只需要激活,已经预安装垫片;旧版本 npm install -g corepack

  • pnpm

    npm i -g pnpm 或者 corepack prepare pnpm@6.24.2 --activate(和 Yarn v1/v2 一样,在高版本 Node 中激活即可)

项目结构

所有工具都存储了重要的元信息在 package.json,而且根目录下的配置文件可以设置依赖解析的方法

  • npm:执行 npm i ,生成 package-lock.json + node_modules;其中 .npmrc 可以放在根目录作为配置文件

    .
    ├── node_modules/
    ├── .npmrc 
    ├── package-lock.json
    └── package.json
    
  • Yarn Classic:执行 yarn,创建 yarn.lock + node_modules;其中 .yarnrc 可以配置(也支持 .npmrc);

    .yarn 文件可选,用于缓存(cache/) + 存储当前 yarn 版本(releases/)

    .
    ├── .yarn/
    │   ├── cache/
    │   └── releases/
    │       └── yarn-1.22.17.cjs
    ├── node_modules/
    ├── .yarnrc
    ├── package.json
    └── yarn.lock
    
  • Yarn Berry:不支持 .npmrc 和 .yarnrc,需要一个 .yarnrc.yml 配置文件

    1、为了实现传统模式,配置一个 nodeLinker 为 node-modules

    # .yarnrc.yml
    nodeLinker: node-modules # or pnpm
    

    执行 yarn ,生成 node_modules + yarn.lock (新形式的 lock 文件,和 Yarn v1 不兼容)

    .
    ├── .yarn/
    │   ├── cache/
    │   └── releases/
    │       └── yarn-3.1.1.cjs
    ├── node_modules/
    ├── .yarnrc.yml
    ├── package.json
    └── yarn.lock
    

    2、配置为新的模式 - PnP

    # .yarnrc.yml
    nodeLinker: pnp
    pnpMode: loose # or strict(默认模式为 strict)
    

    执行 yarn 生成 .yarn/cache + .yarn/unplugged + .pnp.cjs + yarn.lock;其中 .yarn/sdk 提供 IDE 支持

    .
    ├── .yarn/
    │   ├── cache/
    │   ├── releases/
    │   │   └── yarn-3.1.1.cjs
    │   ├── sdk/
    │   └── unplugged/
    ├── .pnp.cjs
    ├── .pnp.loader.mjs
    ├── .yarnrc.yml
    ├── package.json
    └── yarn.lock
    
  • pnpm:执行 pnpm i,生成 pnp-lock.yml + node_modules (内容完全和 npm 不一样,因为 pnpm 用的是内容可寻址存储);支持 .npmrc 的配置文件

    .
    ├── node_modules/
    │   └── .pnpm/
    ├── .npmrc
    ├── package.json
    └── pnpm-lock.yml
    
lockfile 和依赖存储

lockfile 出现在 npm > v5 (package-lock.json);pnpm(pnpm-lock.yaml);Yarn v2(新形式的 yarn.lock);

npm 、 Yarn Classic、pnpm 用的都是将依赖存储于 node_modules,pnpm 用了更高效的办法(内容可寻址存储),Yarn Berry - PnP 没有用 node_modules,而是用 .yarn/cache/.pnp.cjs 存储了依赖的 zip 文件

CLI 指令
Action npm Yarn Classic Yarn Berry pnpm
安装全部 package.json npm install
alias: i, add
yarn install or yarn like Classic pnpm install
alias: i
更新到最新版本 npm update react@latest yarn upgrade react --latest yarn up react pnpm up -L react
新增依赖 npm i react yarn add react like Classic pnpm add react
卸载依赖,同时从 package.json 移除 npm uninstall react
alias: remove, rm, r, un, unlink
yarn remove react like Classic pnpm remove react
alias: rm, un, uninstall

为了安全性考虑,Yarn Berry 和 pnpm 只允许执行在 package.json 或者 /.bin 文件中声明的二进制文件。

配置文件

包括 package.json 和指定的配置文件

  • npm - .npmrc

    # .npmrc
    # 可以配置私有表,重用已经安装过的共享库
    @doppelmutzi:registry=https://gitlab.doppelmutzi.com/api/v4/projects/41/packages/npm/
    # 指定依赖源
    registry=https://registry.npmmirror.com/
    
  • Yarn Classic - .yarnrc

    # .yarnrc
    # 可以指定特定的 Yarn 版本
    yarn-path: .yarn/releases/yarn-3.1.1.cjs
    
  • Yarn Berry - .yarnrc.yml

    # .yarnrc.yml
    # 注意属性命名变了
    yarnPath: .yarn/releases/yarn-3.1.1.cjs
    # 可以通过插件 yarn-plugin-import 扩展
    plugins:
      - path: .yarn/plugins/@yarnpkg/plugin-semver-up.cjs
        spec: "https://raw.githubusercontent.com/tophat/yarn-plugin-semver-up/master/bundles/%40yarnpkg/plugin-semver-up.js"
    # 用于解决 PnP 模式下的不兼容问题
    packageExtensions:
      "styled-components@*":
        dependencies:
          react-is: "*"
    
  • pnpm - .npmrc,和 npm 类似(工作区支持多包,如果需要 monorepo,需要在 pnpm-workspace.yaml 中配置)

    # pnpm-workspace.yaml
    packages:
      - 'packages/**'
    
Monorepo 支持情况(配置工作区)
  • npm > v7 支持工作区,大部分指令支持配置是否需要支持多包工作区

    ⚠️ npm v8 当前并不支持高级过滤,也不支持并行制从多个与工作区相关的命令

    # Installing all dependencies for all workspaces
    $ npm i --workspaces.
      
    # 针对一个包执行
    $ npm run test --workspace=hooks
    # 针对多个包执行
    $ npm run test --workspace=hooks --workspace=utils
    # run against all
    $ npm run test --workspaces
    # ignore all packages missing test
    $ npm run test --workspaces --if-present
    
  • Yarn Classic

    在 2017.08 之前,需要用第三方软件(例如 Lerna)支持多包项目,2017.08 之后发布了在工作空间对 monorepo 进行一流的支持。

    # Installing all dependencies for all workspaces
    $ yarn
    # 显示依赖树
    $ yarn workspaces info
    # 仅对一个包运行 start 命令
    $ yarn workspace awesome-package start
    # 添加 Webpack to package
    $ yarn workspace awesome-package add -D webpack
    # 添加 React to all packages
    $ yarn add react -W
    
  • Yarn Berry

    基于 Yarn v1 ,一开始 Yarn v2 就以工作区为特色。相比于 Yarn v1, Yarn v2 明确定义依赖项(dependencies or devDependencies)必须是此 monorepo 中的包之一

    # 在安装包时复用来自其他工作区的版本
    $ yarn add --interactive
    # 跨所有工作区更新包
    $ yarn up
    # 仅为单个工作区安装依赖项
    $ yarn workspaces focus
    # 在所有工作区上运行命令
    $ yarn workspaces foreach
    
  • pnpm

    和 Yarn v2 相似实现了 monorepo,大部分指令接受一些选项--recursive (-r)、--filter

    # 修剪所有工作区 
    $ pnpm -r exec -- rm -rf node_modules && rm pnpm-lock.yaml  
    # 对范围为 @doppelmutzi 的所有工作区运行所有测试
    $ pnpm recursive run test --filter @doppelmutzi/
    
性能和磁盘空间效率

结论: Yarn Berry(PnP模式)安装速度最快

image-20220915205017688

:::tips 我们常说 yarn 比 npm 快,其实我们如果不是用 yarn v2,那么不如使用 npm ,速度要快,当然包大小还是 yarn 有优势 :::

安全功能
  • npm - 太宽松,正在解决安全 gaps,尤其是落后于 Yarn 的安全问题

  • Yarn - v1 和 v2 从一开始就从 yarn.lock 校验包完整性,在安装期间检测到没有在 package.json 中声明的变量,则会中止

    Yarn v2 没有传统的 node_modules 方面的安全问题,有更高的指令执行的安全性

  • pnpm - 在执行前也会用 checksum(校验和) 验证完整性

    相比于 npm 和 Yarn v1 使用提升 node_modules 带来的安全问题,pnpm 生成的嵌套的 node_modules 消除了非法依赖访问的风险,因为这保证了只能访问 package.json 中声明的依赖项

一些热门项目在用什么包管理器呢?
  • npm - Svelte、Preact、Express.js、Meteor、Apollo Server
  • Yarn Classic - React、Angular、Ember、Next.js、Gatsby、Nuxt、Create React App、webpack-cli、Emotion
  • Yarn Berry - Jest (with node_modules)、Storybook (with node_modules)、Babel (with node_modules)、Redux Toolkit (with node_modules)
  • pnpm - Vue 3、Browserlist、Prisma、SvelteKit

总结(关键点):

  • npm > v8时,yarn v1 没有 npm 好用
    • 配置文件 .npmrc + lockfile:package-lock.json
  • yarn v1
    • 并行化安装,快于早期的 npm
    • 和 npm 使用相同的依赖解析方法 -通过依赖 提升,来扁平化 node_modules
    • 配置文件 .yarnrc,也支持 .npmrc + lockfilke:yarn.lock
  • yarn v2(PnP模式)
    • 不生成 node_modules ,生成 依赖查找表 .pnp.cjs,包以 zip 文件在缓存中
    • 配置文件:不支持 .npmrc 和 .yarnrc,而是 .yarnrc.yml + yanr.lock(和 yarn v1 的不兼容)
  • pnpm
    • 生成嵌套的 node_modules,内容可寻址存储,只需要一次物理存储
    • 在 Node v16.9.0 后不必单独安装(pnpm、yarn v1/v2 都只需要激活)
    • 配置文件:.npmrc + pnpm-lock.yml

回答博客开始的疑问:npm v8 和 yarn v1 其实在存储依赖上没有大的差异,之所以在 Node14 版本(npm v6)安装失败,应该是 npm v7 时支持了 yarn.lock,使项目可以按照 lockfile 安装指定版本