framekit で静的サイトを API 化する

4 min read読了の目安(約3600字

Framekit とは

静的サイトを API 化する実験的なライブラリです。

https://github.com/mizchi/framekit
$ npm install @mizchi/framekit comlink

これは monaco-editor で作られたエディタを操作してるサンプルです。

アイデア

複雑な SPA を同じページ内部で動かそうとすると、主にビルド方法や相対パス周りで問題が発生します。例えば vite と webpack で噛み合わなかったり、monaco-editor のように暗黙の loader を大量に設定する必要がある場合だったり、デプロイされた場所のどういう相対 URL でモジュールを解決するか、です。

このうち、部分的に複雑なコンポーネントを埋め込む場合(例の monaco-editor がそうです)、古の iframe widget のアプローチでこれを解決できるのでは、と考えました。また実際に手元で発生していた問題で、とある画面でコンパイラ一式を埋め込んで rollup のビルド結果をプレビューする、という機能がプロジェクト全体を重くしてしまった、というのがありました。これを isolate したい。

しかし、最近の iframe はサードパーティスクリプト絡みでどんどんセキュリティサンドボックスが厳しくなっていたり、postMessage をベースとしてるのでコントロールが難しい、といった問題が知られています。

これを解決する方法として、 iframe はホストやパーミッションを何も知らない前提で、明示的に RPC で操作するクライントとしてセットで提供する、というアプローチが取れるのでは、と思いつきました。

また、それを使いやすく実装するために comlink で iframe を RPC 化する、という機能を見つけたことで、実際にいけるのでは?と思いつきました

https://github.com/GoogleChromeLabs/comlink/tree/master/docs/examples/99-nonworker-examples/iframes

今回は iframe をターゲットとしましたが、portals を使うと、更に便利なのでは、という発展の余地があります。

Hands-on with Portals: seamless navigation on the web

framekit のワークフロー

  • RPC API を expose した静的サイトを netlify 等にデプロイする
  • その iframe を framekit の client で操作する

Example: Iframe Application

実装例です。動いてるものを見たい場合、ここを参考にしてください。

framekit/examples/example-vite at main · mizchi/framekit

型を宣言します。

Declare api types

// api.d.ts
import type { ApiBase } from "@mizchi/framekit";

export interface Api extends ApiBase {
  init(base: number, callback: (now: number) => void): Promise<void>;
}

その API を complink で実装した iframe を実装します。

// src/index.ts
import type { Api } from "../api";
import { exposeIframe } from "@mizchi/framekit";

export const api: Api = {
  async init(base: number, callback: (now: number) => void) {
    setInterval(() => {
      const delta = Date.now() - base;
      document.body.innerHTML = delta.toString();
      callback(delta);
    }, 1000);
  },
  async __ready__() {
    // required: detect ready
    return true;
  },
  async __standalone__() {
    // required: at host
    api.init(0, console.log);
  },
};

exposeIframe(api);

__ready__ は最初に呼ばれる API です。

__standalone__ は Iframe じゃないときに実行されるエントリポイントです。

これを静的サイトとしてデプロイします。

Deploy static site

$ npm i -g netlify-cli
$ yarn build # generate dist
$ netlify deploy --prod -d dist

使う側の例

デプロイされた URL を iframe を生成し、それを操作する API を呼びます。

中は comlink なので、コールバック関数を渡す際は proxy 化する必要があります。

import { create } from "@mizchi/framekit";
import { proxy } from "comlink";

// refer your api types
import type Api from "../api";

(async () => {
  // create iframe element and renderer
  const { element, render } = await create({
    url: "<your-deployed-url>",
  });
  // set size of iframe
  element.style.width = "160px";
  element.style.height = "40px";
  element.style.outline = "1px solid black";

  // get api instance from result of render
  // it ensures iframe is loaded and connect.
  const api = await render(document.body);

  await api.init(
    // transferrable object
    Date.now(),
    // wrap function with comlink.proxy for function
    proxy((delta) => {
      console.log("delta", delta);
    })
  );
})().catch(console.error);

実際にやってみた感想

  • ビルド構成を切り分けられるのが楽。これがすべて。
  • netlify の DL 速度があんまり早くないので、15MB の JS を初期化するのに 20 秒かかったりしました。キャッシュが効けば早くなりますが、iframe だからかキャッシュが捨てられるのも、どうやら早いみたいです。
  • シングルファイルが問題にならないなら、iframe をハックするより、 webcomponents で提供したほうが楽かも
  • iframe 内のアセットはブラウザにとって優先度が遅いので、重い画面の中に重い画面を埋め込むと、かなり遅い