前言
今天给大家介绍一种新的包管理器:pnpm
,pnpm
由 zkochan 开发,最初也是在用 npm
或者 yarn
遇到了一些不爽的地方,于是自己做了一个开源的包管理器,也为优化包管理提供了一种不同的新思路,接下来我们就看一下为何要使用 pnpm
,以及他能做什么
值得一提的是,zkochan 也是一名乌克兰的开发者,他也在主页呼吁大家援助一下乌克兰,我也希望当前的国际局势能更快的稳定下来,不要损失这么优秀的开发者
为什么会有 pnpm
一句话概括就是:节约磁盘空间并提升安装速度
设想一下,假如公司的每个项目都用了 vue 全家桶,那么每个项目都需要安装 vue、vue-router、vuex、axios 等几乎相同的库,如果有 100 个项目,那就要重复安装 100 遍!!,要知道,现如今,硬盘虽然便宜,但也没那么多插槽啊,更别说笔记本电脑这寸土寸金的资源空间。
因此,pnpm
的核心就是将所有的包都存放在一个资源仓库中,而每个项目的 node_modules
通过软链接的形式将包链接到资源仓库中,这样不仅节省了空间,同时也加快了安装速度
不止于此
pnpm
的另一个优点,在保留了非扁平化的 node_modules
文件夹,同时节省了空间
什么是扁平化的 node_modules
我们简单回顾一下 npm 的发展历史
1.最初,npm
只是简单的通过依赖去递归的安装包,所有的依赖放在相应的包文件夹下面,假如 A
依赖了 foo
,B
也依赖了 foo
,那么就会有两份 foo
安装在 node_modules
node_modules
├── A
│ └── node_modules
│ └── foo
└── B
└── node_modules
└── foo
2.经过一次优化, npm
为了节省空间,采用了扁平化的 node_modules
,这样,假如 A
和 B
都依赖了 foo
,那么 foo
会提升至顶层,也就是说 node_modules
文件夹里的包会变成 A
、B
、foo
node_modules
├── A
├── B
└── foo
这样的好处是,同一个包 foo
只会有一份,节省了空间,但是引入了一个新的问题,引用混乱!!。
试想一下,我们的项目依赖本来只有 A
和 B
,假如你想 import foo
,肯定是找不到的,但是扁平化的结果,将 A
和 B
的依赖层级提升到了顶级,这样,我们直接引用 foo
,其实也不会报错,但实际上我们并没有指定依赖 foo
,导致了使用上的混乱,假如有一天,A
和 B
都不依赖于 foo
了,那我们的项目或者说我提供的包就会报错
这个不良的行为其实已经默默的融入我们的开发中,当我们使用
ant-design
时,我们可以直接使用moment
,当我们使用axios
时,我们可以直接使用qs
,当我们使用webpack
时,我们可以直接使用lodash
,而无需额外的安装包
pnpm 的 node_modules
假设我们的项目依赖 foo
包,而 foo
又依赖 bar
包,那么 pnpm 安装后的实际 node_modules
结果如下
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
我们分析一下这个目录结构
- 包都是从
store
软链接而来 - 项目直接使用的包只有一个
foo
,保证了只有依赖项中的包才能访问 - 有个隐藏的文件夹
.pnpm
,这里将所有的依赖进行扁平化,这样避免了循环符号链接,同时foo
还能引用自己
如果添加 qar@2.0.0
作为 bar
和 foo
的依赖项,那么结果会变成
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar -> <store>/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo -> <store>/foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
└── qar -> <store>/qar
如何使用 pnpm
安装
直接安装
curl -fsSL https://get.pnpm.io/install.sh | PNPM_VERSION=7.0.0-rc.2 sh -
或者
wget -qO- https://get.pnpm.io/install.sh | PNPM_VERSION=7.0.0-rc.2 sh -
通过 npm 安装
npm install -g pnpm@next-7
通过 HomeBrew 安装( 一种 MacOS 包管理器)
brew install pnpm
升级
简单的通过 pnpm
自己的命令升级即可
pnpm add -g pnpm
使用
配置
大部分配置可以直接使用 npm
的配置,与之区别的是有些配置可能是 pnpm
专有的,例如:
# 配置仓库路径
pnpm config set store-dir /path/to/.pnpm-store
命令
- 安装依赖
pnpm install
# 其他一些参数
--offline 仅从 store 中离线下载
--dev, -D 只下载 devDependencies 依赖
--frozen-lockfile 不会生成 lockfile
--shamefully-hoist 创建扁平结构,类似于 npm ,不推荐
- 增加依赖
pnpm add <pkg>
这里 pnpm
有多种增加方式
- 直接通过配置的源安装
在
workspace
中,会先检查workspace
是否有相应包,至于workspace
是什么,在下面会详细说道
- 从本地安装: 例如你想调试开发的包时
pnpm add ./package.tar.gz pnpm add ./some-directory
- 从远端安装 Tar 包
pnpm add https://github.com/indexzero/forever/tarball/v0.5.6
- 从 git 安装
pnpm add <git remote url>
# 其他一些参数
--save-dev, -D 安装为 devDependencies,也就是开发环境使用的包
--save-peer 安装为 peerDependencies ,同时安装到 devDependencies, peerDependencies 通常是和该包一起使用的其他包,需要使用者同时下载
--global, -g 安装为全局依赖
- 发布依赖
pnpm [-r] publish [<tarball|folder>] [--tag <tag>]
[--access <public|restricted>] [options]
--no-git-checks 不检查当前分支是否是可发布分支,是否有没有提交的代码,直接发布
--dry-run 执行发布包的所有流程,但唯独不会真正把包发布到源上,这个在测试发布的时候非常有用
-r 递归的执行发布命令,通常在 workspace 中使用
这里只列出常用的一些,更多的命令可以参考官网
和其他包管理器比较
npm 命令 | pnpm 等效 |
---|---|
npm install | pnpm install |
npm i | [pnpm add |
npm run | [pnpm |
功能 | pnpm | Yarn | npm |
---|---|---|---|
工作空间支持(monorepo) | ✔️ | ✔️ | ✔️ |
隔离的 node_modules | ✔️ - 默认 | ✔️ | ❌ |
提升的 node_modules | ✔️ | ✔️ | ✔️ - 默认 |
Plug'n'Play | ✔️ | ✔️ - 默认 | ❌ |
零安装 | ❌ | ✔️ | ❌ |
修补依赖项 | ❌ | ✔️ | ❌ |
管理 Node.js 版本 | ✔️ | ❌ | ❌ |
有锁文件 | ✔️ - pnpm-lock.yaml | ✔️ - yarn.lock | ✔️ - package-lock.json |
支持覆盖 | ✔️ | ✔️ - 通过 resolutions | ✔️ |
内容可寻址存储 | ✔️ | ❌ | ❌ |
动态包执行 | ✔️ - 通过 pnpm dlx | ✔️ - 通过 yarn dlx | ✔️ - 通过 npx |
Monorepo 及 工作空间(Workspace)
什么是 Monorepo
Monorepo
由两个单词组成,Mono
指的是单个(single),repo
指的是项目存储(Repository),合起来意思就是说,大量的项目存储在单一的库中,用人话讲就是,不同的项目都写在一个 git 库里,第一次听到这个概念的时候大家肯定觉得这人指定有什么大病,这么多项目放一起,大家在里面各种修改,一个人项目修改了,另一个不相干的项目肯定还得更新代码,这不是技术倒退么,这个其实还真就是 Monorepo
的缺点,但是静下心来考虑,他的优点也还是很多的,例如:
- 不同项目之间,如果需要复用相同的组件或者方法,现在就可以很方便的引用了,同时,这些公用方法一旦修改了,其他项目的相应逻辑交互也都更新了,不同在担心忘了安装或者更新,同时也比单纯的 复制黏贴 要更科学
这里插一句,很多公司的项目说是独立的,其实都是从已有的项目中各种复制来的,连带着以前的 bug 也都有了,还不如放一起呢
- 也不是所有的项目都适合放在一起,只有项目之间重合度高的,才应该放一起,例如:管理系统,基本表单操作之类的复用度很高,如果是不相关的项目,彼此没有交集,那就别折腾了
- 不同项目之间,如果拆分的力度比较细,互相之间有调用,那就适合放在一起,最常见的是 npm 库,不同的库之间可能有依赖,同时每个库还需要单独发布
- 它倡导了一种开放,透明,共享的组织文化,所有的开发者都可以浏览学习其他人的代码
说完了 Monorepo
,离真正应用还是有点差距,多个项目之间需要保证自己的独立性,还需要能方便的根据其他项目的变动做出相应的改变,目前市面上也有很多其他的方便管理 Monorepo
的工具,例如:lernajs,幸运的是,pnpm
也对 Monorepo
有着良好的支持,pnpm
称他为 workspace
值得一提的是,vite 也对
Monorepo
做了支持,这使得当引用的其他库发生变化时,正在使用 vite dev 开发的项目也会热更新,详细可见Monorepo 和链接依赖
Workspace
pnpm
可以创建一个 workspace
将多个项目合并到一个中,一个 workspace
的根目录下必须有 pnpm-workspace.yaml
文件,常见的 pnpm-workspace.yaml
类似于下面这种配置,可以定义哪些文件或者文件夹应该包含于 workspace
中
packages:
# all packages in subdirs of packages/ and components/
- 'packages/**'
- 'components/**'
# exclude packages that are inside test directories
- '!**/test/**'
在 workspace
根目录使用 pnpm install
命令时,会自动将 workspace
中的所有项目执行 pnpm install
workspace: 协议
pnpm
支持协议 workspace:
,当使用这个协议时,pnpm
只会解析本地 workspace
中的包,workspace:
协议的使用方法和 package.json
引用包的方法一致
"foo": "workspace:*"
"foo": "workspace:../foo"
"foo": "workspace:~",
"foo": "workspace:^",
"zoo": "workspace:^1.5.0"
当需要发布包时,例如使用 pnpm pack
或者 pnpm publish
命令时,将动态的把 workspace:
协议替换为真正的版本号
结语
pnpm
目前被越来越多的人使用,其中也包括 vue
这样的一线开源框架,具体可以参见 使用示例
在诸如 pnpm
或者 yarn
这样的开源项目影响下,npm
也在反思自己,也及时的做出了更新,例如:支持 workspace
等功能,希望这样的良性循环也让 js 社区越来越好,或许有一天我还会重投 npm
的怀抱也说不定