Comlink makes WebWorkers enjoyable. Comlink is a tiny library (1.1kB), that removes the mental barrier of thinking about postMessage and hides the fact that you are working with workers.
At a more abstract level it is an RPC implementation for postMessage and ES6 Proxies.
comlink 的核心实现基于 postMessage
和 ES6 Proxy,理论上在支持 Proxy
与类 postMessage
双向通信机制的 JavaScript 环境中都可以实现一套 comlink 适配器,使之可以在 WebWorkers 之外的环境使用,适配器的实现可以参考 node-adapter。
部分 comlink 的高级功能需要用到 MessageChannel 与 MessagePort 传递,有些平台的适配器可能无法支持,涉及的高级功能有:
- 使用
new ProxyTarget()
构造远程代理对象 - Comlink.proxy
- Comlink.createEndpoint
目前实现的适配器如下:
欢迎提 issues 或者一起新增其他应用平台的适配器。
# npm
npm i comlink comlink-adapters -S
# yarn
yarn add comlink comlink-adapters
# pnpm
pnpm add comlink comlink-adapters
Adapters:
electronMainEndpoint
用于主进程创建Endpoint
对象。electronRendererEndpoint
用于渲染进程创建Endpoint
对象。
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ✅ | await new ProxyObj(); |
|
proxy function | ✅ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
|
createEndpoint | ✅ | proxyObj[comlink.createEndpoint](); |
不建议使用 |
release | ✅ | proxyObj[comlink.releaseProxy](); |
createEndpoint 支持但不建议使用,内部实现使用 MessagePort 与 MessagePortMain 进行桥接,效率较差。
electronMainEndpoint:
interface ElectronMainEndpointOptions {
sender: WebContents;
ipcMain: IpcMain;
messageChannelConstructor: new () => MessageChannelMain;
channelName?: string;
}
interface electronMainEndpoint {
(options: ElectronMainEndpointOptions): Endpoint;
}
- sender: 与之通信的 renderer WebContents 对象。
- ipcMain: Electron 中的 IpcMain 对象。
- messageChannelConstructor: MessageChannel 的构造器,在主进程使用 MessageChannelMain。
- channelName: IPC channel 标识,默认为
__COMLINK_MESSAGE_CHANNEL__
,可以通过 channelName 创建多对 comlink endpoint。
// main.ts
import { ipcMain, MessageChannelMain } from 'electron';
import { expose } from 'comlink';
import { electronMainEndpoint } from 'comlink-adapters';
import type { WebContents, IpcMainEvent } from 'electron';
const add = (a: number, b: number) => a + b;
const senderWeakMap = new WeakMap<WebContents, boolean>();
const ackMessage = (sender: WebContents) => {
sender.postMessage('init-comlink-endponit:ack', null);
};
ipcMain.on('init-comlink-endponit:syn', (event: IpcMainEvent) => {
if (senderWeakMap.has(event.sender)) {
ackMessage(event.sender);
return;
}
// expose add function
expose(
add,
electronMainEndpoint({
ipcMain,
messageChannelConstructor: MessageChannelMain,
sender: event.sender,
})
);
ackMessage(event.sender);
senderWeakMap.set(event.sender, true);
});
electronRendererEndpoint:
interface ElectronRendererEndpointOptions {
ipcRenderer: IpcRenderer;
channelName?: string;
}
interface electronRendererEndpoint {
(options: ElectronRendererEndpointOptions): Endpoint;
}
- ipcRenderer: Electron 中的 IpcRenderer 对象。
- channelName: IPC channel 标识。
// renderer.ts
import { ipcRenderer } from 'electron';
import { wrap } from 'comlink';
import { electronRendererEndpoint } from 'comlink-adapters';
import type { Remote } from 'comlink';
type Add = (a: number, b: number) => number;
(async function () {
const useRemoteAdd = () => {
return new Promise<Remote<Add>>((resolve) => {
ipcRenderer.on('init-comlink-endponit:ack', () => {
resolve(wrap<Add>(electronRendererEndpoint({ ipcRenderer })));
});
ipcRenderer.postMessage('init-comlink-endponit:syn', null);
});
};
const remoteAdd = await useRemoteAdd();
const sum = await remoteAdd(1, 2);
// output: 3
})();
Adapters:
figmaCoreEndpoint
用于 Figma 沙箱中主线程创建Endpoint
对象。figmaUIEndpoint
用于 Figma UI 进程创建Endpoint
对象。
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ❌ | await new ProxyObj(); |
Core 线程不支持 MessageChannel,Core 与 UI 线程无法传递 MessagePort |
proxy function | ❌ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
同上 |
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
同上 |
release | ✅ | proxyObj[comlink.releaseProxy](); |
figmaCoreEndpoint:
interface FigmaCoreEndpointOptions {
origin?: string;
checkProps?: (props: OnMessageProperties) => boolean | Promise<boolean>;
}
interface figmaCoreEndpoint {
(options: FigmaCoreEndpointOptions): Endpoint;
}
- origin: figma.ui.postMessage 的
origin
配置,默认为*
。 - checkProps: 用于检查 figma.ui.on('message', (msg, props) => {}) 返回
props
中的origin
来源。
// core.ts
import { expose } from 'comlink';
import { figmaCoreEndpoint } from 'comlink-adapters';
expose((a: number, b: number) => a + b, figmaCoreEndpoint());
figmaUIEndpoint:
interface FigmaUIEndpointOptions {
origin?: string;
}
interface figmaUIEndpoint {
(options: FigmaUIEndpointOptions): Endpoint;
}
- origin: UI iframe 中 window:postMessage 的
targetOrigin
配置,默认为*
// ui.ts
import { wrap } from 'comlink';
import { figmaUIEndpoint } from 'comlink-adapters';
(async function () {
const add = wrap<(a: number, b: number) => number>(figmaUIEndpoint());
const sum = await add(1, 2);
// output: 3
})();
Adapters:
chromeRuntimePortEndpoint
用于扩展基于长会话创建Endpoint
对象。chromeRuntimeMessageEndpoint
用于扩展基于简单一次性请求创建Endpoint
对象。
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ❌ | await new ProxyObj(); |
API 接口不支持传递 MessagePort |
proxy function | ❌ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
同上 |
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
同上 |
release | ✅ | proxyObj[comlink.releaseProxy](); |
Chrome Extensions 中的通信形式主要为两种,长会话与简单一次性请求,就 comlink 使用来说更推荐长会话,其更简单也更便于理解。注意在使用扩展之间通信时需要先在 manifest.json
配置 externally_connectable。
chromeRuntimePortEndpoint:
interface chromeRuntimePortEndpoint {
(port: chrome.runtime.Port): Endpoint;
}
port runtime.connect 或 tabs.connect 创建的 Port
对象。
扩展内部消息调用,前台页面调用背景页面:
// front.ts (content scripts/popup page/options page)
import { wrap } from 'comlink';
import { chromeRuntimePortEndpoint } from 'comlink-adapters';
(async function () {
const port = chrome.runtime.connect({
name: 'comlink-message-channel',
});
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimePortEndpoint(port)
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// background.ts
import { expose } from 'comlink';
import { chromeRuntimePortEndpoint } from 'comlink-adapters';
chrome.runtime.onConnect.addListener(function (port) {
if (port.name === 'comlink-message-channel') {
expose(
(a: number, b: number) => a + b,
chromeRuntimePortEndpoint(port)
);
}
});
扩展之间相互通信:
// extension A background
import { wrap } from 'comlink';
import { chromeRuntimePortEndpoint } from 'comlink-adapters';
(async function () {
const targetExtensionId = 'B Extension ID';
const port = chrome.runtime.connect(targetExtensionId, {
name: 'comlink-message-channel',
});
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimePortEndpoint(port)
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// extension B background
import { expose } from 'comlink';
import { chromeRuntimePortEndpoint } from 'comlink-adapters';
chrome.runtime.onConnectExternal.addListener((port) => {
if (port.name === 'comlink-message-channel') {
expose(
(a: number, b: number) => a + b,
chromeRuntimePortEndpoint(port)
);
}
});
chromeRuntimeMessageEndpoint:
interface chromeRuntimeMessageEndpoint {
(options?: { tabId?: number; extensionId?: string }): Endpoint;
}
- tabId 与之通信的页面 tab id
- extensionId 与之通信扩展 id
如果不提供 tabId
和 extensionId
则表明时插件的内部页面间通信。
插件内部页面与背景页通信:
// popup page/options page
import { wrap } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
(async function () {
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimeMessageEndpoint()
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// background
import { expose } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
expose((a: number, b: number) => a + b, chromeRuntimeMessageEndpoint());
Content Scripts 与背景页通信:
// content scripts
import { wrap } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
(async function () {
await chrome.runtime.sendMessage('create-expose-endpoint');
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimeMessageEndpoint()
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// background
import { expose } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message === 'create-expose-endpoint') {
expose(
(a: number, b: number) => a + b,
chromeRuntimeMessageEndpoint({ tabId: sender.tab?.id })
);
sendResponse();
return true;
}
sendResponse();
return true;
});
扩展之间相互通信:
// extension A background
import { wrap } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
(async function () {
const targetExtensionID = 'B Extension ID';
chrome.runtime.sendMessage(targetExtensionID, 'create-expose-endpoint');
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimeMessageEndpoint()
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// extension B background
import { expose } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
chrome.runtime.onMessageExternal.addListener(
(message, sender, sendResponse) => {
if (message === 'create-expose-endpoint') {
expose(
(a: number, b: number) => a + b,
chromeRuntimeMessageEndpoint({
extensionId: sender.id,
})
);
sendResponse();
return true;
}
sendResponse();
return true;
}
);
Adapters:
nodeProcessEndpoint
用于在 node process 中创建Endpoint
对象。
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ✅ | await new ProxyObj(); |
|
proxy function | ✅ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
|
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
不支持 MessagePort 传递 |
release | ✅ | proxyObj[comlink.releaseProxy](); |
nodeProcessEndpoint:
interface nodeProcessEndpoint {
(options: {
nodeProcess: ChildProcess | NodeJS.Process;
messageChannel?: string;
}): Endpoint;
}
- nodeProcess: node process 或者 node child_process。
- messageChannel: 用于在 process 通信中划分信道,可以通过不同
messageChannel
创建不同的 endpoint,默认为__COMLINK_MESSAGE_CHANNEL__
。
// child.ts
import { nodeProcessEndpoint } from 'comlink-adapters';
import { expose } from 'comlink';
expose(
(a: number, b: number) => a + b,
nodeProcessEndpoint({ nodeProcess: process })
);
// main.ts
import { fork } from 'node:child_process';
import { nodeProcessEndpoint } from 'comlink-adapters';
import { wrap } from 'comlink';
(async function () {
const add = wrap<(a: number, b: number) => number>(
nodeProcessEndpoint({ nodeProcess: fork('child.ts') })
);
const sum = await add(1, 2);
// output: 3
})();
Adapters:
socketIoEndpoint
用于 socket.io 在客户端与服务端创建Endpoint
对象。
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ✅ | await new ProxyObj(); |
|
proxy function | ✅ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
|
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
不支持 MessagePort 传递 |
release | ✅ | proxyObj[comlink.releaseProxy](); |
socketIoEndpoint:
interface SocketIoEndpointOptions {
socket: ServerSocket | ClientSocket;
messageChannel?: string;
}
interface socketIoEndpoint {
(options: SocketIoEndpointOptions): Endpoint;
}
- socket:
socket.io
或socket.io-client
创建的 socket 实例。 - messageChannel: 用于 socket 实例发送/监听 comlink 消息所用事件名称,可以通过不同
messageChannel
创建不同的 endpoint,默认为__COMLINK_MESSAGE_CHANNEL__
。
// server.ts
import Koa from 'koa';
import { createServer } from 'http';
import { Server } from 'socket.io';
import { expose } from 'comlink';
import { socketIoEndpoint } from '@socket/adapters';
const app = new Koa();
const httpServer = createServer(app.callback());
const io = new Server(httpServer, {});
io.on('connection', (socket) => {
expose((a: number, b: number) => a + b, socketIoEndpoint({ socket }));
});
httpServer.listen(3000);
// client.ts
import { io } from 'socket.io-client';
import { wrap } from 'comlink';
import { socketIoEndpoint } from 'comlink-adapters';
(async function () {
const socket = io('ws://localhost:3000');
const add = wrap<(a: number, b: number) => number>(
socketIoEndpoint({ socket })
);
const sum = await add(1, 2);
// output: 3
})();
Adapters:
webSocketEndpoint
用于 WebSocket 创建Endpoint
对象。
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ✅ | await new ProxyObj(); |
|
proxy function | ✅ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
|
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
不支持 MessagePort 传递 |
release | ✅ | proxyObj[comlink.releaseProxy](); |
webSocketEndpoint:
import type { WebSocket as LibWebSocket } from 'ws';
interface webSocketEndpoint {
(options: {
webSocket: WebSocket | LibWebSocket;
messageChannel?: string;
}): Endpoint;
}
- webSocket: webSocket 或 ws 创建的 webSocket 实例。
- messageChannel: 用于在 webSocket 通信中划分信道,可以通过不同
messageChannel
创建不同的 endpoint,默认为__COMLINK_MESSAGE_CHANNEL__
。
// server.ts
import { WebSocketServer } from 'ws';
import { expose } from 'comlink';
import { webSocketEndpoint } from 'comlink-adapters';
const wss = new WebSocketServer({ port: 8888 });
wss.addListener('connection', (ws: WebSocket) => {
expose(
(a: number, b: number) => a + b,
webSocketEndpoint({ webSocket: ws })
);
});
// client.ts
import WebSocket from 'ws';
import { webSocketEndpoint } from 'comlink-adapters';
import { wrap } from 'comlink';
(async function () {
const ws = new WebSocket('ws://localhost:8888');
const add = wrap<(a: number, b: number) => number>(
webSocketEndpoint({ webSocket: ws })
);
const sum = await add(1, 2);
// output: 3
})();
install
pnpm i
dev
cd core
pnpm run dev
cd examples/xxx-demo
pnpm run dev
# or
pnpm -r run dev
build
cd core
pnpm run build