@northsea4/tab-proxy-service
v0.2.1
Published
A type-safe wrapper for calling services registered in content scripts from background or other tabs, with automatic type inference
Maintainers
Readme
@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
安装
# 使用 pnpm
pnpm add @northsea4/tab-proxy-service
# 使用 npm
npm install @northsea4/tab-proxy-service
# 使用 yarn
yarn add @northsea4/tab-proxy-service快速开始
💡 推荐: 使用
defineTabProxyServiceAPI,它会自动提供完整的类型提示,无需手动传递泛型类型。
步骤 1: 定义服务
支持三种服务定义方式:
方式 A: 使用 Class (推荐)
// 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: 使用对象字面量
export const [registerPageService, getPageService] = defineTabProxyService('page-service', () => ({
async getTitle() {
return document.title
},
async getUrl() {
return window.location.href
}
}))方式 C: 带参数的服务
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 中注册服务
// 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 中调用服务
// 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 管理器:
// background.ts
import { initTabServiceManager } from '@northsea4/tab-proxy-service'
// 启动时初始化
initTabServiceManager({
logger: console // 可选:启用日志
})2. 在 Content Script 中注册 Service
// 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
// 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
// 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 间通信示例
// 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)// 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 (推荐)
这是最简单的方式,自动获得类型提示:
// 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'
await registerPageService()
// background.ts
import { getPageService } from './services/page-service'
const pageService = getPageService({ targetTabId: 123 })
const title = await pageService.getTitle() // ✅ 自动类型提示方式 2: 使用 TabProxyServiceKey
如果使用传统方式,推荐使用类型约束的 service key:
// 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>// 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)// background.ts
import { createTabProxyService } from '@northsea4/tab-proxy-service'
import { PAGE_SERVICE_KEY } from './service-keys'
// 自动推断类型,无需手动指定泛型
const pageService = createTabProxyService(PAGE_SERVICE_KEY)查询已注册的 Services
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 定义服务提供了最好的代码组织和可维护性:
// 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 支持
使用:
// 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. 对象字面量
简单场景可以使用对象字面量:
export const [registerMyService, getMyService] = defineTabProxyService('my-service', () => ({
async getData(): Promise<Data> {
// ...
},
async setData(data: Data): Promise<void> {
// ...
}
}))3. 函数
async function myFunction(arg: string): Promise<number> {
// ...
}
export const [registerMyFunction, getMyFunction] = defineTabProxyService(
'my-function',
() => myFunction
)4. 嵌套对象
### 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> { /* ... */ },
},
})
);使用:
const api = getApiService({ targetTabId: 123 })
const users = await api.users.list()
const posts = await api.posts.list()辅助工具
flattenPromise
用于简化处理 Promise<Dependency>:
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 调用
完整示例
// ===== 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>- 获取服务代理函数
示例:
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 keyservice: T- 服务实例config?: TabProxyServiceConfig- 可选配置
返回: Promise<() => void> - 返回清理函数
createTabProxyService(key, options?)
创建一个 tab service 的代理。
参数:
key: string | TabProxyServiceKey<T>- Service keyoptions?: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 启发,实现了相反方向的代理服务功能。
