🐺

ライブラリの依存関係を解決しないUIのimport

2023/12/01に公開

概要

UIコンポーネントをjsファイルにbundleし、作成されたjsファイルを特定のプロジェクトの、UIコンポーネントとして表示させたのでまとめたいと思います。

背景

近年、javascriptのbuildに関するツールがいろいろ存在します。関連してmonorepoやmicro frontendがあると思います。UIコンポーネントをライブラリ化し特定のプロジェクトで呼び出したいケースが想定されますが、今回ライブラリとしてimportしたくないケースが発生しました。

UIコンポーネントをライブラリ化したくない

ライブラリとして作成したUIコンポーネントをimportする方法はライブラリの依存関係の整理が必要であり、ライブラリの依存関係の問題を一旦先送りにしたい場合には使えないです。サクッとリリースに混ぜてあとで整理したいケースではまずはリリースにこじつけたいところです。
UIをhostingしてiframeやWebViewなどから表示することもできますが、任意のタイミングでrenderingさせたりpropsを渡したり、動作確認しながらスピード感のある開発をしたりする上でそこまでもっていくのはなかなか大変そうです。
https://speakerdeck.com/berlysia/jsconf-jp-2022

そこで、チーム内で以下のような方法でやればできるのではないかと教えてもらいました。実際にリリースまでもっていけたので今回はその方法をまとめていきたいと思います。

  • 別プロジェクトとしてUIを作成
  • bundleしたjsファイルを作成する
  • bundleしたファイルを呼び出したいプロジェクトに配置
  • 特定のprojectからUI呼び出す

今回は全てreactでコンポーネントは作成されvite@4.4.5を使ってUIコンポーネントをbundleしています。

別プロジェクトとしてUIを作成

今回はviteを使います。まずはプロジェクトを作成します。ディレクトリの場所は特に限定しないです。

npm create vite@latest sub-ui -- --template react-ts

次にコンポーネントを作り、app.tsxで表示されるように設定します。

sub-ui/app.tsx
function App() {

  return (
    <div>sub-ui</div> // 読み込みたいUIコンポーネント
  )
}

export default App

idをrootから任意のidに変えます。

sub-ui/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

/* 変更箇所 
document.getElementById('root') -> document.getElementById('sub-ui')
*/
ReactDOM.createRoot(document.getElementById('sub-ui')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

bundleしたjsファイルを作成する

vite-config.ts を編集しbundleの設定をします。
以下をvite-config.ts に設定するとcssが内包されたjsファイルとassetがbundleされ dist ディレクトリに作成されると思います。

sub-ui/vite-config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  base: "/sub-ui",
  build: {
    rollupOptions: {
      input: {
        main: "./src/main.tsx"
      },
      output: {
        inlineDynamicImports: true,
        entryFileNames: "[name].js",
        assetFileNames: "[name].[ext]",
        format: "iife"
      }
    }
  }
});

bundleしたファイルを呼び出したいプロジェクトに配置

Webページを立ち上げる場合はファイルをbuildした上で環境が立ち上がると思います。buildされたファイル(おそらくdist)に、cp commandなどを使ってbundleしたUIコンポーネントのファイルを移動させます。vite-config.tsでbaseをsub-uiにしておいたのでsub-uiディレクトリを作成し配置させます。

UIコンポーネントを呼び出す

root から先ほど変更した任意のidをもつノードを作成します。

SubUIView.tsx
import React from "react";

export const SubUIView = () => {
  return (
    <div
      id="sub-ui"
    ></div>
  );
};

bundleしたjsファイルをscriptタグとして追加されるように設定します。
以下ファイルをimportするように設定します。

useScript.ts
import { useEffect } from "react";

export const useScript = (src: string, option?: any) => {
  useEffect(() => {
    const script = document.createElement("script");
    script.src = src;
    script.async = true;
    document.body.appendChild(script);

    return () => {
      document.body.removeChild(script);
    };
  }, [option]);  // 再レンダリングのタイミングの制御が必要ななければ空で良い
};

renderingのタイミングも調整したい場合があると思いますが、その場合はuseScriptの関数の引数とuseEffectの依存する変数の配列に、変更があったら再レンダリングしてほしい変数を追加します。

また、呼び出したprojectからpropsを渡したい場合があると思います。その場合はdata-*を使ってpropsを渡します。

SubUIView.tsx
import React from "react";
import { useScript } from "../hooks/useScript";


type Props = {
  userId: string;
  userName: string;
};

export const SubUiView = ({ userId, userName }: Props) => {
  useScript(`/sub-ui/main.js`, options);
  return (
    <div
      id="sub-ui"
      data-user-id={userId}
      data-user-name={userName}
    ></div>
  );
};

sub-ui/app.tsx
import { SubUi } from "../lib/Components/SubUi";

function App(dataset: DOMStringMap) {
return (
    <SubUi
	userId={dataset.userId}
        teamId={dataset.teamId}>
    /> 
  )
}

export default App;

sub-ui/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const elm = document.getElementById("sub-ui")!;
ReactDOM.createRoot(document.getElementById("sub-ui")!).render(
  <React.StrictMode>
    <App {...elm.dataset} />
  </React.StrictMode>
);

さいごに

今回、ライブラリ依存は解決したくないがファイルを分けて開発したいケースについて記述しました。hooksの管理が煩雑化するので必要な時のみの利用がいいと思いますが、うまくできていると思います。手伝ってもらったチームのメンバーには感謝です。

Aidemy Tech Blog

Discussion