ライブラリの依存関係を解決しないUIのimport
概要
UIコンポーネントをjsファイルにbundleし、作成されたjsファイルを特定のプロジェクトの、UIコンポーネントとして表示させたのでまとめたいと思います。
背景
近年、javascriptのbuildに関するツールがいろいろ存在します。関連してmonorepoやmicro frontendがあると思います。UIコンポーネントをライブラリ化し特定のプロジェクトで呼び出したいケースが想定されますが、今回ライブラリとしてimportしたくないケースが発生しました。
UIコンポーネントをライブラリ化したくない
ライブラリとして作成したUIコンポーネントをimportする方法はライブラリの依存関係の整理が必要であり、ライブラリの依存関係の問題を一旦先送りにしたい場合には使えないです。サクッとリリースに混ぜてあとで整理したいケースではまずはリリースにこじつけたいところです。
UIをhostingしてiframeやWebViewなどから表示することもできますが、任意のタイミングでrenderingさせたりpropsを渡したり、動作確認しながらスピード感のある開発をしたりする上でそこまでもっていくのはなかなか大変そうです。
そこで、チーム内で以下のような方法でやればできるのではないかと教えてもらいました。実際にリリースまでもっていけたので今回はその方法をまとめていきたいと思います。
- 別プロジェクトとして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で表示されるように設定します。
function App() {
return (
<div>sub-ui</div> // 読み込みたいUIコンポーネント
)
}
export default App
idをroot
から任意のidに変えます。
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
ディレクトリに作成されると思います。
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をもつノードを作成します。
import React from "react";
export const SubUIView = () => {
return (
<div
id="sub-ui"
></div>
);
};
bundleしたjsファイルをscriptタグとして追加されるように設定します。
以下ファイルをimportするように設定します。
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を渡します。
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>
);
};
import { SubUi } from "../lib/Components/SubUi";
function App(dataset: DOMStringMap) {
return (
<SubUi
userId={dataset.userId}
teamId={dataset.teamId}>
/>
)
}
export default App;
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の管理が煩雑化するので必要な時のみの利用がいいと思いますが、うまくできていると思います。手伝ってもらったチームのメンバーには感謝です。
Discussion