Comlinkを使ってiframeと通信する
Comlink とは
Comlink は、Web Worker などの異なるコンテキスト間での通信を簡単にするためのライブラリです。
公式では、Web Worker を enjoyable なものにするためのライブラリと紹介されています。
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.
1.1kB
と非常に軽量なのも嬉しいですね。
本来は、postMessage
を通してメッセージを送信する必要がありますが、Comlink を使うことで、以下のサンプルのように、あたかも別のコンテキストの関数を実行しているかのように扱うことができます。
サンプル
// worker.ts
import * as Comlink from "comlink";
const handlers = {
add: (a: number, b: number) => a + b,
};
Comlink.expose(handlers);
// main.ts
import * as Comlink from "comlink";
const handlers = Comlink.wrap(new Worker("worker.js"));
// worker.ts で定義した関数を呼び出したかのように使用できる
const result = await handlers.add(1, 2);
console.log(result); // 3
expose
でオブジェクトを公開し、wrap
で expose
したオブジェクトを取得します。
wrap
で取得したオブジェクトは全て非同期になります。
本来 Comlink は、Web Worker 間で使用することを想定していますが、iframe 間でも同様に使用することができます。
iframe 間で Comlink を使う
今回の本題です。
基本的な使い方は同じですが、iframe など他のウィンドウと連携する場合は、次のようにwindowEndpoint
を活用します。
// main.ts
import * as Comlink from "comlink";
const handlers = Comlink.wrap(Comlink.windowEndpoint(iframe.contentWindow));
// iframe.ts
import * as Comlink from "comlink";
const handlers = {
add: (a: number, b: number) => a + b,
};
Comlink.expose(handlers, Comlink.windowEndpoint(self.parent));
iframe 側では、Web Worker のときと同様にexpose
を行い関数を受け取れるようにします。
こちら側でも、同様windowEndpoint
を使用します。
windowEndpoint
の詳細は、以下のドキュメントを参照してください。
オリジンが異なる場合
もし、iframe のオリジンが異なる場合は、 targetOrigin
を指定します。
// main.ts
import * as Comlink from "comlink";
const handlers = Comlink.wrap(
Comlink.windowEndpoint(iframe.contentWindow, self, targetOrigin)
);
型をつける
expose
するオブジェクトには型をつけることができます。
iframe 側を別のリポジトリで開発するときなどは、 turborepo
などのモノレポで開発して、型を共有すると便利です。
// packages/schema/types.ts
type Handlers = {
add: (a: number, b: number) => Promise<number>;
};
// iframe.ts
import * as Comlink from "comlink";
const handlers: Handlers = {
add: (a: number, b: number) => Promise<number>;
};
Comlink.expose(handlers);
// main.ts
import * as Comlink from "comlink";
const handlers: Comlink.Remote<Handlers> = Comlink.wrap<Handlers>(
Comlink.windowEndpoint(iframe.contentWindow)
);
wrap
したオブジェクトは、Remote<T>
型になり、
wrap
したオブジェクトは、非同期になります。
React で Comlink を使う
ほぼ同じですが、wrap
したオブジェクトを React の useRef
で管理します。
// main.tsx
import { useRef } from "react";
import * as Comlink from "comlink";
import type { Handlers } from "@/packages/types";
function Example() {
const workerRef = useRef<Comlink.Remote<Handlers> | null>(null);
const handleOnLoad = (e: React.SyntheticEvent<HTMLIFrameElement>) => {
const iframe = e.target;
if (!iframe) return;
const contentWindow = iframe.contentWindow;
if (!contentWindow) return;
const worker = Comlink.wrap<Handlers>(
Comlink.windowEndpoint(contentWindow)
);
workerRef.current = worker;
};
return <iframe onLoad={handleOnLoad} />;
}
// iframe.tsx
import * as Comlink from "comlink";
const handlers = {
add: (a: number, b: number) => a + b,
};
Comlink.expose(handlers);
onLoad
イベントで workerRef.current
に wrap
したオブジェクトをセットします。
これで、他の箇所でも workerRef.current
を使用して wrap
したオブジェクトを扱うことができます。
expose
する
親側から iframe に 上記のサンプルでは、iframe 側で expose
したオブジェクトを親側で wrap
していました。
反対に、親側から iframe に expose
することもできます。
// main.tsx
import { useRef } from "react";
import * as Comlink from "comlink";
import type { Handlers } from "@/packages/types";
function Example() {
const getApiData = async (key: string) => {
// データをCMSなどから取得
const data = await getCMSData(key);
return data;
};
const handleOnLoad = (e: React.SyntheticEvent<HTMLIFrameElement>) => {
const iframe = e.target;
if (!iframe) return;
const contentWindow = iframe.contentWindow;
if (!contentWindow) return;
// expose する関数の一覧を定義
const parentHandlers = {
getApiData,
};
Comlink.expose(parentHandlers, Comlink.windowEndpoint(contentWindow));
};
return <iframe onLoad={handleOnLoad} />;
}
// iframe.ts
import * as Comlink from "comlink";
const parentHandlers = Comlink.wrap(Comlink.windowEndpoint(self.parent));
const result = await parentHandlers.getApiData("/api/data");
このように、親側で定義した関数を iframe 側で使用することができます。
iframe 側から親側の関数を呼び出すことができるので、postMessage
で実装するのが大変だった処理も簡単に実装できます。
Comlink.Proxy を使う
Comlink では、シリアライズ可能なデータ のみ送信できます。
シリアライズできないデータを送信しようとすると、エラーが発生します。
正確な挙動は、Comlink の GitHub 上に記載されています。
GitHub - Comlink/structured-clone-table.md
MDN では、シリアライズ可能なデータの一覧が記載されています。
シリアライズできないデータを送信する場合は、Comlink.Proxy を使ってシリアライズ可能なデータに変換します。
// iframe.ts
import * as Comlink from "comlink";
type Handlers = {
callFunction: (fn: () => void) => void;
};
const handlers: Handlers = {
callFunction: (fn) => {
fn();
},
};
Comlink.expose(handlers);
// main.tsx
const handleOnClick = () => {
const fuga = () => {
console.log("fuga");
};
workerRef.current?.callFunction(Comlink.proxy(fuga));
};
上記のように、Comlink.proxy
を使ってシリアライズ可能なデータに変換します。
まとめ
Comlink を使うことで、iframe 間での通信を簡単に行うことができます。
それでは、良い Comlink ライフを!
参考
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion
これ最後の こうでは?
同様の問題が他にもありそうだが。
ありがとうございます!