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 可以自動提取為 transferComlink 需要你手動處理。

更好的連接管理 Comctx 內置了心跳檢測,能自動等待遠程服務準備好。這解決了 Comlink 中常見的時序問題 —— 有時候你調用方法時,對方還沒準備好接收消息。

類型安全 TypeScript 支持和 Comlink 一樣好,該有的類型推導都有。

設計思路上的差異#

ComlinkComctx 的設計思路有本質區別:

Comlink 的做法

// 直接包裝整個 worker
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 的簡潔性,學習成本幾乎為零。

相關資源#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。