npm package: @northsea4/tab-proxy-service
Description: A type-safe wrapper for calling services registered in content scripts from background or other tabs, with automatic type inference
Author: nornorlunn
Latest version: 0.2.1

# @northsea4/tab-proxy-service

一个类型安全的 WebExtension 工具库,支持在 content script 中注册 service,并允许从 background 或其他 tab 调用这些 service。

## 特性

✅ **在 Content Script 中注册 Service** - 与 `@webext-core/proxy-service` 相反,支持在 tab 中注册服务  
✅ **Background 调用 Tab Service** - 从 background script 调用任意 tab 中的服务  
✅ **Tab 间通信** - 支持一个 tab 调用另一个 tab 中的服务(通过 background 转发)  
✅ **完整的类型安全** - 使用 TypeScript 泛型确保类型安全  
✅ **自动类型推断** - 使用 `defineTabProxyService` 自动获得类型提示  
✅ **嵌套对象支持** - 支持深层嵌套的对象和方法调用  
✅ **自动清理** - Tab 关闭时自动注销服务  
✅ **查询功能** - 可查询所有已注册的 tab services

## 安装

```bash
# 使用 pnpm
pnpm add @northsea4/tab-proxy-service

# 使用 npm
npm install @northsea4/tab-proxy-service

# 使用 yarn
yarn add @northsea4/tab-proxy-service
```

## 快速开始

> **💡 推荐**: 使用 `defineTabProxyService` API，它会自动提供完整的类型提示，无需手动传递泛型类型。

### 步骤 1: 定义服务

支持三种服务定义方式：

#### 方式 A: 使用 Class (推荐)

```ts
// services/hello-service.ts
import { defineTabProxyService } from '@northsea4/tab-proxy-service'

// 使用 class 定义服务
class HelloService {
  async sayHello(name: string): Promise<string> {
    return `Hello, ${name}! From tab: ${document.title}`
  }

  async getTabInfo(): Promise<{ title: string; url: string }> {
    return {
      title: document.title,
      url: window.location.href
    }
  }
}

// 定义服务 - 自动获得类型提示! 🎉
export const [registerHelloService, getHelloService] = defineTabProxyService(
  'hello-service',
  () => new HelloService()
)
```

#### 方式 B: 使用对象字面量

```ts
export const [registerPageService, getPageService] = defineTabProxyService('page-service', () => ({
  async getTitle() {
    return document.title
  },
  async getUrl() {
    return window.location.href
  }
}))
```

#### 方式 C: 带参数的服务

```ts
class ConfigService {
  constructor(private apiUrl: string) {}

  async fetchConfig() {
    return fetch(`${this.apiUrl}/config`).then((r) => r.json())
  }
}

// 支持初始化参数
export const [registerConfigService, getConfigService] = defineTabProxyService(
  'config-service',
  (apiUrl: string) => new ConfigService(apiUrl)
)
```

### 步骤 2: 在 Content Script 中注册服务

```ts
// content-script.ts
import { registerHelloService } from './services/hello-service'

// 注册服务
const cleanup = await registerHelloService()

// 页面卸载时清理
window.addEventListener('beforeunload', cleanup)

// 如果服务需要参数
// const cleanup = await registerConfigService('https://api.example.com');
```

### 步骤 3: 在 Background 中调用服务

```ts
// background.ts
import { getHelloService } from './services/hello-service'

// 获取服务 - 自动有完整的类型提示和自动补全! 🚀
const helloService = getHelloService({ targetTabId: 123 })

// sayHello 方法会有自动补全和类型检查 ✨
const greeting = await helloService.sayHello('World')
const info = await helloService.getTabInfo()
```

---

## 传统方式 (不推荐)

> **⚠️ 注意**: 以下方式需要手动传递类型参数，建议使用上面的 `defineTabProxyService` 方式。

使用 `registerTabService` 和 `createTabProxyService` (需要手动传递类型参数):

### 1. 在 Background 中初始化管理器

首先,必须在 background script 中初始化 tab service 管理器:

```ts
// background.ts
import { initTabServiceManager } from '@northsea4/tab-proxy-service'

// 启动时初始化
initTabServiceManager({
  logger: console // 可选:启用日志
})
```

### 2. 在 Content Script 中注册 Service

```ts
// content-script.ts
import { registerTabService } from '@northsea4/tab-proxy-service'

// 定义你的 service
const pageService = {
  async getPageTitle(): Promise<string> {
    return document.title
  },

  async getPageUrl(): Promise<string> {
    return window.location.href
  },

  async extractData(): Promise<any[]> {
    // 提取页面数据
    return Array.from(document.querySelectorAll('.item')).map((el) => ({
      text: el.textContent,
      href: el.getAttribute('href')
    }))
  }
}

// 注册 service
const cleanup = await registerTabService('page-service', pageService)

// 页面卸载时清理
window.addEventListener('beforeunload', cleanup)
```

### 3. 从 Background 调用 Tab Service

```ts
// background.ts
import { createTabProxyService, type TabProxyServiceKey } from '@northsea4/tab-proxy-service'

// 定义类型
interface PageService {
  getPageTitle(): Promise<string>
  getPageUrl(): Promise<string>
  extractData(): Promise<any[]>
}

// 创建代理(会调用任意一个注册了该 service 的 tab)
const pageService = createTabProxyService<PageService>('page-service')

// 使用
const title = await pageService.getPageTitle()
const data = await pageService.extractData()

console.log('Page title:', title)
console.log('Extracted data:', data)
```

### 4. 调用特定 Tab 中的 Service

```ts
// background.ts 或 另一个 content-script.ts
import { createTabProxyService } from '@northsea4/tab-proxy-service'

// 指定目标 tab ID
const specificTabService = createTabProxyService<PageService>('page-service', {
  targetTabId: 123 // 目标 tab 的 ID
})

const title = await specificTabService.getPageTitle()
```

### 5. Tab 间通信示例

```ts
// content-script-a.ts (Tab A)
import { registerTabService } from '@northsea4/tab-proxy-service'

const dataService = {
  async shareData() {
    return { data: 'from tab A', timestamp: Date.now() }
  }
}

await registerTabService('data-service', dataService)
```

```ts
// content-script-b.ts (Tab B)
import { createTabProxyService } from '@northsea4/tab-proxy-service'

// Tab B 调用 Tab A 中的服务
const dataService = createTabProxyService('data-service', {
  targetTabId: tabAId // Tab A 的 ID
})

const data = await dataService.shareData()
console.log('Data from Tab A:', data)
```

## 类型安全最佳实践

### 方式 1: 使用 `defineTabProxyService` (推荐)

这是最简单的方式，自动获得类型提示：

```ts
// services/page-service.ts
import { defineTabProxyService } from '@northsea4/tab-proxy-service'

class PageService {
  async getTitle(): Promise<string> {
    return document.title
  }

  async extractLinks(): Promise<string[]> {
    return Array.from(document.querySelectorAll('a'))
      .map((a) => a.href)
      .filter(Boolean)
  }
}

// 导出注册和获取函数
export const [registerPageService, getPageService] = defineTabProxyService(
  'page-service',
  () => new PageService()
)
```

**使用:**

```ts
// content-script.ts
import { registerPageService } from './services/page-service'
await registerPageService()

// background.ts
import { getPageService } from './services/page-service'
const pageService = getPageService({ targetTabId: 123 })
const title = await pageService.getTitle() // ✅ 自动类型提示
```

### 方式 2: 使用 `TabProxyServiceKey`

如果使用传统方式，推荐使用类型约束的 service key：

```ts
// service-keys.ts
import type { TabProxyServiceKey } from '@northsea4/tab-proxy-service'
import type { PageService } from './page-service'

// 使用类型约束的 key
export const PAGE_SERVICE_KEY = 'page-service' as TabProxyServiceKey<PageService>
```

```ts
// content-script.ts
import { registerTabService } from '@northsea4/tab-proxy-service'
import { PAGE_SERVICE_KEY } from './service-keys'
import { createPageService } from './page-service'

const pageService = createPageService()
await registerTabService(PAGE_SERVICE_KEY, pageService)
```

```ts
// background.ts
import { createTabProxyService } from '@northsea4/tab-proxy-service'
import { PAGE_SERVICE_KEY } from './service-keys'

// 自动推断类型,无需手动指定泛型
const pageService = createTabProxyService(PAGE_SERVICE_KEY)
```

## 查询已注册的 Services

```ts
import { queryTabServices } from '@northsea4/tab-proxy-service'

const services = await queryTabServices()
console.log('Registered services:', services)
// [
//   { tabId: 123, serviceKey: 'page-service', registeredAt: 1234567890 },
//   { tabId: 456, serviceKey: 'data-service', registeredAt: 1234567891 },
// ]
```

## 支持的 Service 类型

### 1. Class（推荐）

使用 class 定义服务提供了最好的代码组织和可维护性：

```ts
// services/data-service.ts
import { defineTabProxyService } from '@northsea4/tab-proxy-service'

class DataService {
  private cache = new Map<string, any>()

  constructor(private apiUrl?: string) {}

  async getData(key: string): Promise<any> {
    if (this.cache.has(key)) {
      return this.cache.get(key)
    }

    const data = await fetch(`${this.apiUrl || ''}/data/${key}`).then((r) => r.json())
    this.cache.set(key, data)
    return data
  }

  async setData(key: string, value: any): Promise<void> {
    this.cache.set(key, value)
  }

  async clearCache(): Promise<void> {
    this.cache.clear()
  }
}

// 定义服务
export const [registerDataService, getDataService] = defineTabProxyService(
  'data-service',
  (apiUrl?: string) => new DataService(apiUrl)
)
```

**优点:**

- ✅ 支持私有成员和状态管理
- ✅ 支持构造函数参数
- ✅ 良好的代码组织
- ✅ 完整的 IDE 支持

**使用:**

```ts
// content-script.ts
const cleanup = await registerDataService('https://api.example.com')

// background.ts
const dataService = getDataService({ targetTabId: 123 })
const data = await dataService.getData('users')
```

### 2. 对象字面量

简单场景可以使用对象字面量：

```ts
export const [registerMyService, getMyService] = defineTabProxyService('my-service', () => ({
  async getData(): Promise<Data> {
    // ...
  },
  async setData(data: Data): Promise<void> {
    // ...
  }
}))
```

### 3. 函数

```ts
async function myFunction(arg: string): Promise<number> {
  // ...
}

export const [registerMyFunction, getMyFunction] = defineTabProxyService(
  'my-function',
  () => myFunction
)
```

### 4. 嵌套对象

````ts
### 4. 嵌套对象

支持深层嵌套的对象和方法调用：

```ts
export const [registerApiService, getApiService] = defineTabProxyService(
  'api-service',
  () => ({
    users: {
      async list(): Promise<User[]> { /* ... */ },
      async get(id: string): Promise<User> { /* ... */ },
    },
    posts: {
      async list(): Promise<Post[]> { /* ... */ },
      async create(post: Post): Promise<void> { /* ... */ },
    },
  })
);
````

**使用:**

```ts
const api = getApiService({ targetTabId: 123 })
const users = await api.users.list()
const posts = await api.posts.list()
```

## 辅助工具

### flattenPromise

用于简化处理 `Promise<Dependency>`:

```ts
import { flattenPromise } from '@northsea4/tab-proxy-service'

function createService(dbPromise: Promise<Database>) {
  const db = flattenPromise(dbPromise)

  return {
    async getData() {
      // 不需要 await (await dbPromise).query()
      return await db.query('SELECT * FROM table')
    }
  }
}
```

## 与 @webext-core/proxy-service 的对比

| 功能                       | @webext-core/proxy-service | @northsea4/tab-proxy-service    |
| -------------------------- | -------------------------- | ------------------------------- |
| 在 Background 注册服务     | ✅                         | ❌                              |
| 在 Content Script 注册服务 | ❌                         | ✅                              |
| Background → Content 调用  | ❌                         | ✅                              |
| Content → Background 调用  | ✅                         | 使用 @webext-core/proxy-service |
| Tab → Tab 调用             | ❌                         | ✅                              |
| 类型安全                   | ✅                         | ✅                              |
| 嵌套对象支持               | ✅                         | ✅                              |

**建议**: 两个库可以配合使用:

- 使用 `@webext-core/proxy-service` 在 background 注册服务,供其他上下文调用
- 使用 `@northsea4/tab-proxy-service` 在 content script 注册服务,供 background 或其他 tab 调用

## 完整示例

```ts
// ===== services/page-service.ts =====
import { defineTabProxyService } from '@northsea4/tab-proxy-service'

class PageService {
  async getTitle(): Promise<string> {
    return document.title
  }

  async extractLinks(): Promise<string[]> {
    return Array.from(document.querySelectorAll('a'))
      .map((a) => a.href)
      .filter(Boolean)
  }
}

export const [registerPageService, getPageService] = defineTabProxyService(
  'page-service',
  () => new PageService()
)

// ===== content-script.ts =====
import { registerPageService } from './services/page-service'

// 注册服务
const cleanup = await registerPageService()

// 清理
window.addEventListener('beforeunload', cleanup)

// ===== background.ts =====
import { initTabServiceManager } from '@northsea4/tab-proxy-service'
import { getPageService } from './services/page-service'

// 初始化管理器
initTabServiceManager({ logger: console })

// 监听扩展图标点击
browser.action.onClicked.addListener(async (tab) => {
  if (!tab.id) return

  // 调用当前 tab 中的服务
  const pageService = getPageService({ targetTabId: tab.id })

  try {
    const title = await pageService.getTitle()
    const links = await pageService.extractLinks()

    console.log('Page title:', title)
    console.log('Links found:', links.length)
  } catch (error) {
    console.error('Failed to call service:', error)
  }
})
```

## API 参考

### `defineTabProxyService(name, init, config?)` (推荐)

定义一个 tab proxy service 的辅助函数,自动提供类型提示。

**参数:**

- `name: string` - Service 的唯一名称
- `init: (...args: TArgs) => TService` - 初始化函数,返回服务实例
- `config?: TabProxyServiceConfig` - 可选配置

**返回:** `[registerService, getService]` - 返回元组

- `registerService: (...args: TArgs) => Promise<() => void>` - 注册服务函数
- `getService: (options?) => TabProxyService<TService>` - 获取服务代理函数

**示例:**

```ts
export const [registerHelloService, getHelloService] = defineTabProxyService(
  'hello-service',
  () => new HelloService()
)
```

### `initTabServiceManager(config?)`

在 background 中初始化 tab service 管理器。必须在使用其他功能前调用。

**参数:**

- `config?: TabProxyServiceConfig` - 可选配置
  - `logger?: Console` - 日志记录器
  - `timeout?: number` - 消息超时时间(毫秒)

### `registerTabService(key, service, config?)`

在 content script 中注册一个服务。

**参数:**

- `key: string | TabProxyServiceKey<T>` - Service key
- `service: T` - 服务实例
- `config?: TabProxyServiceConfig` - 可选配置

**返回:** `Promise<() => void>` - 返回清理函数

### `createTabProxyService(key, options?)`

创建一个 tab service 的代理。

**参数:**

- `key: string | TabProxyServiceKey<T>` - Service key
- `options?`:
  - `targetTabId?: number` - 目标 tab ID(可选)
  - `config?: TabProxyServiceConfig` - 配置(可选)

**返回:** `TabProxyService<T>` - Service 代理

### `queryTabServices()`

查询所有已注册的 tab services。

**返回:** `Promise<TabServiceInfo[]>` - Service 信息列表

### `flattenPromise(promise)`

扁平化 Promise,用于简化处理 `Promise<Dependency>`。

**参数:**

- `promise: Promise<T>` - 要扁平化的 Promise

**返回:** `TabProxyService<T>` - 扁平化后的代理

## License

MIT

## 作者

nornorlunn

## 致谢

本项目受 [@webext-core/proxy-service](https://github.com/aklinker1/webext-core) 启发,实现了相反方向的代理服务功能。