molvqingtai

molvqingtai

JavaScript Developer and TypeScript Gymnast.

Comctx:Comlink より優れた JS コンテキスト通信ライブラリ

Comlink は Web Worker 通信において素晴らしい性能を発揮し、これが主な設計目標です。しかし、他の環境で使用しようとすると、適応作業が非常に困難であることがわかります。

私が Comctx を開発したのは、この問題を解決するためです。これは Comlink のシンプルな API を維持しつつ、アダプターパターンを通じて環境適応を簡単にします。

具体的に解決した問題#

Comlink は主に Web Worker 用に設計されていますが、理論的には他の環境にも適応可能ですが、実際には非常に困難です。

例えば、ブラウザ拡張では、Content Script と Background Script の間は chrome.runtime API を介してのみ通信できます。Comlink を使用する場合、この API を MessagePort の形式にラップする方法を考えなければなりません。このプロセスは非常に複雑で、Comlink のアダプタコードを再構築する必要があります issuse(438)

同様の問題は Electron や特定の制限された環境でも存在します。新しい環境に遭遇するたびに、複雑な適応作業を行う必要があります。

Comctx の考え方はシンプルです:

  • 特定の通信方法に制限しない
  • メッセージの送信方法、受信方法を教えるアダプタインターフェースを提供する
  • 残りの RPC ロジックはすべて処理する

これにより、同じサービスコードをさまざまな環境で再利用できるようになります。

どこで使えるか見てみましょう#

1. ブラウザ拡張開発#

// 共有のストレージサービス
class StorageService {
  async get(key) {
    const result = await chrome.storage.local.get(key)
    return result[key]
  }
  
  async set(key, value) {
    await chrome.storage.local.set({ [key]: value })
  }
  
  async onChanged(callback) {
    chrome.storage.onChanged.addListener(callback)
  }
}

const [provideStorage, injectStorage] = defineProxy(() => new StorageService())

// Background Script (サービス提供者)
class BackgroundAdapter {
  sendMessage = (message) => chrome.runtime.sendMessage(message)
  onMessage = (callback) => chrome.runtime.onMessage.addListener(callback)
}
provideStorage(new BackgroundAdapter())

// Content Script (サービス使用者)
const storage = injectStorage(new BackgroundAdapter())
await storage.set('userPrefs', { theme: 'dark' })
const prefs = await storage.get('userPrefs')

2. Web Worker 計算タスク#

// 画像処理サービス
class ImageProcessor {
  async processImage(imageData, filters) {
    // 複雑な画像処理アルゴリズム
    return processedData
  }
  
  async onProgress(callback) {
    // 処理進捗コールバック
  }
}

const [provideProcessor, injectProcessor] = defineProxy(() => new ImageProcessor())

// Worker 側
class WorkerAdapter {
  sendMessage = (message) => postMessage(message)
  onMessage = (callback) => addEventListener('message', event => callback(event.data))
}
provideProcessor(new WorkerAdapter())

// メインスレッド
const processor = injectProcessor(new WorkerAdapter())
// 進捗コールバック
processor.onProgress(progress => updateUI(progress))
// 処理結果
const result = await processor.processImage(imageData, filters)

3. iframe クロスドメイン通信#

// 支払いサービス(安全な iframe 内で実行)
class PaymentService {
  async processPayment(amount, cardInfo) {
    // 安全な支払い処理ロジック
    return paymentResult
  }
  
  async validateCard(cardNumber) {
    return isValid
  }
}

// iframe 内の支払いサービス
class IframeAdapter {
  sendMessage = (message) => parent.postMessage(message, '*')
  onMessage = (callback) => addEventListener('message', event => callback(event.data))
}
provide(new IframeAdapter())

// メインページが支払いサービスを呼び出す
const payment = inject(new IframeAdapter())
const result = await payment.processPayment(100, cardInfo)

4. Electron プロセス間通信#

// ファイル操作サービス(メインプロセスでファイルシステムアクセスを提供)
class FileService {
  async readFile(path) {
    return fs.readFileSync(path, 'utf8')
  }
  
  async writeFile(path, content) {
    fs.writeFileSync(path, content)
  }
  
  async watchFile(path, callback) {
    fs.watchFile(path, callback)
  }
}

// メインプロセス
class MainProcessAdapter {
  sendMessage = (message) => webContents.send('ipc-message', message)
  onMessage = (callback) => ipcMain.on('ipc-message', (_, data) => callback(data))
}
provide(new MainProcessAdapter())

// レンダープロセス
class RendererAdapter {
  sendMessage = (message) => ipcRenderer.send('ipc-message', message)
  onMessage = (callback) => ipcRenderer.on('ipc-message', (_, data) => callback(data))
}
const fileService = inject(new RendererAdapter())
const content = await fileService.readFile('/path/to/file')

5. マイクロフロントエンドアーキテクチャ#

// 共有のユーザー認証サービス
class AuthService {
  async login(credentials) { /* ... */ }
  async logout() { /* ... */ }
  async getCurrentUser() { /* ... */ }
  async onAuthStateChange(callback) { /* ... */ }
}

// メインアプリが認証サービスを提供
class MicroFrontendAdapter {
  sendMessage = (message) => window.postMessage({ ...message, source: 'main-app' }, '*')
  onMessage = (callback) => {
    window.addEventListener('message', event => {
      if (event.data.source === 'micro-app') callback(event.data)
    })
  }
}

// 各マイクロフロントエンドアプリが同じ認証サービスを使用できる
const auth = inject(new MicroFrontendAdapter())
const user = await auth.getCurrentUser()

これらの例からわかるように、基盤となる通信メカニズムが何であれ、ビジネスコードは同じです。これがアダプターパターンの利点です。

環境制限の問題を解決するだけでなく、Comctx は他の面でもいくつかの最適化を行っています:

パッケージサイズが小さい コアコードの極めてシンプルな設計のおかげで、Comctx は 1KB+ であり、Comlink は 4KB です。

Transferable Objects の自動処理 ArrayBufferImageData などの大きなオブジェクトを転送する際、Comctx は自動的に transfer に抽出できます。Comlink では手動で処理する必要があります。

より良い接続管理 Comctx はハートビート検出を内蔵しており、リモートサービスが準備できるのを自動的に待機します。これにより、Comlink でよく見られるタイミングの問題が解決されます —— 時にはメソッドを呼び出すときに、相手がメッセージを受け取る準備ができていないことがあります。

型安全性 TypeScript のサポートは Comlink と同様に優れており、必要な型推論がすべてあります。

設計思想の違い#

ComlinkComctx の設計思想には本質的な違いがあります:

Comlink のやり方

// ワーカー全体を直接ラップする
const api = Comlink.wrap(worker)
await api.someMethod()

この方法は非常に直接的ですが、問題は通信メカニズムが固定されていることです。Worker オブジェクトは MessagePort をサポートする必要があり、環境を変えると機能しません。

Comctx のやり方

// 先にサービスを定義する
const [provide, inject] = defineProxy(() => new Service())

// サーバー側:サービスを公開する
provide(adapter)

// クライアント側:サービスを使用する
const service = inject(adapter)

ここでの重要な点は adapter です。これは Comctx にメッセージの送受信方法を教えますが、具体的にどの方法を使用するかを制限しません。これにより、通信方法とビジネスロジックの分離が実現されます。

さらに、Comctx にはハートビート検出メカニズムがあり、接続が生きていることを確認します。これにより、Comlink でよく見られる接続のタイミングの問題が解決されます。

まとめ#

Comctx の開発の目的は非常にシンプルです:RPC 通信を環境の制限から解放することです。

もしあなたが Web Worker の中で使うだけであれば、Comlink で十分です。しかし、あなたのプロジェクトがブラウザ拡張、iframe、Electron、または他のカスタム通信シナリオを含む場合、Comctx はより良い選択肢となるでしょう。

これは環境適応の問題を解決するだけでなく、パッケージサイズ、パフォーマンス、信頼性の面でも改善されています。最も重要なのは、API 設計が Comlink のシンプルさを維持しており、学習コストはほぼゼロであることです。

関連リソース#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。