🎼 前言
作为前端,你一定听说过 Workspace,一定也听说过 Monorepo。相关的工具链也层出不穷,你或许正在用着某些。
可是,这些工具对我们如何组织管理前端项目有哪些帮助,它们之间有哪些区别,我们应该怎样取舍呢?
本文将利用一个很小的项目,带你了解最流行的前端包管理工具 npm-workspaces、yarn-workspaces 和 pnpm-workspaces 之间的差异。
虽然提到了最近很火的 bun,可是它很早就输了。
TL;DR
用 pnpm 管理 Workspace,可能是目前的最佳方案。
主要内容
适合读者
- 前端开发
- 主要是 package 开发者,需要 Monorepo 的
你将获得
- Workspace 的基本概念
- 支持 Workspace 的包管理器有:npm、yarn、pnpm、bun
- 以上工具之间的重要功能比较结果
- 决定最适合自己的工具
历史
日期 | 版本说明 |
---|---|
2023/09/11 | V1 |
🧨 什么是 Workspace
以下引用各官方的说明。
npm-workspaces: Workspaces is a generic term that refers to the set of features in the npm cli that provides support to managing multiple packages from your local file system from within a singular top-level, root package.
yarn-workspaces: Workspaces are the name of individual packages that are part of the same project and that Yarn will install and link together to simplify cross-references.
pnpm-workspaces: pnpm has built-in support for monorepositories (AKA multi-package repositories, multi-project repositories, or monolithic repositories). You can create a workspace to unite multiple projects inside a single repository.
bun-workspaces: Bun supports workspaces inpackage.json
. Workspaces make it easy to develop complex software as a monorepo consisting of several independent packages.
总结来说就是,Workspace 是一套功能,用来辅助在一个项目(repo)下一起开发很多包的应用场景。
⚒️ 准备工具
本文将用到的工具:
| 包管理器 | 版本 | | --- | --- | --- | | Node.js | 20.5.1 | | npm | 10.1.0 | | yarn | 1.22.19 | | pnpm | 8.7.5 | | bun | 1.0.1 |
其中,本文涉及到的「包管理器」是 npm、yarn、pnpm、bun 四个。
Node.js
官网下载安装包,或者(我的选择):
bash
brew install node
提一嘴 corepack(我最后卸载了它)。
「包管理器的管理器」,目前还处于实验阶段,需要调用命令 corepack enable
以启用。
需要注意的是,brew install
的 node 是没有 corepack 的,需要单独安装:
bash
brew install corepack
这样安装后的 corepack 即为启用状态:
如果你此时执行 corepack disable
会发现上图中的几个软链接被干掉了。
之所以提 corepack 是因为,yarn 的版本,只有它可以安装到最新的 3.x
。然而💥,我试了无论 v1、v2 还是 v3,yarn 都没有支持 yarn workspaces foreach。而且 v3 在 install 后并不能如 1.x 那样初始化 Workspace,故此,本篇依然使用 yarn@1.x
。
npm
支持 Workspaces:v7.0.0,2020 年。
npm 跟随 Node.js 安装,但版本不是最新的,比如 node@20.6.1
捆绑了 npm@9.8.1
,执行命令 npm i -g npm
安装最新的 10.1.0
。
我的原则是,如果是 npm 和 brew 都有,优先使用
npm i -g
。
yarn
支持 Workspace:v0.28.1,2016。
yarn 是第一个提供原生 Workspace 支持的包管理器。
Workspaces were initially popularized by projects like Lerna, but Yarn was the first package manager to provide native support for them - support which never stopped improving over years as we build more features around them.
bash
npm i -g yarn
# 或
brew install yarn
pnpm
支持 Workspace:?可能一开始就支持的吧。
bash
npm i -g pnpm
# 或
brew install pnpm
bun
支持 Workspace:?可能一开始就支持的吧。
这个工具太新了,以至于 Homebrew 上都还没有。
bash
curl -fsSL https://bun.sh/install | bash
😈 准备 Demo Project
初始化 Project
bash
md workspace-demo # 新建目录
cd workspace-demo # 进入目录
npm init -y # 初始化 package.json 一切按默认
git init # 初始化本地 git
加个 .gitignore
,内容如下:
gitignore
# common
.*
!.*ignore
!.*.yaml
!.*rc
!.*rc.*
!.husky
*.log*
.npmrc
*-lock.yaml
*-lock.json
# dev
**/node_modules
# generated
**/build/
根 package.json
对自动生成的 package.json 做一些调整:
- 去掉
main
,因为根目录不会被发布成 package - 增加
"private": true
,确保根目录下执行npm publish
报错 - 增加
workspaces
配置项
json
{
"name": "workspace-demo",
"version": "1.0.0",
"description": "",
"private": true,
"keywords": [],
"author": "",
"license": "ISC",
"workspaces": [
"packages-*/*"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
packages-*/*
表示项目根目录下所有 packages-xx 下的一级目录都将属于 Workspace。
workspaces
还有对象格式:
json
{
...,
"workspaces": {
"packages": [
"packages-*/*"
]
}
}
既然是对象,你肯定会问,除了 packages
之外,还有其他什么字段?
完整的 workspaces
类型描述如下:
ts
{
workspaces: string | {
packages?: string[];
nohoist?: string[];
};
}
我没有找到官方的文档,仅在 Yarn 的一篇 blog nohoist in Workspaces 中有提及。
nohoist
举例:
**/react-native
,不要去提升react-native
,不管它的位置在哪里**/react-native/**
,不要去提升react-native
的任何依赖包
文件总览
为了调戏 Workspace 的工作机制,我们需要:
- 一个没有依赖的包
@workspace-demo/ts-config
,它将被所有 TS 写的包依赖 - 两个具有相同依赖
react@18
的包@workspace-demo/react-hook-is-unmounted
和@workspace-demo/react-hook-safe-state
(用以查看 Workspace 对相同依赖的处理) @workspace-demo/react-hook-safe-state
会依赖@workspace-demo/react-hook-is-unmounted
的构建产物@workspace-demo/react-hook-safe-state
比@workspace-demo/react-hook-is-unmounted
多一份lodash-es
的依赖(用以查看 Workspace 对仅某一个包的依赖项的处理)
以往的经验告诉我,包会越来越多,推荐预先开辟几个 packages-xx
,以二级目录的形式组织:
bash
md packages-dev/ts-config # 无依赖,当前项目下的共享 tsconfig
md packages-hook/react-hook-is-unmounted # 依赖 react
md packages-hook/react-hook-safe-state # 依赖 react、lodash-es 和 packages-hook/react-hook-is-unmounted
没有任何多余文件的文件结构如下:
txt
workspace-demo/
├─ packages-dev/
│ └─ ts-config/
│ ├─ index.json
│ └─ package.json
├─ packages-hook/
│ ├─ react-hook-is-unmounted/
│ │ ├─ src/
│ │ │ └─ index.ts
│ │ ├─ package.json
│ │ └─ tsconfig.json
│ └─ react-hook-safe-state/
│ ├─ src/
│ │ └─ index.ts
│ ├─ package.json
│ └─ tsconfig.json
├─ .gitignore
└─ package.json
所有的 tsconfig.json 的内容统一如下,后不再赘述:
json
{
"extends": "@workspace-demo/ts-config/index.json",
"include": [
"src"
]
}
根据 Workspace 的工作原理,对 @workspace-demo/ts-config/index.json
的引用将最终指向 packages-dev/ts-config/index.json。
packages-dev/ts-config/
一个不需要构建的 package,将被用于所有其他 package 作为 tsconfig.json 的基础。
package.json
json
{
"name": "@workspace-demo/ts-config",
"version": "1.0.0",
"description": "A sharable tsconfig, extend it and put an `include` in your own tsconfig.json",
"license": "MIT",
"main": "index.json"
}
index.json
json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"es2015",
"es2017"
],
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"strict": true,
"allowJs": false,
"allowSyntheticDefaultImports": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"noUncheckedIndexedAccess": true,
"declaration": true,
"skipLibCheck": true,
"sourceMap": true,
"jsx": "react",
"baseUrl": "./",
"outDir": "lib"
}
}
packages-hook/react-hook-is-unmounted/
一个简单的 useIsUnmounted
hook。
package.json
json
{
"name": "@workspace-demo/react-hook-is-unmounted",
"version": "1.0.0",
"description": "useIsUnmounted",
"license": "MIT",
"sideEffects": false,
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
"types": "build/types/index.d.ts",
"devDependencies": {
"@workspace-demo/ts-config": "^1.0.0",
"@types/react": "^18.2.21",
"react": "^18.2.0",
"typescript": "^5.2.2"
},
"peerDependencies": {
"react": ">=16.8"
},
"scripts": {
"clean": "rm -rf build",
"build:esm": "tsc --module es6 --declaration false --rootDir src --outDir build/esm",
"build:cjs": "tsc --module commonjs --declaration false --rootDir src --outDir build/cjs",
"build:types": "tsc --rootDir src --outDir build/types --declaration --emitDeclarationOnly",
"build": "npm run build:esm && npm run build:cjs && npm run build:types",
"prepublishOnly": "npm run clean && npm run build"
}
}
src/index.ts
ts
import {
useRef,
useCallback,
useEffect
} from 'react';
export default function useIsUnmounted(): () => boolean {
const ref = useRef<boolean>(false);
const isUnmounted = useCallback(() => ref.current, []);
useEffect(() => {
return () => {
ref.current = true;
};
}, []);
return isUnmounted;
}
packages-hook/react-hook-safe-state/
一个简单的 useSafeState
,永不报错的 useState
,依赖 useIsUnmounted
。
package.json
json
{
"name": "@workspace-demo/react-hook-safe-state",
"version": "1.0.0",
"description": "useSafeState",
"license": "MIT",
"sideEffects": false,
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
"types": "build/types/index.d.ts",
"devDependencies": {
"@workspace-demo/ts-config": "^1.0.0",
"@types/react": "^18.2.21",
"react": "^18.2.0",
"typescript": "^5.2.2"
},
"peerDependencies": {
"react": ">=16.8"
},
"dependencies": {
"@workspace-demo/react-hook-is-unmounted": "^1.0.0",
"lodash-es": "^4.17.21"
},
"scripts": {
"clean": "rm -rf build",
"build:esm": "tsc --module es6 --declaration false --rootDir src --outDir build/esm",
"build:cjs": "tsc --module commonjs --declaration false --rootDir src --outDir build/cjs",
"build:types": "tsc --rootDir src --outDir build/types --declaration --emitDeclarationOnly",
"build": "npm run build:esm && npm run build:cjs && npm run build:types",
"prepublishOnly": "npm run clean && npm run build"
}
}
src/index.ts
未使用
lodash-es
,仅仅是为了观察依赖安装后,是否会将单独依赖安装在本地(实验证明不会)。
ts
import {
Dispatch,
SetStateAction,
useCallback,
useState
} from 'react';
import useIsUnmounted from '@workspace-demo/react-hook-is-unmounted';
export default function useSafeState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] {
const [state, setState] = useState<S>(initialState);
const isUnmounted = useIsUnmounted();
const setSafeState = useCallback((v: SetStateAction<S>): void => {
if (!isUnmounted()) {
setState(v);
}
}, [isUnmounted]);
return [state, setSafeState];
}
收功,打 Tag
项目所有文件准备完毕,打个 Tag 存一下:
bash
git add .
git commit -m 'chore: files ready'
git tag '1-files-ready'
🛵 调戏 Workspace 1:初始化、构建与发布
至此,一个「健康」的项目已经准备完毕,依赖版本都是目前最新的,且不存在同名依赖不同版本的问题。
由于尚未安装任何依赖,WebStorm 各种报错:
npm
初始化(npm install)
执行 npm i
即可完成所有依赖的安装:
安装后的效果:
- 依赖全部「提升」安装在根目录下 node_moduels 下
- 自己的所有 package 均被 npm-link 至 node_moduels 对应的目录下
- 有个 package-lock.json
- 对
@workspace-demo/react-hook-is-unmounted
的引用依然报错(因为它尚未构建) - 对
@workspace-demo/ts-config
的引用已经正确(因为它不需要构建)
构建(prepublishOnly)
只有 @workspace-demo/react-hook-is-unmounted
和 @workspace-demo/react-hook-safe-state
需要构建,在它们的 package.json 中我已经写好构建脚本:
json
{
"scripts": {
"clean": "rm -rf build",
"build:esm": "tsc --module es6 --declaration false --rootDir src --outDir build/esm",
"build:cjs": "tsc --module commonjs --declaration false --rootDir src --outDir build/cjs",
"build:types": "tsc --rootDir src --outDir build/types --declaration --emitDeclarationOnly",
"build": "npm run build:esm && npm run build:cjs && npm run build:types",
"prepublishOnly": "npm run clean && npm run build"
}
}
我们可以手动 cd
到对应的目录,先构建 @workspace-demo/react-hook-is-unmounted
,再构建 @workspace-demo/react-hook-safe-state
。但这不是 Workspace 式的做法。
「更好」的办法方式利用 --workspaces
参数(注意有 s 是复数),在根目录下运行(--if-present
是为了防止在 ts-config 下报错):
bash
npm run --workspaces --if-present prepublishOnly
以 @workspace-demo/react-hook-is-unmounted
为例,可以看到构建结果全部正确:
非字母序构建(prepublishOnly)
我们目前的项目结构简单,@workspace-demo/react-hook-safe-state
依赖了 @workspace-demo/react-hook-is-unmounted
,而它们的目录字母序恰好是 react-hook-is-unmounted → react-hook-safe-state ,这样在构建 @workspace-demo/react-hook-safe-state
的时候,其所有的依赖均已准备好。
本着「求甚解」的精神,我们来测一下非字母序是否依然好使:
bash
rm -rf **/build # 删除之前的构建结果
mv packages-hook/react-hook-safe-state/ packages-hook/react-hook-a-safe-state/ # 改名
npm run --workspaces --if-present prepublishOnly
mv packages-hook/react-hook-a-safe-state/ packages-hook/react-hook-safe-state/ # 还原
可惜,构建失败:
可见 npm-workspaces 并没有对依赖树做排序。要在构建顺序上得到解决,需要引入 monorepo 管理工具,这个不在本篇讨论范围内。
发布(publish)
workspace-demo 项目下的包都是 @workspace-demo/xx
,我没有建此 org(有也不是我的),因此是无法正式发布的,所以调戏 publish
的时候需要带上 --dry-run
参数。
bash
npm --workspaces publish --dry-run
汇总
将之前用到的命令写到根 package.json 下,增加相关 clean
方法:
json
{
...,
"scripts": {
"clean:build": "rm -rf packages-*/**/build",
"clean:node_modules": "rm -rf node_modules packages-*/**/node_modules",
"clean": "npm run clean:build && npm run clean:node_modules",
"boot": "npm i && rm package-lock.json",
"boot:packages": "npm run --workspaces --if-present prepublishOnly",
"pub": "npm --workspaces publish --dry-run"
}
}
说明:
clean
若只写**/build
无法删除在 packages-xx/name 下对应的目录boot
安装成功后删除了 lock 文件(无法通过参数,可以用本地的.npmrc
或全局禁用)
收功:验证 → 打 Tag
bash
npm run clean # 1. 还原
npm run boot # 2. 初始化 workspace
npm run boot:packages # 3. 对 workspace 下的 package 进行构建
npm run pub # 4. 实验 publish
npm run clean # 5. 还原
git commit -am 'chore: npm workspaces' # 6. 提交
git tag 2-npm-workspaces # 7. 打 Tag
yarn
初始化(yarn install)
执行 yarn install
(注意没有短命令别名):
注意到有两个「warning」,这个问题好解决,只需要在根 package.json 增加 devDependencies
即可(但我们先不这么做):
json
{
...,
"devDependencies": {
"react": "^18.2.0"
}
}
再来看安装的内容和 npm install
的区别:
- 相同点
- 在根 node_modules 下安装了所有依赖
- 在根 node_modules 下 link 了项目下的包
- 不同点
- .yarn-integrity 对应 npm 的 .package-lock.json
- 在 package 下也会有 node_modules ,但只有 .bin 目录,且里边是软链接
- yarn.lock 对应 npm 的 package-lock.json
构建(prepublishOnly)
执行 yarn workspaces run prepublishOnly
,会报错:
不幸的是,yarn 并没有 --if-present
或类似的参数,唯一的办法就是给 ts-config/package.json 来上这么一段:
json
{
...,
"scripts": {
"prepublishOnly": "echo '`yarn workspaces run` has no --if-present arg'"
}
}
再构建:
非字母序构建
bash
rm -rf **/build # 删除之前的构建结果
mv packages-hook/react-hook-safe-state/ packages-hook/react-hook-a-safe-state/ # 改名
yarn workspaces run prepublishOnly
mv packages-hook/react-hook-a-safe-state/ packages-hook/react-hook-safe-state/ # 还原
和 npm run --workspaces
一样,yarn workspace run
也只是按照字母序执行:
发布
没找到方法...
汇总
将之前用到的命令写到根 package.json 下:
json
{
...,
"scripts": {
...
"boot-yarn": "yarn install --no-lockfile",
"boot-yarn:packages": "yarn workspaces run prepublishOnly",
"pub-yarn": "echo 'no f* way with yarn'"
}
}
说明:
yarn install
加了--no-lockfile
boot-yarn:packages
要求每个包都必须有prepublishOnly
这样,对新 clone 的项目,就可以两步初始化完毕:
bash
yarn boot-yarn # 初始化 workspace
yarn boot-yarn:packages # 对 workspace 下的 package 进行构建
收功:验证 → 打 Tag
记得还原 ts-config/package.json。
bash
yarn clean # 1. 还原
yarn boot-yarn # 2. 初始化 workspace
yarn boot-yarn:packages # 3. 对 workspace 下的 package 进行构建
yarn pub-yarn # 4. 仅输出一段话
yarn clean # 5. 还原,并且还原掉 ts-config/package.json
git commit -am 'chore: yarn workspaces' # 6. 提交
git tag 3-yarn-workspaces # 7. 打 Tag
pnpm
初始化(yarn install)
此时执行 pnpm i
(pnpm install
),结果什么事情都不做:
pnpm
并不鸟 package.json 里的 workspaces
配置:
A workspace must have a
pnpm-workspace.yaml
file in its root. A workspace also may have an.npmrc
in its root.
建一个 pnpm-workspace.yaml,内容:
yaml
packages:
- 'packages-*/*'
再次执行 pnpm i
:
注意,它说的我们有 4 projects,多了一个,这是因为:
The root package is always included, even when custom location wildcards are used.
安装后依赖效果:
- 所有 package 依赖真正安装的位置在 node_modules/.pnpm 下,并且命名+版本平铺化
.modules.yaml
- 每个 package 有自己的 node_modules ,看上去跟在 package 下单独执行
npm i
差不多 - package _module_modules/.bin 下的命令被 pnpm 重写过,都是真文件,不像之前两种方式的软连接形式
pnpm-lock.yaml
构建(prepublishOnly)
执行 pnpm -r run prepublishOnly
(显示效果比前两个好太多):
注意此时我已经把 ts-config 下的 prepublishOnly
杀掉了,没有报错,也不需要参数 --if-present
(它有)。
非字母序构建
bash
rm -rf **/build # 删除之前的构建结果
mv packages-hook/react-hook-safe-state/ packages-hook/react-hook-a-safe-state/ # 改名
pnpm -r run prepublishOnly
mv packages-hook/react-hook-a-safe-state/ packages-hook/react-hook-safe-state/ # 还原
按照依赖顺序,而不是字母序,构建成功!🎉
发布
bash
pnpm -r run publish --dry-run
报错:
提交代码后再执行:
汇总
将之前用到的命令写到根 package.json 下:
json
{
...,
"scripts": {
...
"boot-pnpm": "pnpm i --no-lockfile",
"boot-pnpm:packages": "pnpm -r run prepublishOnly",
"pub-pnpm": "pnpm -r run publish --dry-run"
}
}
说明:
pnpm i
加了--no-lockfile
收功:验证 → 打 Tag
记得还原 ts-config/package.json。
bash
pnpm clean # 1. 还原
pnpm boot-pnpm # 2. 初始化 workspace
pnpm boot-pnpm:packages # 3. 对 workspace 下的 package 进行构建
git commit -am 'chore: pnpm workspaces' # 4. 提交,否则 publish 报错
pnpm pub-pnpm # 5. publish dry-run
pnpm clean # 6. 还原
git tag 4-pnpm-workspaces # 7. 打 Tag
bun
运行 bun install
报错了:
必须把根 package.json 的 workspaces
改了:
json
{
...,
"workspaces": [
"packages-dev/*",
"packages-hook/*"
]
}
我给提了一个 Issue,然后发现他们文档里边已经有说明了(光这一点,我觉得可以放弃 bun)。
Glob support --- Bun supports simple
<directory>/*
globs in "workspaces". Full glob syntax (e.g. ** and ?) is not yet supported. - bun-workspaces
效果和 npm 的方式比较像:
💥 没有找到对应的办法,直接彻底先放弃 bun 吧。
🚜 调戏 Workspaces 2:某三方依赖有多个不同版本的情况
本次调戏,只看安装后的效果,不进行构建。
准备
这次我们新建一个 package:md packages-rc/rc-button
,文件组织:
txt
rc-button/
├─ src/
│ └─ index.tsx
├─ package.json
└─ tsconfig.json
package.json
这里的 react 版本特意写 17,而不是之前的 18。这样,我们有两个 package 是 react@18,和一个 package 的 react@17。
json
{
"name": "@workspace-demo/rc-button",
"version": "1.0.0",
"description": "Button",
"license": "MIT",
"sideEffects": false,
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
"types": "build/types/index.d.ts",
"devDependencies": {
"@workspace-demo/ts-config": "^1.0.0",
"@types/react": "^17.0.65",
"react": "^17.2.21",
"typescript": "^5.2.2"
},
"peerDependencies": {
"react": ">=16.8"
},
"dependencies": {
"@workspace-demo/react-hook-safe-state": "^1.0.0"
},
"scripts": {
"clean": "rm -rf build",
"build:esm": "tsc --module es6 --declaration false --rootDir src --outDir build/esm",
"build:cjs": "tsc --module commonjs --declaration false --rootDir src --outDir build/cjs",
"build:types": "tsc --rootDir src --outDir build/types --declaration --emitDeclarationOnly",
"build": "npm run build:esm && npm run build:cjs && npm run build:types",
"prepublishOnly": "npm run clean && npm run build"
}
}
src/index.tsx
tsx
import React, {
ButtonHTMLAttributes,
ReactElement,
MouseEvent,
useCallback
} from 'react';
import useSafeState from '@workspace-demo/react-hook-safe-state';
interface IButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
label: string | ReactElement;
}
export default function Button({
label,
onMouseEnter,
onMouseLeave,
...props
}: IButtonProps): ReactElement {
const [safeStateHovered, setSafeStateHovered] = useSafeState<boolean>(false);
const handleMouseEnter = useCallback((e: MouseEvent<HTMLButtonElement>) => {
setSafeStateHovered(true);
onMouseEnter?.(e);
}, [setSafeStateHovered]);
const handleMouseLeave = useCallback((e: MouseEvent<HTMLButtonElement>) => {
setSafeStateHovered(false);
onMouseLeave?.(e);
}, [setSafeStateHovered]);
return <button {...{
...props,
'data-hovered': safeStateHovered ? 1 : 0,
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave
}}>{label}</button>
}
bash
git add .
git commit -m 'feat: add rc-button with different react version'
git tag 5-diff-react-version
npm
bash
npm run boot
可以看到,「额外」的版本被安装在了它当前的包下:
npm 多版本的主次策略是什么呢?一试便知:
bash
npm run clean
mv packages-rc packages-0rc
npm run boot
结论
- 以先安装的版本为主版本
- 次版本是实体安装到对应目录下的,也就是说,会有 n-1 的冗余
还原
bash
mv packages-0rc packages-rc
npm run clean
yarn
bash
yarn boot-yarn
bash
yarn clean
mv packages-rc packages-0rc
yarn boot
虽然 rc-button
下的 react@17
是先安装的,但 react@18
还是坐了根目录下的主位。不过,到底是因为版本大坐的主位,还是个数多坐的主位呢?我把 packages-0rc 和 packages-hook 下相关版本调换了一下,发现是「人多力量大」。
结论
- 以个数多的版本作为主版本
- 次版本也是实体安装,也会有 n-1 的冗余,但由于多者为主,相对 npm 的方式稍稍合理一些
还原
bash
mv packages-0rc packages-rc
yarn clean
pnpm
我们已经知道 Workspace 的每个 package 都有自己完整的 node_modules ,只是里边的包都是根 node_modules/.pnpm 下对应的软链接。由于 .pnpm 下的依赖项会根据版本平铺,因此可以想到 pnpm 下的效果,也就没有必要改名测试了。
结论
- 没有主副版本
- 没有实体冗余
确实应了它宣称的 Saving disk space。
还原
bash
pnpm clean
🎰 大总结
bun 在最初的配置项就被刷下来了,不做比较。
能力 | npm | yarn | pnpm |
---|---|---|---|
支持 Workspace | 7.0.0 2020 |
0.28.1 2016 |
? |
配置 | package.json#workspaces | package.json#workspaces | pnpm-workspace.yaml |
install |
npm i |
yarn install |
pnpm i |
install 无 lock |
💥 无参数,只能 rm |
--no-lockfile |
--no-lockfile |
run 命令 |
npm run --workspaces <script> |
yarn workspaces run <script> |
pnpm -r run |
run 可判空 |
--if-present |
💥 无法判空 | 🎊 天然判空 |
run 顺序,假设 A 依赖 B |
💥 字母序 A → B | 💥 字母序 A → B | 🎊 按照依赖顺序 B → A |
publish |
npm --workspaces publish --dry-run |
💥 不行 | pnpm -r publish --dry-run |
自定义 script 可省 run 执行 |
💥 否 | 🎊 是 | 🎊 是 |
多版本处理 | 💥 以安装先后,先安装的为主版本,其余为独立冗余副版本 | 💥 以引用多的为主版本,其余为独立冗余副版本 | 🎊 都是软链接,每个版本都只有一份实体 |
加一些别的方面的比较:
能力 | npm | yarn | pnpm |
---|---|---|---|
支持 workspaces.nohoist 配置 |
❌ | ✅ | 🎊 都是软链接,无所谓 |
向某个 package 单独安装依赖 | npm -w <workspace> npm i <dep> 这里 <workspace> 既可以是包名,也可以是包目录 |
yarn workspace <workspace> add <dep> 这里 <workspace> 只能是包名 |
💥 未找到方法 |
exec 运行系统命令 |
npm --workspaces exec <command> |
💥 不行 | pnpm -r exec <command> |
上述两个表格,已经可以很明显看出 pnpm 的优势。
最后,如果你的项目是这样一个架构:
txt
your-project/
├─ packages-data/
├─ packages-rc/
├─ ...
├─ src/
└─ package.json
其中 packages- 是用来发包的,最外层 src 是某个前端项目的 assets,整个项目会被推到云端进行构建。
那么用 pnpm
无疑比 npm
更合适,因为云端一般都会用 npm
。不配 npm workspace 的情况下,npm i
得到的依赖永远是 src 下真正需要的,而不是可能在 packages- 下的 link 文件。
最后的最后,Workspace 对本地项目初始化、包的初始化有很好的帮助,但对于多包的发布,尤其是当需要联动修改版本号的时候,可能就力不从心了。
这个时候,就需要有 monorepo 管理工具出场了,这个不在本文讨论范围内,不过还是需要顺带一提:
更多的工具,可以看 Monorepo Tools。
🙋 FAQ
还没有。