Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 334 additions & 0 deletions jsLearn/pnpm-link使用指南.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
# pnpm link 使用指南

## 一、什么是 pnpm link

`pnpm link` 是 pnpm 提供的一个命令,用于将**本地的包**(package)符号链接(symlink)到当前项目中,从而实现**本地开发调试**时无需发布到 npm 仓库即可引用修改后的包。

它的核心价值在于:**在开发阶段,实时使用本地正在开发的包,而不是 npm registry 上的版本**。

---

## 二、使用场景

### 场景 1:本地开发一个 npm 包并在另一个项目中测试

你正在开发一个工具库 `my-utils`,同时有一个业务项目 `my-app` 需要使用它。你希望在 `my-app` 中实时引用本地的 `my-utils`,而不是每次都 `npm publish`。

### 场景 2:调试第三方开源库

你发现一个第三方库有 bug,你 fork 并 clone 到本地修改后,想在自己的项目中测试修复是否有效。

### 场景 3:Monorepo 中跨包联调

在 Monorepo(如使用 pnpm workspace)中,各子包之间的依赖通常通过 workspace 协议自动链接,但在某些特殊场景下也可手动使用 `pnpm link`。

### 场景 4:CLI 工具本地开发调试

开发一个 CLI 工具(如 `my-cli`),想在本地全局安装并测试,不需要先发布到 npm。

---

## 三、pnpm link 的两种用法

### 用法 1:`pnpm link <dir>` — 直接链接本地目录

将指定目录的包链接到当前项目的 `node_modules` 中。

```bash
# 在业务项目 my-app 中执行
cd /path/to/my-app
pnpm link /path/to/my-utils
```

**效果**:`my-app/node_modules/my-utils` 将变成一个指向 `/path/to/my-utils` 的符号链接。

**适用于**:明确知道本地包路径,一步完成链接。

### 用法 2:`pnpm link --global` + `pnpm link --global <pkg>` — 通过全局 store 链接

这是一个两步操作:

**第一步**:在被链接的包目录中,注册到全局 store

```bash
cd /path/to/my-utils
pnpm link --global
```

**第二步**:在使用方项目中,从全局 store 链接

```bash
cd /path/to/my-app
pnpm link --global my-utils
```

**效果**:同样会在 `my-app/node_modules/` 下创建指向 `my-utils` 的符号链接。

**适用于**:不想记路径、或多个项目都要用同一个本地包时更方便。

---

## 四、完整实战示例

### 示例:本地开发一个工具库并在项目中使用

#### 1. 创建工具库 `my-utils`

```bash
mkdir my-utils && cd my-utils
pnpm init
```

编辑 `package.json`:

```json
{
"name": "my-utils",
"version": "1.0.0",
"main": "index.js"
}
```

编辑 `index.js`:

```javascript
function add(a, b) {
return a + b;
}

function multiply(a, b) {
return a * b;
}

module.exports = { add, multiply };
```

#### 2. 创建业务项目 `my-app`

```bash
mkdir my-app && cd my-app
pnpm init
```

编辑 `index.js`:

```javascript
const { add, multiply } = require("my-utils");

console.log("add(1, 2) =", add(1, 2));
console.log("multiply(3, 4) =", multiply(3, 4));
```

#### 3. 使用 pnpm link 链接

**方式 A:直接路径链接**

```bash
cd /path/to/my-app
pnpm link /path/to/my-utils
```

**方式 B:通过全局 store**

```bash
# 先在 my-utils 中注册
cd /path/to/my-utils
pnpm link --global

# 再在 my-app 中引用
cd /path/to/my-app
pnpm link --global my-utils
```

#### 4. 运行测试

```bash
cd /path/to/my-app
node index.js
# 输出:
# add(1, 2) = 3
# multiply(3, 4) = 12
```

#### 5. 修改工具库后立即生效

编辑 `my-utils/index.js`,修改 `add` 函数:

```javascript
function add(a, b) {
console.log(`Adding ${a} + ${b}`);
return a + b;
}
```

回到 `my-app` 重新运行:

```bash
node index.js
# 输出:
# Adding 1 + 2
# add(1, 2) = 3
# multiply(3, 4) = 12
```

修改**即时生效**,无需重新 link 或安装。

---

## 五、取消链接(unlink)

### 取消项目中的链接

```bash
cd /path/to/my-app
pnpm unlink my-utils
# 然后重新安装正式依赖
pnpm install
```

### 取消全局链接

```bash
cd /path/to/my-utils
pnpm unlink --global
```

---

## 六、pnpm link vs npm link vs yarn link 对比

| 特性 | pnpm link | npm link | yarn link |
|------|-----------|----------|-----------|
| **链接方式** | 符号链接 | 符号链接 | 符号链接 |
| **支持直接路径** | `pnpm link <dir>` | `npm link <dir>`(npm 7+) | 不支持 |
| **全局链接** | `pnpm link --global` | `npm link` | `yarn link` |
| **依赖隔离** | 严格隔离(pnpm 特性) | 扁平化 node_modules | 扁平化 node_modules |
| **幽灵依赖问题** | 不存在 | 可能存在 | 可能存在 |
| **速度** | 快 | 较慢 | 一般 |

> **幽灵依赖**(Phantom Dependencies):项目中没有显式声明但能使用的依赖。pnpm 的严格 node_modules 结构天然避免了这个问题。

---

## 七、常见问题与解决方案

### 问题 1:链接后找不到包

**原因**:被链接的包的 `package.json` 中 `name` 字段与 `require/import` 的名称不一致。

**解决**:确保 `package.json` 的 `name` 字段正确:

```json
{
"name": "my-utils"
}
```

在使用方代码中:

```javascript
const utils = require("my-utils"); // 名称必须一致
```

### 问题 2:链接后依赖报错(peer dependencies 问题)

**原因**:pnpm 的严格依赖隔离可能导致被链接包无法访问宿主项目的依赖。

**解决**:在项目的 `.npmrc` 中添加:

```ini
shamefully-hoist=true
```

或者更精确地:

```ini
public-hoist-pattern[]=*
```

### 问题 3:TypeScript 项目中类型解析失败

**原因**:TypeScript 默认不解析符号链接。

**解决**:在 `tsconfig.json` 中添加:

```json
{
"compilerOptions": {
"preserveSymlinks": true
}
}
```

### 问题 4:React/Vue 等框架出现多实例问题

**原因**:链接的包和主项目各自安装了一份 React/Vue,运行时存在两个实例。

**解决**:在被链接的包中,把 React/Vue 设为 `peerDependencies` 而非 `dependencies`:

```json
{
"peerDependencies": {
"react": ">=17.0.0"
}
}
```

---

## 八、pnpm link 在 Monorepo 中的使用

在 pnpm workspace 中,通常使用 `workspace:` 协议来引用本地包,而不需要手动 link:

### pnpm-workspace.yaml

```yaml
packages:
- "packages/*"
```

### 项目中引用

```json
{
"dependencies": {
"my-utils": "workspace:*"
}
}
```

执行 `pnpm install` 后,pnpm 会自动将 `packages/my-utils` 链接到需要它的项目中。

但如果你需要链接 **workspace 外部** 的包,仍然可以使用 `pnpm link`:

```bash
pnpm link /path/to/external-package
```

---

## 九、最佳实践总结

1. **开发阶段使用 link,发布前使用正式版本**:link 仅用于开发调试,发布前务必切换回 npm registry 的正式版本。

2. **优先使用 `pnpm link <dir>` 而非全局链接**:直接路径链接更明确,不会污染全局环境。

3. **配合 `pnpm watch` 或构建工具的 watch 模式**:如果被链接的包需要编译(如 TypeScript),记得开启 watch 模式。

4. **团队协作时注意 `.npmrc` 配置**:将必要的 hoist 配置提交到仓库,避免其他成员环境差异。

5. **使用完后及时 unlink**:避免残留的符号链接影响后续的 `pnpm install`。

---

## 十、命令速查表

| 命令 | 说明 |
|------|------|
| `pnpm link <dir>` | 将指定目录的包链接到当前项目 |
| `pnpm link --global` | 将当前包注册到全局 store |
| `pnpm link --global <pkg>` | 从全局 store 链接包到当前项目 |
| `pnpm unlink <pkg>` | 取消当前项目中某个包的链接 |
| `pnpm unlink --global` | 取消全局 store 中当前包的注册 |
| `pnpm ls` | 查看当前项目的依赖树(可确认链接状态) |
| `pnpm why <pkg>` | 查看某个包被安装的原因 |