Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ProxyによるWindow間RPC機構の構築
Search
syumai
September 05, 2025
Programming
1
190
ProxyによるWindow間RPC機構の構築
フロントエンドカンファレンス北海道2025 (2025/9/5)の発表資料です。
サンプル:
https://github.com/syumai/easy-admin-demo
syumai
September 05, 2025
Tweet
Share
More Decks by syumai
See All by syumai
CloudflareのChat Agent Starter Kitで簡単!AIチャットボット構築
syumai
1
290
Go製CLIツールをnpmで配布するには
syumai
2
1.3k
MCPで実現できる、Webサービス利用体験について
syumai
7
2.7k
GoのGenericsによるslice操作との付き合い方
syumai
3
870
GoのWebAssembly活用パターン紹介
syumai
3
11k
Cloudflare Workersで進めるリモートMCP活用
syumai
13
2.6k
Go 1.24でジェネリックになった型エイリアスの紹介
syumai
2
650
StarlingMonkeyを触ってみた話 - 2024冬
syumai
3
450
初めてDefinitelyTypedにPRを出した話
syumai
1
740
Other Decks in Programming
See All in Programming
RDoc meets YARD
okuramasafumi
4
160
AIを活用し、今後に備えるための技術知識 / Basic Knowledge to Utilize AI
kishida
13
3.5k
250830 IaCの選定~AWS SAMのLambdaをECSに乗り換えたときの備忘録~
east_takumi
0
350
パッケージ設計の黒魔術/Kyoto.go#63
lufia
3
390
2025 年のコーディングエージェントの現在地とエンジニアの仕事の変化について
azukiazusa1
2
310
Improving my own Ruby thereafter
sisshiki1969
1
140
サイトを作ったらNFCタグキーホルダーを爆速で作れ!
yuukis
0
740
MLH State of the League: 2026 Season
theycallmeswift
0
210
AIコーディングAgentとの向き合い方
eycjur
0
250
KessokuでDIでもgoroutineを活用する / Go Connect #6
mazrean
0
130
LLMOpsのパフォーマンスを支える技術と現場で実践した改善
po3rin
8
1k
SOCI Index Manifest v2が出たので調べてみた / Introduction to SOCI Index Manifest v2
tkikuc
1
120
Featured
See All Featured
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.5k
Typedesign – Prime Four
hannesfritz
42
2.8k
How GitHub (no longer) Works
holman
315
140k
Exploring the Power of Turbo Streams & Action Cable | RailsConf2023
kevinliebholz
34
6k
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
8
510
Building Flexible Design Systems
yeseniaperezcruz
328
39k
Automating Front-end Workflow
addyosmani
1370
200k
XXLCSS - How to scale CSS and keep your sanity
sugarenia
248
1.3M
The Pragmatic Product Professional
lauravandoore
36
6.8k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
358
30k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
31
2.2k
Transcript
Proxy によるWindow 間RPC 機構の構築 syumai フロントエンドカンファレンス北海道2025 (2025/9/5)
自己紹介 syumai ECMAScript 仕様輪読会 / Asakusa.go 主催 株式会社ベースマキナで管理画面のSaaS を開発中 Go
でGraphQL サーバー (gqlgen) や TypeScript でフロン トエンドを書いています Software Design 2023 年12 月号から2025 年2 月号まで Cloudflare Workers の連載をしました Twitter ( 現𝕏): @__syumai Website: https://syum.ai
本日話すこと JavaScript のProxy の基本 Window 間通信の基本 別Window 上のメソッド呼び出しのProxy による実装 Window
間通信のProxy によるWrap 方法 別Window 上のメソッドの型情報の利用 Comlink の簡単な紹介
Proxy の基本
JavaScript のProxy とは? Object に対する操作に割り込める特別なclass プロパティアクセスなど 以下の2 つをProxy のコンストラクタに渡して使う 操作の割り込み対象のObject
どう割り込むかを指定するhandler
例1 - プロパティの値の操作 // 操作の割り込み対象のObject const obj = { a:
1, b: 2, };
例1 - プロパティの値の操作 // プロパティの取得操作に割り込み、値を2倍するhandler const doubleHandler = { get(target,
prop) { const value = target[prop]; return value * 2; } };
例1 - プロパティの値の操作 const doubleProxy = new Proxy(obj, doubleHandler); //
2倍された値がプロパティから取れる console.log(doubleProxy.a); // 2 console.log(doubleProxy.b); // 4
例2 - 存在しないプロパティの取得 // プロパティのないオブジェクト const obj = {}; const
proxy = new Proxy(obj, { // プロパティの取得操作に割り込み、プロパティ名をそのまま値として返す get(_, prop) { return prop; } }) // 存在しないプロパティの値が取れる console.log(proxy.a); // a console.log(proxy.b); // b
Proxy にできること Object に対する操作への割り込み Object に対する操作を行った時の振る舞いが決まっている ここでは、これをObject の振る舞いと呼ぶことにします -> Object
の振る舞いはどう決まるのか?
「Object の振る舞い」はどう決まるのか? ECMAScript の仕様上で、Object の内部メソッド (internal method) として定義されて いる 全てのObject
は、この内部メソッドを実装している
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy より引用
Object の分類 全てのObject は2 つの種類に分類できる 1. ordinary object あらゆるObject が基本的にはこれ
2. exotic object Array などの、独自定義の内部メソッドを持つObject がこれ Array は [[DefineOwnProperty]] の動作が特殊 Proxy は、ユーザー定義のexotic object を作る機能 これらの用語はECMAScript 仕様にも頻繁に現れます
Window 間通信の基本
Window 間通信とは? Web アプリケーション上に現れる複数のWindow Object 間での値のやり取り
Window の参照の取得方法 iframe, window.open などで得られる 参照取得方法の例 経路 親 => 子
子 => 親 iframe iframe.contentWindow window.parent window.open(url) open 関数の返り値 window.opener <a ... target="_blank"> - window.opener
Window 間での値の受け渡し方法 以下の2 つがある 1. プロパティアクセス Window Object のグローバルなプロパティを介して値を受け渡す 例:
window.parent.a = 1 2. postMessage グローバルなプロパティを直接使わずメッセージ送信する 例: window.parent.postMessage(1)
Window 間での値の受け渡し方法 使えるシーンが異なる 1. プロパティアクセス Origin が同じ場合だけ可能 2. postMessage Origin
が異なる場合も可能 → ほとんどのユースケースでこちらが使われる
補足: Origin とは eiji さんの記事より Origin は scheme (http とか https
とか) 、hostname 、port の組み合わせを指す。 same-origin と言った場合、これらすべてが一致するものを示している same-site/cross-site, same-origin/cross-origin をちゃんと理解する: https://zenn.dev/agektmr/articles/f8dcd345a88c97
Window 間通信の主なユースケース Origin 跨ぎでの利用が基本 Web サイトへの埋め込みウィジェット iframe sandbox ユーザー入力スクリプトの実行
microCMS さんの例 https://document.microcms.io/manual/field-extension より引用
iframe sandbox については、以前別の勉強会で話しました
postMessage の使い方 受け手側 自身のWindow Object に message イベントリスナーを設定する 送り手側 送り先のWindow
Object の postMessage メソッドを呼ぶ
postMessage の利用例 受け手側 window.addEventListener("message", (event) => { console.log(event.data); }); 送り手側
// iframeの親にメッセージを送る window.parent.postMessage("Hello from child window!");
転送できる値の種類 構造化複製アルゴリズム (structured clone) で複製できるものだけが対象 抜粋 Symbol 以外の全てのプリミティブ型 Array Object
( プレーンなオブジェクトのみ) Date Blob File Map Set ネイティブの Error 型
転送できない値の種類 Function Object は複製できない 独自定義のclass を含む Error class を継承して作った独自エラーなども渡せない 例えば、iframe
で作った関数を親Window に送って実行、などとするのは postMessage では不可能
具体例
例: 管理画面SaaS ( 仮称: Easy Admin) の組み込み画面 Easy Admin は、顧客がリッチな画面を構築するために、自由に画面を組み立てて、
iframe で組み込む機能を持っている Easy Admin は、公開API を持っていないが、iframe 内からサービス固有の機能を呼び 出すためのSDK を提供している 例: ログイン中ユーザー情報の取得、DB へのクエリ実行など ( 注: 似ていますが、ベースマキナの話ではないです。また、Easy Admin ( 仮称) と同名 のプロダクトがあったとしても無関係です)
Easy Admin の画面のモック ( 右側が顧客の実装した「組み込み画面」 )
例: 「ログイン中ユーザー情報の取得」機能の実装 顧客が、Easy Admin にログインしているユーザー情報を表示する画面を実装する場 合について検討する 利用イメージ 顧客は @easy-admin/sdk をインストールし、そこから
client をimport して使う ログイン中ユーザー情報の取得は client.getCurrentUser で呼び出す 画面はReact で構築する
例: 「ログイン中ユーザー情報の取得」機能の実装 最終形としてこの形を目指す type Client = { getCurrentUser: () =>
Promise<User> // ... その他のメソッド }
顧客側の実装イメージ import { client } from "@easy-admin/sdk"; const UserCard =
() => { // ログイン中ユーザー情報を取得する const user = use(client.getCurrentUser); return ( // ユーザー情報を表示するUI ); }; const App = () => { return ( <Suspense fallback={<p>読み込み中です...</p>}> <UserCard /> </Suspense> ); }; export default App;
実装
実装方針 顧客が実装するページであるiframe 側のWindow をクライアント、Easy Admin 側の Window をサーバーと見立てて、クライアント・サーバー風の作りにします iframe ->
Easy Admin がリクエスト Easy Admin -> iframe がレスポンス まずは顧客が生でpostMessage を使う形にして、最後にpostMessage を隠蔽した関数 に改良し、SDK から配布する形にします 最終的には、 getCurrentUser 以外の処理呼び出しにも対応します
実装ステップの紹介における注意点 リクエスト / レスポンスのメッセージなどの型定義情報は、Easy Admin と顧客がそ れぞれ自身で定義を持っている ( 重複する) 想定で話を進めます
また、ところどころ省略もしています
実装ステップ 1. iframe からEasy Admin へのgetCurrentUser のリクエスト送信 2. Easy Admin
のiframe からのgetCurrentUser のリクエスト受信 3. Easy Admin からiframe へのgetCurrentUser のレスポンス送信 4. iframe のEasy Admin からのgetCurrentUser のレスポンス受信 5. ( 安全機構の追加) 6. postMessage 呼び出しの隠蔽
None
実装ステップ 1. iframe からEasy Admin へのgetCurrentUser のリクエスト送信 2. Easy Admin
のiframe からのgetCurrentUser のリクエスト受信 3. Easy Admin からiframe へのgetCurrentUser のレスポンス送信 4. iframe のEasy Admin からのgetCurrentUser のレスポンス受信 5. ( 安全機構の追加) 6. postMessage 呼び出しの隠蔽
1. iframe からEasy Admin へのgetCurrentUser のリクエスト送信 まずは、顧客が自分でpostMessage 呼び出しを書く形式での実装を行う 顧客のコード上に、 getCurrentUser
関数を実装していく type EasyAdminRequest = { operation: "getCurrentUser" } // 今は1種類 const getCurrentUser = () => { // リクエストを組み立てる const reqMsg = { operation: "getCurrentUser" } satisfies EasyAdminRequest; // postMessageでiframeからEasy Adminにリクエストを送る window.parent.postMessage(reqMsg); // TODO: レスポンスの受信処理 };
実装ステップ 1. iframe からEasy Admin へのgetCurrentUser のリクエスト送信 2. Easy Admin
のiframe からのgetCurrentUser のリクエスト受信 3. Easy Admin からiframe へのgetCurrentUser のレスポンス送信 4. iframe のEasy Admin からのgetCurrentUser のレスポンス受信 5. ( 安全機構の追加) 6. postMessage 呼び出しの隠蔽
2. Easy Admin のiframe からのgetCurrentUser のリクエスト受信 Easy Admin は、自身のWindow Object
に message イベントハンドラーを設定してリ クエストを待ち受けます window.addEventListener("message", (event) => { const reqMsg = event.data as EasyAdminRequest; // TODO: レスポンスの送信処理 });
実装ステップ 1. iframe からEasy Admin へのgetCurrentUser のリクエスト送信 2. Easy Admin
のiframe からのgetCurrentUser のリクエスト受信 3. Easy Admin からiframe へのgetCurrentUser のレスポンス送信 4. iframe のEasy Admin からのgetCurrentUser のレスポンス受信 5. ( 安全機構の追加) 6. postMessage 呼び出しの隠蔽
3. Easy Admin からiframe へのgetCurrentUser のレスポンス送信 Easy Admin は、iframe のWindow
Object へpostMessage でレスポンスを送信します window.addEventListener("message", async (event) => { const reqMsg = event.data as EasyAdminRequest; // Easy Admin側のgetCurrentUser関数を呼んで、ユーザー情報を取得する const user = await getCurrentUser(); const resMsg = { operation: "getCurrentUser", payload: user, } satisfies EasyAdminResponse; // iframeにレスポンスを送る iframe.contentWindow.postMessage(resMsg); });
実装ステップ 1. iframe からEasy Admin へのgetCurrentUser のリクエスト送信 2. Easy Admin
のiframe からのgetCurrentUser のリクエスト受信 3. Easy Admin からiframe へのgetCurrentUser のレスポンス送信 4. iframe のEasy Admin からのgetCurrentUser のレスポンス受信 5. ( 安全機構の追加) 6. postMessage 呼び出しの隠蔽
4. iframe のEasy Admin からのgetCurrentUser のレスポンス受信 iframe 側は、自身のWindow Object に
message イベントハンドラーを設定してレス ポンスを待ち受けます 現時点では、 message イベントハンドラーは、関数呼び出しの度に登録し、関 数呼び出し完了時に登録解除する const getCurrentUser = () => { const handler = (event) => { const resMsg = event.data as EasyAdminResponse; // TODO: ユーザー情報を返す // 呼び出し完了時に登録解除する window.removeEventListener("message", handler); } window.addEventListener("message", handler); // ... };
4. iframe のEasy Admin からのgetCurrentUser のレスポンス受信 ここでpostMessage のやり取りをPromise でWrap する
利用者視点で、ただのasync function に見えるようになる const getCurrentUser = () => { return new Promise((resolve) => { const handler = (event) => { const resMsg = event.data as EasyAdminResponse; // ユーザー情報でresolveする resolve(resMsg.payload); window.removeEventListener("message", handler); } window.addEventListener("message", handler); // リクエスト送信処理もPromise内に入れる (messageイベントリスナー設定後に送信する必要があるため) }); };
4. iframe のEasy Admin からのgetCurrentUser のレスポンス受信 ステップ4 時点での最終形です Easy Admin
とiframe の間のリクエスト・レスポンスは送り合えるようになった const getCurrentUser = () => { return new Promise((resolve) => { const handler = (event) => { const resMsg = event.data as EasyAdminResponse; resolve(resMsg.payload); window.removeEventListener("message", handler); } window.addEventListener("message", handler); const reqMsg = { operation: "getCurrentUser" } satisfies EasyAdminRequest; window.parent.postMessage(reqMsg); }); };
5. 安全機構の追加 実は、今のままの実装だとあまり安全ではない 意図しない情報が混ざってしまうことがある ( あまり深入りしません)
5. 安全機構の追加 - メッセージへのID 付与 今の実装だと、 getCurrentUser の結果が呼び出し順に返ることが保証されない リクエスト・レスポンスの両方に呼び出しの対応関係を保証するID を付ける
( crypto.randomUUID() などを使う) type EasyAdminRequest = { id: string; operation: "getCurrentUser" } type EasyAdminResponse = { id: string; operation: "getCurrentUser"; payload: User; };
5. 安全機構の追加 - 送信先Origin の指定 postMessage は、送信先Origin を指定できる iframe 側は、Easy
Admin 以外のサイトに埋め込まれた時に、メッセージ内容を 読まれたくない // Easy AdminのOriginにだけメッセージを送る! window.parent.postMessage(reqMsg, easyAdminOrigin);
5. 安全機構の追加 - 送信元のバリデーション postMessage で送られてきたevent には、送信元の情報が含まれる 送信元Window Object 送信元Origin
Easy Admin 側は、意図したiframe から以外の送信を弾くべき const iframe = document.getElementById("client-app"); window.addEventListener("message", async (event) => { if (event.source !== iframe.contentWindow) { return; } if (event.origin !== clientAppOrigin) { return; } const reqMsg = event.data as EasyAdminRequest; // ...
6. postMessage 呼び出しの隠蔽 先ほどの getCurrentUser 実装を @easy-admin/sdk に持っていってexport すれば 完了!
export const getCurrentUser = ...
現状の実装の課題
現状の実装の課題 現時点では1 関数のみなので特に課題はないが、すぐに関数の追加が大変という問題に直 面する
関数の追加が大変 例えば「ユーザー一覧の取得 (listUsers) 」に対応するのを考える
関数の追加が大変 呼び出せる関数の種類が増える度に型定義が増える type EasyAdminRequest = ( { operation: "getCurrentUser" }
| { operation: "listUsers" // (NEW!) } ) & { id: string; } type EasyAdminResponse = ( { operation: "getCurrentUser"; payload: User; } | { operation: "listUsers"; // (NEW!) payload: User[]; } ) & { id: string; };
関数の追加が大変 Easy Admin 側のswitch のケースも都度増える window.addEventListener("message", async (event) => {
const reqMsg = event.data as EasyAdminRequest; let resMsg: EasyAdminResponse; switch (reqMsg.operation) { // operationの種類が増えるとswitchが必要 case: "getCurrentUser": const user = await getCurrentUser(); resMsg = { operation: "getCurrentUser", payload: user, }; break; case: "listUsers": // (NEW!) const users = await listUsers(); // ... } iframe.contentWindow.postMessage(resMsg); });
本当にやりたかったのは… Easy Admin 側のこれらの関数を公開したかっただけでは? 別の言い方をすると、別Window の関数をRPC として呼び出したかったという事 const user =
await getCurrentUser(); const users = await listUsers(); → Proxy で解決!
関数呼び出しのProxy 化 まず、Easy Admin 側から公開したい関数を、1 つのObject に束ね、型情報だけexport する const builtInFuncs
= { getCurrentUser, listUsers, } as const; type BuiltInFuncs = typeof builtInFuncs;
関数呼び出しのProxy 化 iframe 側は、 get に割り込むProxy Object を作り、プロパティアクセスに対して postMessage のやり取りをする関数を返すようにする
const client = new Proxy({}, { get(_, prop) { // プロパティ名が、そのままEasy Admin側の呼びたいメソッド名になる! createRequestSender({ operation: prop }); } });
関数呼び出しのProxy 化 リクエストも、レスポンスも、形式がまとまる const createRequestSender = ({ operation }: {
operation: string }) => // 引数に渡った値をそのままEasy Adminに送る (...payload) => new Promise((resolve) => { const handler = (event) => { const resMsg = event.data; resolve(resMsg.payload); // ... } // ... const reqMsg = { id, operation, payload }; // リクエストの形式が1種類になった! window.parent.postMessage(reqMsg); });
関数呼び出しのProxy 化 Proxy のObject は、 BuiltInFuncs 型のProxy としてexport する //
`BuiltInFuncs` を型引数に指定する const client = new Proxy<BuiltInFuncs>({}, { get(_, prop) { createRequestSender(prop); } });
関数呼び出しのProxy 化 Easy Admin 側は、switch を書かなくてよくなり、非常にシンプルになる window.addEventListener("message", async (event) =>
{ const reqMsg = event.data; // 指定された関数を取得して呼び出す const func = builtInFuncs[reqMsg.operation]; const payload = func(...reqMsg.payload); const resMsg = { operation: reqMsg.operation, payload, }; iframe.contentWindow.postMessage(resMsg); });
最初の実装イメージの形になった! import { client } from "@easy-admin/sdk"; const UserCard =
() => { const user = use(client.getCurrentUser); // ... };
機能改善 - エラーハンドリング Easy Admin 側での関数実行でthrow されたError をiframe でcatch したい
Easy Admin 側で catch して、Error も値としてレスポンスに含めて返す そして、 iframe 側でError を再度throw する const func = builtInFuncs[reqMsg.operation]; let resMsg; try { const payload = func(...reqMsg.payload); resMsg = { operation: reqMsg.operation, payload, }; } catch(error) { resMsg = { operation: reqMsg.operation, error, }; }
ここまでのまとめ Window 間通信にはpostMessage を使う 素の実装だと、RPC 呼び出しの実装は手数が多く大変 Proxy を使うことで、postMessage を隠蔽したシンプルなRPC 実装が可能
これを毎回ゼロから実装する必要があるのか?
None
Comlink を使えばOK !
https://github.com/GoogleChromeLabs/comlink
None
None
Comlink もProxy で作られている
throw された値をメッセージに含めて返すなどの点も同じ
Comlink Web Worker だけでなく、iframe との通信に使うアダプターも提供されている 値の変換が TransferHandler として抽象化されているので、構造化複製アルゴリズ ムに対応していない値の変換も可能 Window
間RPC の実装におすすめです
Comlink ブラウザ以外の事例もあります!
Comlink でRN<->WebView 間通信: https://zenn.dev/ubie_dev/articles/e720224828aa43
最後に Proxy はRPC の実装に活用可能 自前実装しなくても、Comlink で事足りるシーンも多い Window 間通信以外にも幅広く使えるポテンシャルがある機能
ご清聴ありがとうございました!
補足 今回の資料の実装では、async function 以外の関数公開がうまくできない 同期関数も、async function に変換するような実装や、型の操作が必要 発表スライドを元にClaude に実装してもらったサンプル実装が以下です https://github.com/syumai/easy-admin-demo
雰囲気は伝わると思います!