もしもあなたがWeb Componentについて知識があるなら、Shadow DOM についても知っているでしょう。Shadow DOM は外部と隔離された DOM ツリーを作成するためのもので、マイクロフロントエンドではよく使用されます。内部で任意のスタイルを定義することができ、外部のスタイルを汚染することはありませんが、その特徴のために Shadow DOM は外部のスタイルを継承しません。もしも TailwindCSS や他のコンポーネントライブラリのスタイルを使用している場合、Shadow DOM 内でも適用されます。
例#
まず、シンプルな TailwindCSS のシングルページアプリケーションを作成しましょう。
// シャドウDOMツリーを作成し、カスタム要素を定義する
class MyCustomElement extends HTMLElement {
constructor() {
super()
// カスタム要素にシャドウDOMをアタッチする
const shadow = this.attachShadow({ mode: 'open' })
// シャドウDOM用のコンテンツを作成する
const wrapper = document.createElement('div')
wrapper.setAttribute('class', 'text-2xl bg-primary text-white p-4')
wrapper.textContent = 'I am in the Shadow DOM tree.'
// コンテンツをシャドウDOMに追加する
shadow.append(wrapper)
}
}
// カスタム要素を定義する
customElements.define('my-custom-element', MyCustomElement)
export const Component = () => {
return (
<>
<p className="text-2xl bg-primary text-white p-4">
I'm in the Root DOM tree.
</p>
<my-custom-element />
</>
)
}
上記のコードの実行結果は以下の通りです:
上記の要素はホスト(ルート)DOM 内にあり、TailwindCSS のスタイルが正しく適用されていますが、ShadowRoot 内の要素はスタイルが適用されず、ブラウザのデフォルトスタイルが表示されています。
解決策#
私たちはパッケージャーが CSS スタイルをdocument.head
に注入することを知っていますので、これらのタグを抽出し、同じように ShadowRoot に注入すれば良いです。
では、どのように実現するのでしょうか。
React を例に取り上げますが、他のフレームワークでも同様です。
React では、react-shadowを使用して Shadow DOM を利用することで、DX を向上させることができます。
npm i react-shadow
上記のコードを以下のように変更することができます:
import root from 'react-shadow'
export const Component = () => {
return (
<>
<p className="text-2xl bg-primary text-white p-4">
I'm in the Root DOM tree.
</p>
<root.div>
<p className="text-2xl bg-primary text-white p-4">
I'm in the Shadow DOM tree.
</p>
</root.div>
</>
)
}
現在、まだスタイルが適用されていません。次に、ホストのスタイルを注入します。
import type { ReactNode } from 'react'
import { createElement, useState } from 'react'
import root from 'react-shadow'
const cloneStylesElement = () => {
const $styles = document.head.querySelectorAll('style').values()
const reactNodes = [] as ReactNode[]
let i = 0
for (const style of $styles) {
const key = `style-${i++}`
reactNodes.push(
createElement('style', {
key,
dangerouslySetInnerHTML: { __html: style.innerHTML },
}),
)
}
document.head.querySelectorAll('link[rel=stylesheet]').forEach((link) => {
const key = `link-${i++}`
reactNodes.push(
createElement('link', {
key,
rel: 'stylesheet',
href: link.getAttribute('href'),
crossOrigin: link.getAttribute('crossorigin'),
}),
)
})
return reactNodes
}
export const Component = () => {
const [stylesElements] = useState<ReactNode[]>(cloneStylesElement)
return (
<>
<p className="text-2xl bg-primary text-white p-4">
I'm in the Root DOM tree.
</p>
<root.div>
<head>{stylesElements}</head>
<p className="text-2xl bg-primary text-white p-4">
I'm in the Shadow DOM tree.
</p>
</root.div>
</>
)
}
これでスタイルが正常に注入されました。ShadowDOM はホストのスタイルを継承していることがわかります。
ホストスタイルのレスポンシブな更新#
現在のスタイルの注入方法では、ホストのスタイルが変更されても、ShadowDOM のスタイルは更新されません。
たとえば、ボタンを追加し、クリックすると新しいスタイルが追加されるようにしてみましょう。
<button
className="btn btn-primary mt-12"
onClick={() => {
const $style = document.createElement('style')
$style.innerHTML = `p { color: red !important; }`
document.head.append($style)
}}
>
Update Host Styles
</button>
ShadowDOM のスタイルは更新されません。
<head />
の更新を監視するためにMutationObserverを使用することができます。
export const Component = () => {
useLayoutEffect(() => {
const mutationObserver = new MutationObserver(() => {
setStylesElements(cloneStylesElement())
})
mutationObserver.observe(document.head, {
childList: true,
subtree: true,
})
return () => {
mutationObserver.disconnect()
}
}, [])
// ..
}
以下のような効果が得られます:
問題が解決しました。
まとめ#
なぜ ShadowDOM を使用するのかというと、ShadowDOM ではグローバルなスタイルを汚染することなく、任意のスタイルを注入することができるからです。
この解決策は非常にシンプルで、どのフレームワークでも、または純粋な DOM 操作でも使用できます。これはフレームワークに依存しないネイティブな解決策です。
私が言いたいのは、モダンなフロントエンドのツールチェーンやプラグインに頭がいっぱいになりすぎて、少しの問題でもフレームワークやプラグインから出発しようとすることです。それはただの普通の DOM 操作です。だから笑い話ができるのです。現代のフロントエンド開発者は、jQuery の DOM トラバースすら知らないのです。
この記事はMix Spaceから xLog に同期されています。
元のリンクはhttps://innei.in/posts/tech/ShadowDOM-style-isolation-and-inheritanceです。