🎨

Shadow DOMでIslands Architectureっぽく(CSS編)

に公開

記事のモチベ

定期的に、既存アプリケーションのリプレースの効率や、既存を残しつつ新規のアプリケーションを導入しやすい方法を考えることがあります。直近Vue2のプロジェクトをReactに変える相談があって今年度例の定期入ったかというモチベでこの記事を書いてます。

前の定期時にはマイクロフロントエンドと向きあってみるというのをアウトプットしてたりしました。

記事の内容

Shadow DOMの特性を使って、既存アプリケーションに対し、異なるWebフロントエンドの実装を入れてみたというものになります。

既存のアプリケーションとShadow DOM内のCSSのスコープが分離できるため、TailwindのようなCSSでも分離して使いやすいのでは?という過程で試してみました。

あと、この記事を書き終えたぐらいのタイミングで正味、今だったらAstro移行するのもありかなとは思いましたが、この記事としては移行用のライブラリ依存(複数アプリを1つのビルドにまとめるようなもの)なしに比較的にWeb標準の機能で徐々に移し替えが可能な手段として記載しました。

今回はCSSにフォーカスした内容となり、別でグローバルステート編も記載したいと思います。

動作環境

https://igara.github.io/multi-fw-demo/nextjs/multi.html

shadow_dom_apps

1つのページに対し、上からShadow DOM上にあるVue2のアプリケーション、ページ全体を構成しているNext.jsのアプリケーション、Shadow DOM上にあるReactのアプリケーションが動いています。

Next.jsとReactのアプリケーションは同じshadcn/uiで実装したコンポーネントを使用しています。適応するテーマのCSSだけを変えて表示していますが、それぞれのアプリケーションでCSSが干渉していないことがわかります。

解説

それぞれのアプリケーションで展開するためのRoute設定

今回はGitHub Pages上で動かすため、部分的にあまりしない設定していると思います。

Next.jsアプリケーション

  • next.config.ts
    next.config.ts
    const nextConfig: NextConfig = {
      output: "export",
      basePath: "/multi-fw-demo/nextjs",
      images: {
        unoptimized: true,
      },
    };
    
    GitHub Pagesに展開するためSSGで出力
  • /app/multi/page.tsx
    /multi-fw-demo/nextjs/multiに今回のサンプルページを作るために設置

Reactアプリケーション

TanStack RouterによるRoute設定

  • src/main.tsx
    main.tsx
    let basePath = "/multi-fw-demo/react/";
    if (window.location.pathname.startsWith("/multi-fw-demo/nextjs")) {
      basePath = "/multi-fw-demo/nextjs/";
    }
    const multiRoute = createRoute({
      getParentRoute: () => routeTree,
      path: "multi.html",
      component: Multi,
    });
    // Create a new router instance
    const router = createRouter({
      routeTree: routeTree.addChildren([multiRoute]),
      basepath: basePath,
    });
    
    せっかくTanStack Router使っていますがGitHub Pagesに展開されるNext.jsのページがmulti.htmlなので、そこに合わせてRouteを追加しています。

Vue2アプリケーション

Vue RouterによるRoute設定

  • src/router/index.ts
    src/router/index.ts
    const routes: Array<RouteConfig> = [
      {
        path: '/multi.html',
        name: 'MultiFramework',
        component: HelloWorld
      },
    ]
    let basePath = '/multi-fw-demo/vue2/';
    if (window.location.pathname.startsWith('/multi-fw-demo/nextjs')) {
      basePath = '/multi-fw-demo/nextjs/';
    }
    if (window.location.pathname.startsWith('/multi-fw-demo/react')) {
      basePath = '/multi-fw-demo/react/';
    }
    
    const router = new VueRouter({
      mode: 'history',
      base: basePath,
      routes
    })
    

Shadow DOM上にアプリケーションを展開するためのコード

この2つのコードの違いとしてDOMのid指定がreact-appvue2-appになっている点ぐらいです。

コードの内容を箇条書きすると

  • Shadow Root作成
  • Shadow Rootにマウントするためのアプリケーションのid要素追加
  • それぞれのアプリケーションで使用するCSS読み込み
  • Shadow RootにCSS適応
    • 例外的にCSSの@propertyはShadow DOM内に適応されないためグローバルに適応
  • アプリケーションのJSファイルを読み込み

の流れになります。

詰まった点

shadcn/uiのテーマがShadow DOM内に適応されない

shadcn/ui公式のCSSのままではShadow DOMにはスタイルの適応ができなく、

shadcn

src/themes/blue.css
:root,
+ :host {
  --radius: 0.65rem;

:hostセレクタを追加しました。

Tailwind CSS 4の@propertyルールがShadow DOM内に適応されない

--tw-xxxというような命名規則のあるTailwind CSS共通の@propertyがありますが上書きされて困ることがないのでグローバルに適応するようにしました。

tw-property

参考

GitHubで編集を提案
chot Inc. tech blog

Discussion