# node-fetch 移除与 setFetch 适配器保留 — 技术决策记录

> 2026-07-26

## 背景

2026 年 7 月 26 日，`gamerpc` V9.0.3 从项目中移除了 `node-fetch` 依赖。本文档记录这一决策的技术背景、历史演进，以及保留下来的 `setFetch()` 适配器模式的意义。

## 历史演进

### 阶段 1：Node.js 无原生 fetch（V7.x 时代，~2023 年）

`gamerpc` 诞生于 Node.js v16 时代。当时 `fetch` 尚未成为 Node.js 标准 API，浏览器和 Node.js 之间的 HTTP 请求方式完全不同：

- **浏览器**：有 `window.fetch`，无需任何外部依赖
- **Node.js**：只能用 `http`/`https` 模块，或第三方包如 `node-fetch`、`axios`

为了同一份代码在两端运行，引入了 `setFetch` 适配器模式：

```js
// 核心代码（remote.js / authConn.js）
setFetch(fn) {
    this.fetch = fn;    // 注入外部 fetch 实现
    return this;
}

// 使用时根据环境注入不同的实现
if (typeof window !== 'undefined') {
    this.fetch = window.fetch;              // 浏览器原生
} else {
    this.fetch = require('node-fetch');     // Node.js polyfill
}
```

`node-fetch` v2 成为 `gamerpc` 在 Node.js 环境下的**事实依赖**，写入 `devDependencies` 和所有测试脚本。

### 阶段 2：Node.js 原生 fetch 到来（V8.x → V9.0.1）

Node.js v18.0.0（2022 年 4 月）将 `globalThis.fetch` 标记为 stable，`node-fetch` 从必须品变为可选品。但考虑到向后兼容性，项目未立即移除：

- `package.json` 中保留 `node-fetch` 作为 `devDependency`
- 测试脚本继续调用 `setFetch(require('node-fetch'))`
- 核心代码的 `this.fetch || fetch` fallback 逻辑保证了新旧两套都能工作

### 阶段 3：彻底移除（V9.0.3，2026-07-26）

运行环境已确认升级到 Node.js v22.22.3，`globalThis.fetch` 稳定可用。经审计确认：

1. **核心代码**（`src/`）：**0 处**引用 `node-fetch`
2. **测试脚本**：4 处 `setFetch(require('node-fetch'))` 调用
3. **内置 `fetch` 实测**：不调 `setFetch`，LB 请求正常走通，WS 登录 `getToken 0` 成功

决策：**移除 `node-fetch` 依赖，测试脚本不再调用 `setFetch`。**

改动清单：

| 文件 | 改动 |
|------|------|
| `package.json` | 移除 `"node-fetch": "^2.7.0"` |
| `test/game/event.js` | 移除 `.setFetch(require('node-fetch'))` |
| `test/game/auth.js` | 移除 `.setFetch(require('node-fetch'))` |
| `test/game/crm.js` | 移除 `.setFetch(require('node-fetch'))` |
| `test/blockchain/test.js` | 移除 `.setFetch(require('node-fetch'))` |

## 为什么保留 `setFetch()` 方法

`setFetch` **不是 `node-fetch` 的残余**，而是一个通用的**依赖注入适配器**。它的存在理由有三层：

### 1. 向后兼容 — 不破坏现有集成方

任何已经在调用 `setFetch` 的代码**不受影响**：

```js
// 这些调用方代码全部继续工作
conn.setFetch(require('node-fetch'));     // 旧项目，依赖自己装
conn.setFetch(globalThis.fetch);          // Node.js v18+
conn.setFetch(myCustomAdapter);           // 自定义实现（如 timeout wrapper）
```

### 2. 扩展性 — 运行时的 fetch 行为定制

调用方可以通过 `setFetch` 注入增强逻辑，而不需要修改 `gamerpc` 源码：

```js
// 场景 1：超时控制（gamerpc 内部不负责超时）
conn.setFetch((url, opts) => {
    const { timeout, ...rest } = opts;
    return globalThis.fetch(url, {
        ...rest,
        signal: timeout ? AbortSignal.timeout(timeout) : rest.signal,
    });
});

// 场景 2：请求拦截 / 日志
const originalFetch = globalThis.fetch;
conn.setFetch(async (url, opts) => {
    console.log('[gamerpc]', opts.method, url);
    return originalFetch(url, opts);
});

// 场景 3：自定义代理 / 重定向
conn.setFetch((url, opts) => {
    return globalThis.fetch(url.replace('http://', 'https://'), opts);
});
```

### 3. 架构清晰 — 关注点分离

- **gamerpc 负责**：RPC 协议、认证流程、WebSocket 管理
- **调用方负责**：网络传输层（fetch 实现、超时、重试、代理）

`setFetch` 就是这个边界上的接口契约。

## 与 `node-fetch` 相关的三个无害残留字段

`remote.js` 和 `authConn.js` 的 HTTP 方法中保留了三个 `node-fetch` v2 时代的选项字段：

| 字段 | 位置 | 作用（node-fetch v2） | 内置 fetch 行为 |
|------|------|-----------------------|-----------------|
| `json: true` | `remote.js` `get()`/`post()` | 自动解析 JSON body | **静默忽略**（不报错） |
| `mode: 'cors'` | `remote.js` `get()`/`post()` | 跨域策略 | **静默忽略**（Node.js 无同源策略） |
| `uri` (options) | `authConn.js` `request()` | node-fetch 的替代 URL 写法 | **静默忽略**（URL 已通过第一参数传入） |

这三个字段**不产生任何运行时报错**，`globalThis.fetch` 的实现会安全地忽略它们。保留它们而不是删除的原因是：

- **代码稳定性优先**：删除无害字段可能引入不必要的风险
- **最小 diff 原则**：减少合并冲突和代码审查负担
- **历史可追溯性**：新开发者通过这些字段可以理解项目的技术演进脉络

## 总结

```
              ┌─────────────────────────────────────┐
              │          gamerpc 核心                │
              │                                     │
              │   fetch 调用: this.fetch || fetch   │
              │             ↑           ↑           │
              │    setFetch() |    globalThis.fetch  │
              │    (适配器)   |    (Node.js v18+)    │
              │             ↑           ↑           │
              └─────────────┼───────────┼───────────┘
                            │           │
              ┌─────────────┼───────────┼───────────┐
              │  调用方       │           │           │
              │                                     │
              │  conn.setFetch(...)   不调用         │
              │  → 自定义实现         → 自动用内置   │
              └─────────────────────────────────────┘
```

`node-fetch` 完成了它在 Node.js 无原生 fetch 时代的使命，于 V9.0.3 正式移除。`setFetch()` 作为适配器接口保留，是 `gamerpc` 可扩展性的核心设计，不是历史包袱。
