OSSTech株式会社

SvelteKit アプリケーションの組み込みへの考察

2023-12-08 - 森本 哲也

先日 SvelteKit のコンパイルの仕組み について調査しました。そのときに Vite というビルドツールによって ES モジュールをインポートする形でページが構成されていることもわかりました。Vite の バックエンドとの統合 というドキュメントには従来のバックエンドに Vite アプリケーションを組み込む方法について説明されています。

SvelteKit アプリケーションも Vite アプリケーションの1つです。エンドポイントのパスやエントリーポイントを調整することで組み込みできないか?というのが本稿の主題です。

マイクロフロントエンドの動機づけ

考察を始める前に マイクロフロントエンド というキーワードもよくみかけるので一緒にみておきましょう。マイクロサービスのフロントエンド版という、大雑把な理解でもそれほど間違っていないと思えますが、メルカリ社の記事がわかりやすかったので紹介します。

この記事ではマイクロフロントエンドを成立させるために3つの原則があると説明されています。

  1. 技術: それぞれのチームの技術が他のチームに影響を与えない
  2. コードやルール: コードは共有せず、コーディングルールも独立している
  3. デプロイ: チームがそれぞれ独立してデプロイできる

Microservices と大きく違うのは、Micro Frontends では UI や ビルド、バンドルという概念と、最終的に相当数の Microservices を1つの画面に組み上げる必要があるという点です。

そして、マイクロサービスとマイクロフロントエンドの大きな違いの1つとして API を介して協調するマイクロサービスとは異なり、マイクロフロントエンドは1つの画面を組み上げるというところを言及しています。後述しますが、これは本稿でも実際にやってみてかなり難しいというのがわかってきました。

もともと私が SvelteKit アプリケーションを組み込みできないかと考えたのは、カスタムテンプレートという要件に対して、機能とデザインという目的の違うアプリケーションを別々に管理できた方が開発にとって都合がよいと考えたからでした。マイクロフロントエンドはまさにそういった概念と相性がよさそうに思います。

SvelteKit アプリケーションのレンダリング手法と組み込み

Unicorn Cloud ID Manager (以下UCIDM) の管理画面は次のように BFF (Backend For Frontend) サーバーに Node.js を配置し、サーバーサイドレンダリングを採用した SvelteKit アプリケーションとして構築されています。

SvelteKit アプリケーションに限らない話ですが、サーバーサイドレンダリングのフロントエンドアプリケーションを組み込みに使うのはとても難しいと推測します。それはサーバーサイドの処理を、他のアプリケーションとどう結合するかという難しいインテグレーションの問題があるからです。そして SvelteKit アプリケーションは、svelte/compiler がルーティングと密接なコンポーネントを vite-plugin-svelte を介して透過的にコンパイルするため、直接的に ES モジュールのインポートやパスを扱うことができません。そういった煩雑なところを SvelteKit が抽象化して開発できるところがメリットでもあるため、これはトレードオフともみなせます。

結論から申し上げて、サーバーサイドレンダリングの SvelteKit アプリケーション を組み込みに使うのは、不可能ではないが、内部の仕組みを把握していないとかなり難しいということがわかりました。現時点では SvelteKit はマイクロフロントエンドのような構想をもっておらず、SvelteKit アプリケーションは1つの独立したシステムとして動作することを前提としています。SvelteKit の開発の簡潔さを活かすのであれば、複数の SvelteKit アプリケーションを結合するよりも、1つの SvelteKit アプリケーションをモノリシックに開発する方がずっと簡単だと言えます。

複数の SvelteKit アプリケーションを同時に動かすための試行錯誤

前節ではやらない方がよいと結論付けたものの、そうは言ってもやってみたい人、私の言うことを信じない人、できないと言われた方が逆にやる気になる人、一緒に考察していきましょう。もし私が考察していないことでうまくいく方法があれば、ぜひ教えてください。私が実際に振る舞いを検証した内容は次のリポジトリにあります。このアプリケーションを使って考察を進めていきます。

サンプルアプリケーションの起動

次のように環境構築して、Node.js でサーバーを起動します。vite の開発サーバーを使うと構成が変わってくるため、ここではすべて本番向けビルドのみで振る舞いを確認します。

$ git clone git@github.com:t2y/sveltekit-apps-integration-sample.git
$ cd sveltekit-apps-integration-sample/
$ npm install
$ cd packages/kit-demo1/
$ npm install
$ npm run build
$ cd ../kit-demo2/
$ npm install
$ npm run build
$ cd ../kit-manager/
$ npm install
$ npm run build
$ node build/index.js
Listening on 0.0.0.0:3000
Listening on 0.0.0.0:3005

このリポジトリには次の3つの SvelteKit アプリケーションがあります。

  • kit-manager: デモアプリを管理するための SvelteKit アプリケーション
  • kit-demo1: SvelteKit のデモアプリ (SSR)
  • kit-demo2: SvelteKit のデモアプリ (SSR を取り除いて SSG にしたもの)

次のような構成になっています。

SvelteKit アプリケーションの本番向けビルドのカスタマイズ

Vite のビルドとは別に、SvelteKit アプリケーションのビルドをフックするところに アダプター があります。デプロイ先のインフラ構成にあわせた成果物の生成を行うことがアダプターの目的となります。

Adapter は次の adapter というメソッドを実装するインターフェースです。このインターフェースに渡される Builder のインスタンスが Vite の成果物を保持しています。

adapt(builder: Builder): MaybePromise<void>;

例えば adapter-node の実装 をみると、Vite の成果物に対して Rollup でバンドルし直していることもわかります。デプロイ先にあわせた都合や最適化のために煩雑な処理を実装するフックポイントにみえます。他のアダプターの実装もいくつかみてみましたが、背景がわかっていない人がみてもあまり内容を把握できるようなモジュールではありませんでした。

前節で「サーバーサイドレンダリングの SvelteKit アプリケーションを組み込みに使うのは不可能ではない」と書いた背景の1つとして、アダプターで自分たちの都合のよい成果物に作り直せばできるのではないかと推測します。しかし、コード生成するためのコードを扱う必要があるため、Vite の manifest.json や vite-plugin-svelte の仕組みを把握していないとなかなか難しいようにもみえました。ひとまずアダプターというフックポイントがあることだけ覚えておきましょう。

本稿では adapter-node でビルドした成果物を結合することを前提に進めます。

SvelteKit アプリケーションのエントリーポイント

サーバーサイドレンダリングが有効な SvelteKit のデモアプリケーションのホームにアクセスすると次のような HTML が返されます。これはビルド時ではなく、Node.js 上のアプリケーションサーバーがリクエストされたときに生成して返しているようです。深く検証できていないので外しているかもしれませんが、おそらくサーバーサイドレンダリングのアプリケーションのエントリーポイントはすべてサーバーサイドでリクエスト時に生成しているのではないかと思います。ここで確認することの1つに、サーバーがないとエントリーポイントの HTML を取得する方法がないという点です。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="./favicon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <link href="./_app/immutable/assets/0.fa9427ff.css" rel="stylesheet" />
    <link href="./_app/immutable/assets/2.57239003.css" rel="stylesheet" />
    <link rel="modulepreload" href="./_app/immutable/entry/start.11cd0a52.js" />
    <link
      rel="modulepreload"
      href="./_app/immutable/chunks/scheduler.cbf234a0.js"
    />
    <link
      rel="modulepreload"
      href="./_app/immutable/chunks/singletons.9fecfaae.js"
    />
    <link
      rel="modulepreload"
      href="./_app/immutable/chunks/index.14349a18.js"
    />
    <link
      rel="modulepreload"
      href="./_app/immutable/chunks/parse.bee59afc.js"
    />
    <link rel="modulepreload" href="./_app/immutable/entry/app.f2f0959d.js" />
    <link
      rel="modulepreload"
      href="./_app/immutable/chunks/index.200976ee.js"
    />
    <link rel="modulepreload" href="./_app/immutable/nodes/0.31390708.js" />
    <link
      rel="modulepreload"
      href="./_app/immutable/chunks/stores.7efe407a.js"
    />
    <link rel="modulepreload" href="./_app/immutable/nodes/2.549c4b9e.js" />
    <title>Home</title>
    <!-- HEAD_svelte-t32ptj_START -->
    <meta name="description" content="Svelte demo app" />
    <!-- HEAD_svelte-t32ptj_END -->
  </head>
  <body data-sveltekit-preload-data="hover">
    <div style="display: contents">
      <div class="app svelte-8o1gnw">
        <header class="svelte-1u9z1tp">
          <div class="corner svelte-1u9z1tp" data-svelte-h="svelte-1jb641n">
            <a href="https://kit.svelte.dev" class="svelte-1u9z1tp"
              ><img
                src="/_app/immutable/assets/svelte-logo.87df40b8.svg"
                alt="SvelteKit"
                class="svelte-1u9z1tp"
            /></a>
          </div>
          <nav class="svelte-1u9z1tp">
            <svg viewBox="0 0 2 3" aria-hidden="true" class="svelte-1u9z1tp">
              <path
                d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z"
                class="svelte-1u9z1tp"
              ></path>
            </svg>
            <ul class="svelte-1u9z1tp">
              <li aria-current="page" class="svelte-1u9z1tp">
                <a href="/" class="svelte-1u9z1tp" data-svelte-h="svelte-5a0zws"
                  >Home</a
                >
              </li>
              <li class="svelte-1u9z1tp">
                <a
                  href="/about"
                  class="svelte-1u9z1tp"
                  data-svelte-h="svelte-iphxk9"
                  >About</a
                >
              </li>
              <li class="svelte-1u9z1tp">
                <a
                  href="/sverdle"
                  class="svelte-1u9z1tp"
                  data-svelte-h="svelte-1mtf8rh"
                  >Sverdle</a
                >
              </li>
            </ul>
            <svg viewBox="0 0 2 3" aria-hidden="true" class="svelte-1u9z1tp">
              <path
                d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z"
                class="svelte-1u9z1tp"
              ></path>
            </svg>
          </nav>
          <div class="corner svelte-1u9z1tp" data-svelte-h="svelte-1gilmbv">
            <a href="https://github.com/sveltejs/kit" class="svelte-1u9z1tp"
              ><img
                src="/_app/immutable/assets/github.1ea8d62e.svg"
                alt="GitHub"
                class="svelte-1u9z1tp"
            /></a>
          </div>
        </header>
        <main class="svelte-8o1gnw">
          <section class="svelte-19xx0bt">
            <h1 class="svelte-19xx0bt" data-svelte-h="svelte-11s73ib">
              <span class="welcome svelte-19xx0bt"
                ><picture
                  ><source
                    srcset="/_app/immutable/assets/svelte-welcome.c18bcf5a.webp"
                    type="image/webp" />
                  <img
                    src="/_app/immutable/assets/svelte-welcome.6c300099.png"
                    alt="Welcome"
                    class="svelte-19xx0bt" /></picture
              ></span>

              to your new<br />SvelteKit app
            </h1>
            <h2 data-svelte-h="svelte-1e36z0s">
              try editing <strong>src/routes/+page.svelte</strong>
            </h2>
            <div class="counter svelte-y96mxt">
              <button
                aria-label="Decrease the counter by one"
                class="svelte-y96mxt"
                data-svelte-h="svelte-97ppyc"
              >
                <svg aria-hidden="true" viewBox="0 0 1 1" class="svelte-y96mxt">
                  <path d="M0,0.5 L1,0.5" class="svelte-y96mxt"></path>
                </svg>
              </button>
              <div class="counter-viewport svelte-y96mxt">
                <div
                  class="counter-digits svelte-y96mxt"
                  style="transform: translate(0, 0%)"
                >
                  <strong class="hidden svelte-y96mxt" aria-hidden="true"
                    >1</strong
                  >
                  <strong class="svelte-y96mxt">0</strong>
                </div>
              </div>
              <button
                aria-label="Increase the counter by one"
                class="svelte-y96mxt"
                data-svelte-h="svelte-irev0c"
              >
                <svg aria-hidden="true" viewBox="0 0 1 1" class="svelte-y96mxt">
                  <path
                    d="M0,0.5 L1,0.5 M0.5,0 L0.5,1"
                    class="svelte-y96mxt"
                  ></path>
                </svg>
              </button>
            </div>
          </section>
        </main>
        <footer class="svelte-8o1gnw" data-svelte-h="svelte-1dlfr5">
          <p>
            visit
            <a href="https://kit.svelte.dev" class="svelte-8o1gnw"
              >kit.svelte.dev</a
            >
            to learn SvelteKit
          </p>
        </footer>
      </div>

      <script>
        {
          __sveltekit_jngsr4 = {
            base: new URL(".", location).pathname.slice(0, -1),
            env: {},
          };

          const element = document.currentScript.parentElement;

          const data = [null, null];

          Promise.all([
            import("./_app/immutable/entry/start.11cd0a52.js"),
            import("./_app/immutable/entry/app.f2f0959d.js"),
          ]).then(([kit, app]) => {
            kit.start(app, element, {
              node_ids: [0, 2],
              data,
              form: null,
              error: null,
            });
          });
        }
      </script>
    </div>
  </body>
</html>

このエントリーポイントの HTML をみていてわかることがいくつかあります。

  • 相対パスで CSS や JavaScript ファイルをインポートしている
  • それぞれのファイル名にはハッシュ値のようなものが含まれている (ブラウザのキャッシュ避け?)
  • ブラウザ側で kit.start(app, element, {...}) が呼ばれている

これらを踏まえてカスタマイズできるところもいくつかります。

エンドポイントのパスのカスタマイズ

他のアプリケーションを埋め込むにあたって最初に考えるのはエンドポイントのパスを調整したいという要件があります。ホームを kit-manager で管理するのであれば、少なくともそのアプリケーションとエンドポイントが競合しないように調整する必要があります。埋め込むアプリケーション専用に任意のエンドポイントを設定できればよいです。

前節の HTML から ES モジュールのインポートが相対パスで行われていることに気付きます。

<link rel="modulepreload" href="./_app/immutable/entry/start.11cd0a52.js" />

Configuration: paths にパスをカスタマイズする設定があります。svelte.config.js ファイルで制御します。kit-demo1 アプリケーションの svelte.config.js では次のように設定しています。

kit: {
  paths: {
    relative: false,
    base: '/kit-demo1'
  }
}

relative: false にすることで ES モジュールの先読みに指定されるパスが次のように絶対パスになります。これは kit-manager でプロキシするためには絶対パスで指定されていた方が都合がよいのでこのように設定しています。

<link rel="modulepreload" href="/kit-demo1/_app/immutable/entry/start.d464f935.js">

さらに base: '/kit-demo1' と設定することでコンポーネント内で扱うリンクも制御できます。デモアプリでは Header.svelte にヘッダーコンポーネントを実装していて、それぞれのページへのリンクが記載されています。こういったリンクに対してパスの接頭辞のように {base} を指定することで実際のエンドポイントのパスを調整できます。

<script>
  import { base } from '$app/paths';
</script>

<ul>
  <li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
    <a href="{base}/">Home</a>
  </li>
  <li aria-current={$page.url.pathname === '/about' ? 'page' : undefined}>
    <a href="{base}/about">About</a>
  </li>
  <li aria-current={$page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
    <a href="{base}/sverdle">Sverdle</a>
  </li>
</ul>

もう1つ、ついでに Configuration: csrf の設定も行います。これも検証していて気付いたことなのですが、異なる ORIGIN をもつサーバーに対して POST, PUT, PATCH, DELETE といった HTTP メソッドの通信はデフォルトでブラウザが拒否してしまいます。この制限を回避するために ORIGIN のチェックをしないという設定も追加しておきます。

kit: {
  csrf: {
    checkOrigin: false
  }
}

アダプターのカスタムサーバー

前節で SvelteKit アプリケーションのパスは調整できることがわかりました。

次は adapter-node でビルドした成果物を共存させる方法について考察してみます。例えば kit-demo1 の build/index.js のコードを読んでみると、Polka というアプリケーションサーバーを起動させていることがわかります。そして、SvelteKit アプリケーションのサーバーサイドレンダリングのためのハンドラーは Polka のミドルウェアとしてセットされていることも伺えます。

function polka (opts) {
  return new Polka(opts);
}

const path = env('SOCKET_PATH', false);
const host = env('HOST', '0.0.0.0');
const port = env('PORT', !path && '3000');

const server = polka().use(handler);

server.listen({ path, host, port }, () => {
  console.log(`Listening on ${path ? path : host + ':' + port}`);
});

Node servers: Custom server のドキュメントにはこのアプリケーションサーバーを置き換える方法について説明されています。build/handler.js にハンドラーが定義されているわけですが、同じインターフェースを取るアプリケーションサーバーであればそのままハンドラーとして設定するだけで動くようです。

kit-manager と kit-demo1 の2つの SvelteKit アプリケーションはそれぞれにエンドポイントをもつアプリケーションサーバー向けの成果物として生成されているため、これらのハンドラーを結合してから1つのハンドラーとして生成できればよいのではないかと当初考えました。 そして、ビルドの成果物を改変する余地がアダプターのフックポイントにはあります。しかし、それはかなり難しそうだということもわかっています。

もう1つのアイディアとして、複数の Node.js (アプリケーションサーバー) のプロセスを起動して、透過的にそれらにリクエストしてしまうのはどうかな?というのを次の節で試してみます。

サーバーサイドのフックとプロキシ

adapter-node でビルドした成果物の handler.js は再利用できるということはデバッグしていて分かっていました。しかし、たまたま次の issue を読んでいて、フック を使って任意のアプリケーションサーバーに置き換えることが言及されていました。

アプリケーションサーバーを置き換えられるなら、複数のアプリケーションサーバーを起動してはどうかと考えて試してみました。Node.js には Child process という、子プロセスを生成する API が提供されています。

build/index.js には環境変数により、外部からサーバーの設定を制御できるように作られていますが、shell: true でないと有効にならなかったため、シェルを介して env の環境変数を設定しています。

import { spawn } from 'child_process';

export function startNodeServer(env: { [key: string]: any }) {
	const opts = {
		shell: true,
		env: {
			...process.env,
			PORT: env.port,
			ORIGIN: env.origin,
			NODE_ENV: 'production'
		}
	};
	const node = spawn('node', env.args, opts);
	node.stdout.on('data', (data) => {
		console.log(data.toString());
	});

	node.stderr.on('data', (data) => {
		console.error(data.toString());
	});

	node.on('exit', (code) => {
		console.log(`Child exited with code ${code}`);
	});
}

この Node.js プロセスを起動する処理を hooks.server.ts に実装します。

import { apps } from '$lib/index';
import { startNodeServer } from '$lib/server/server';

const kitDemo1 = apps['kit-demo1'];
startNodeServer(kitDemo1);

kit-manager を起動すると、2つの Node.js (アプリケーションサーバー) のプロセスが起動していることがわかります。

$ node build/index.js
Listening on 0.0.0.0:3000
Listening on 0.0.0.0:3005

これでそれぞれの Node.js (アプリケーションサーバー) のプロセスが独立して動いているならそのまま共存できるのではないかという期待が膨らみます。

追加で hooks.server.ts に /kit-demo1 で始まるエンドポイントへのリクエストをすべて localhost:3005 へプロキシするコードを追加します。本来のサーバーサイドのフックの仕組みはこういったリクエストをフックする用途だと思います。

const prefix1 = '/kit-demo1';

export const handle: Handle = async ({ event, resolve }) => {
	if (event.url.pathname.startsWith(prefix1)) {
		if (event.request.method == 'GET') {
			return fetch(kitDemo1.url + event.url.pathname);
		} else if (event.request.method == 'POST') {
			const data = await event.request.formData();
			const endpoint = kitDemo1.url + event.url.pathname + event.url.search;
			return fetch(endpoint, { method: 'POST', body: data });
		}
	}
	const response = await resolve(event);
	return response;
};

これで準備は整いました。あとはコンポーネントから kit-demo1 へリクエストするコードを追加するだけです。それもブラウザ側からリクエストする方法とサーバー側からリクエストする方法の2つがあります。前節でサーバーサイドレンダリングが有効な SvelteKit のデモアプリケーションにリクエストすると HTML が返ることを確認しました。そのため、サーバー側で取得した HTML をそのままページに返すようなコンポーネントを実装します。

+page.server.ts で load() 関数に kit-demo1 から HTML を fetch してきます。

import type { PageServerLoad } from './$types';
import { apps } from '$lib/index';

export const load: PageServerLoad = async ({ params }) => {
	const res = await fetch(apps['kit-demo1'].entrypoint);
	const html = await res.text();
	return { html };
};

kit-demo1 から返された HTML をコンポーネントに埋め込みます。

<script lang="ts">
	import type { PageData } from './$types';
	export let data: PageData;
</script>

<title>Home</title>
<h1>[Demo1]Manager SvelteKit App</h1>

Write your own header.

<hr />

<div>{@html data.html}</div>

<hr />

Write your own footer.

見た目上は意図したようにコンポーネントを埋め込み、kit-manger と kit-demo1 のアセットも競合せず読み込んでいるようにみえます。

しかし、このページではブラウザ側のカウンターが動作しなかったり、リンクをマウスオーバーしたときにページを遷移 (ブラウザの先読み?) してしまったりします。マイクロフロントエンド的な互いに独立して動くといった概念には程遠い状況です。

ここで調査を一区切りにしましたが、単純にエンドポイントのパスを調整して、それぞれのアプリケーションサーバーにデプロイしただけで結合するというのは不可能だということがわかりました。

静的サイト生成

前節で「サーバーサイドレンダリングの SvelteKit アプリケーションを組み込みに使うのは内部の仕組みを把握していないとかなり難しい」と書いた番外編として、静的なサイトを生成するのであれば簡単なのかどうかも検証してみます。

adapter-static というアダプターを使って、サーバーサイドで処理を実装していない SvelteKit アプリケーションなら静的なビルドの成果物を生成してくれます。prerender というフラグがあり、場合によってはその値を適切に設定する必要もあるかもしれません。

export const prerender = true;

SvelteKit のデモアプリケーションからサーバーサイドのコンポーネントを取り除いたものが kit-demo2 になります。これはただの静的な成果物なのでそのまま kit-manager の static ディレクトリに配置することでアクセスできます。

$ ls -l static/
合計 4
-rw-rw-r-- 1 1571 12月  5 11:10 favicon.png
lrwxrwxrwx 1   21 12月  5 11:10 kit-demo2 -> ../../kit-demo2/build

kit-manger の static な領域にアクセスしているだけなのでエンドポイントもディレクトリをそのまま使えます。

http://localhost:3000/kit-demo2/index.html

但し kit-demo2 のエンドポイントのパスが変わってしまうため、kit-demo2 をビルドするときは static 配下のディレクトリ名にあわせてパス設定してビルドする必要があります。

kit: {
  paths: {
    relative: false,
    base: '/kit-demo2'
  }
}

ブラウザ側のカウンターも動作します。埋め込むのではなく、単体のアプリケーションとして扱う分には問題ないかもしれません。

まとめと考察

まとめを書くには、私がフロントエンドについて明るくないため、いくつかのスレッドを読んでいて参考になった、kit リポジトリの discussions のコメントを引用します。

私はこのトピックを広範囲に研究しました。私の意見では、Svelte をフルダイナミック SSR (プリレンダリングではない)、ハイドレーションを使用した「マイクロフロントエンド」に使うことは、非常に無駄であり、必要ないと率直に言います。まず、このために SvelteKit を使うのは完全に選択肢から外れています。SvelteKit 自体がページ全体を担当することを前提としており、同じページ上に2つのインスタンスを同居させることはできません。これは SvelteKit がルーティングなどのために複数の window globals を追加しているためです。また、SvelteKit はマイクロフロントエンドに期待するような方法でクライアントサイドのハイドレーションスクリプトを出力しないため、ハイドレーションでも問題が発生します。

そのため、マイクロフロントエンドに必要な方法で Svelte を構築するためには、基本的に独自の “SvelteKit” メタフレームワークを書く必要があります。これをやったことのある人なら、できることなら避けることを強く勧めるだろう。考えてみてください。SvelteKit は1年以上たった今でもベータ版で、作成者はフルタイムでそれに取り組んでいます。現実問題として、SvelteKit のクオリティに匹敵するようなものは存在しない。私にとっては、2つの異なる別々にコンパイルされた html と js をページに一緒に置くだけで、間違いなくパフォーマンスが低下することになりますが、その価値はありません。

技術的な面では、私は実際にあなたが必要とするものの簡単なデモを動作させることができました。主な問題は、Svelte が従来の JS フレームワークではなく、コンパイル言語であるということです。このため、従来のフレームワークにはない複雑さがいくつかある。Svelte は、他のフレームワークのような伝統的なランタイムを持たず、コンポーネントのランタイムは容量を節約するためにビルド時にコンパイルされます。そのため、Svelte を2回コンパイルする必要があります。一度はサーバー用に、そして一度はクライアント上のハイドレーション用に、また、これらのファイルを一緒に追跡しなければならず、事態をさらに複雑にしている。その上、Svelte が “コンパイルする " という事実が問題になりそうです。Svelte のコンパイラーは、未使用のスロットやイベントなどを取り除くなど、多くの最適化を行う。伝統的な開発をしているのであれば、これは素晴らしいことですが、あなたの場合、物事を別々にコンパイルしているため、多くの問題を引き起こす可能性があります。ページ上にある2つのコンポーネントは、かなり自己完結している必要があり、おそらくグローバルストアを介してのみ通信する必要があります。また、もう一方のコンポーネントに影響を与える可能性があるため、グローバルブラウザウィンドウへのアクセスを避ける必要があります。

繰り返しますが、それは可能です。しかし、あなたは自分自身にその痛みを与える価値があるかどうかを自問しなければなりません。SvelteKit は現在かなりうまく機能しており、サポートも十分だ。Svelte の主なセールスポイントは、バンドルサイズを削減するためにコンパイルする方法です。SvelteKit 自体も、少なくとも開発段階では Vite のおかげで非常に高速にコンパイルされるので、ビルド時間を心配する必要はありません。SvelteKit にこだわり、マイクロフロントエンドを完全にやめることをお勧めします。

bertybot on Jul 21, 2022 のコメント

このコメントも踏まえて、本稿で試行錯誤したものを見返しながら考察すると、私は次のように感じました。

  • SvelteKit アプリケーションの設計がページ全体を前提としている
    • エントリーポイントをカスタマイズしようと思ったらコンパイルやコード生成をしないといけない
  • 複数の SvelteKit アプリケーションのスクリプトを同時に読み込んでもうまく動作しない
    • Svelte または SvelteKit がグローバルな領域を使っていることへの懸念がある
  • 独自の “SvelteKit” メタフレームワークを書く必要があるというのは、アダプターで実装することの詳細でもある
    • コードを生成するコードを実装するのはとても難しい
  • Svelte がコンパイル言語であること、このことがフレームワークやランタイムの振る舞いをさらに複雑化している
    • 本番向けビルドはサーバー用とクライアント用、別々にコード生成している
    • SvelteKit アプリケーションはシンプルな Vite アプリケーションではない

いくつかのスレッドを読んでいて、あまり議論に出ていない話題として manifest.js があります。Vite の バックエンドとの統合 のドキュメントでは manifest.json が従来のバックエンドと結合するための情報として説明されています。SvelteKit では Vite の成果物を Rollup でバンドルするために manifest.js を生成しています。manifest.js に関するドキュメントや仕様の詳細を私はみつけることはできなかったのですが、おそらく内部的に使っているだけのようにみえます。将来的に SvelteKit が扱っている manifest.js が外部との連携や結合のための仕様として定義され、ブラウザ側のスクリプトがグローバルな領域を使わないようになれば、SvelteKit アプリケーションの連携もいまよりは組み込みやすくなるのかもしれません。現時点では SvelteKit の詳細理解とかなりのハックをしないと難しそうだというのが、私の率直な印象になります。

一方で SvelteKit ではなく Svelte で作った SPA のコンポーネントを他の Web サイトに組み込むのは少しがんばればできそうです。実際にいくつかのスレッドでそういった運用をしているというコメントもありました。Svelte のドキュメントにも Custom elements API があり、いくつか注意事項と制限も説明されています。これらが要件を満たせば Web Components として生成して再利用するといった使い方はできるのかもしれません。

リファレンス