📌

importmapでルーティング? ファイルベースWebコンポーネントに最適な新提案

に公開

はじめに

現代のフロントエンド開発では、ReactやVueに代表されるルーターライブラリを使って、ルートパスとコンポーネントを対応づけるのが一般的です。しかし、Web Components やファイルベースで構成される軽量フレームワークにおいては、従来のルーティング方式がやや冗長に感じられる場面もあります。

この記事では、ブラウザ標準の importmap を活用して、コードを書かずにルート定義を行う新しいアプローチを提案します。


従来の課題

ファイルベースのWebコンポーネントを使う構成では、以下のような課題があります:

  • ルーティング設定とファイル読み込みが別の場所で定義され、整合性が取りにくい
  • パスとコンポーネントのマッピングがコード内にハードコーディングされがち
  • 小規模アプリでもルーターライブラリの導入が必要になることがある

提案:importmapをルーティングテーブルとして使う

importmap はES Modules向けに提供されている標準仕様で、モジュール名と実際のURLパスをマッピングできます。この仕組みを利用して、ルートパスとWebコンポーネントのファイルを対応づけることができます。

importmapの例:

<script type="importmap">
{
  "imports": {
    "routes:": {
      "/": "./components/home-page.js",
      "/users": "./components/user-list.js",
      "/products/:id": "./components/product-detail.js"
    },
    "components:navbar": "./navbar.js"
  }
}
</script>

このように "routes:" 名前空間を用意しておくことで、ルートとファイルを構造的に整理できます。
1ファイル1コンポーネントの利用を想定しています。


defineComponentsでルートとタグ名を対応付け

defineComponents({
  "home-page": "/",
  "user-list": "/users",
  "product-detail": "/products/:id",
  "navbar-component": "components:navbar"
});

このとき、defineComponents 側では、

  • "/users" というキーが importmap.imports["routes:"] に存在するかを確認
  • 存在すればそのURLを import() して Web コンポーネントとして登録

という動作が行われます。

これにより、明示的に "routes:" を書かなくても、ルーティング対象か通常のimport対象かを自動で判別できるようになります。


ルーティングの実装例

以下は、defineComponents() の内部で行われる最小構成のルーティングエンジンの例です。

const routeMap = new Map();
const importMap = document.querySelector('script[type="importmap"]').textContent;
const routes = JSON.parse(importMap).imports["routes:"] || {};

function defineComponents(mapping) {
  for (const [tag, path] of Object.entries(mapping)) {
    if (routes[path]) {
      routeMap.set(path, tag);
    } else {
      // 通常のモジュールとして即時登録(ルーティング対象外)
      import(path).then(mod => customElements.define(tag, mod.default));
    }
  }
  initRouter();
}

function initRouter() {
  window.addEventListener("popstate", () => {
    const currentPath = location.pathname;
    for (const [route, tag] of routeMap.entries()) {
      if (matchRoute(route, currentPath)) {
        loadComponent(tag, routes[route]);
        break;
      }
    }
  });
  window.dispatchEvent(new Event("popstate")); // 初回実行
}

function loadComponent(tag, modulePath) {
  import(modulePath).then(mod => {
    if (!customElements.get(tag)) {
      customElements.define(tag, mod.default);
    }
    const mount = document.querySelector("main");
    mount.innerHTML = "";
    mount.appendChild(document.createElement(tag));
  });
}

function matchRoute(pattern, path) {
  // 簡易実装: 完全一致
  return pattern === path;
}

この実装により:

  • defineComponents() でタグとルートの対応を記憶
  • popstate イベントで現在のURLを監視
  • マッチしたルートに対するWebコンポーネントを import してマウント

という 最小のSPAルーター機能が構築できます。

今後の拡張として、:id のような動的セグメント対応、リンククリック時の pushState 制御なども加えることができます。


メリットまとめ

  • ルーティング構成を importmap に集約できる
  • コード上のマッピングロジックが簡素化される
  • 静的構造なのでビルドや解析がしやすい
  • 他の名前空間(components:, api: など)との共存も可能
  • 最小のコードでシンプルなSPA構築が可能

まとめ

ルーティングをコードではなく構造として記述することで、Webコンポーネントベースのアプリケーション開発はより明快かつ保守しやすくなります。importmapをルーティングテーブルとして活用することで、軽量・宣言的・拡張可能なルーター設計が実現可能になります。

これからの小規模Webアプリ、静的SPA、あるいはWebコンポーネント中心のフレームワークにおいて、この方式は新しい選択肢になるかもしれません。

追記 (2025/4/25)

chromeで試したところ、importsの中にroutes:を作ってその中に入れ子の構造を持たせることはできないと警告が出た。また:は推奨されないので@routes/ルートパスに変更したほうがよい。またルートのトップを表す@routes//で終わると設定するパスも'/'で終わる必要があるので@routes/rootにして内部的に/で扱うような仕様がよいように思われる。

Discussion