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()
通過這些例子可以看出,不管底層用的是什麼通信機制,你的業務代碼都是一樣的。這就是適配器模式的好處。
相比 Comlink 有什麼改進#
除了解決環境限制問題,Comctx 在其他方面也做了一些優化:
包體積更小 得益於核心代碼的極簡設計,Comctx 只有 1KB+,而 Comlink 是 4KB+
自動處理 Transferable Objects 當你傳輸 ArrayBuffer、ImageData 這些大對象時,Comctx 可以自動提取為 transfer。Comlink 需要你手動處理。
更好的連接管理 Comctx 內置了心跳檢測,能自動等待遠程服務準備好。這解決了 Comlink 中常見的時序問題 —— 有時候你調用方法時,對方還沒準備好接收消息。
類型安全 TypeScript 支持和 Comlink 一樣好,該有的類型推導都有。
設計思路上的差異#
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 的簡潔性,學習成本幾乎為零。