You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Shadow DOM多实例场景下style-loader懒加载样式失效问题求助

问题描述

使用webpack + style-loader为Shadow DOM组件注入样式时,当页面中存在该组件的多个实例,样式会停止注入。此前在单实例组件的disconnectCallback中调用styles.unuse()解决过类似问题,但本组件需要同时渲染多个实例,无论是否调用unuse都无法解决样式失效的问题。

相关代码

Component.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';

// Styles
import styles from '../styles/contentScript.css';

// Images
const icon = chrome.runtime.getURL('assets/icon.png');

export default class CustomButton extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });

        // Functions
        this.handleShow = this.handleShow.bind(this);
    }

    handleShow = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
        ...
    };

    connectedCallback() {
        // Inject Styles
        styles.use({ target: this.shadowRoot });

        // Inject Component into ShadowRoot
        createRoot(this.shadowRoot).render(this.render());
    }

    render() {
        return (
            <div className='absolute inset-0'>
                <button className='main-button group' onClick={this.handleShow}>
                    <img src={icon} className='w-7' />
                    <span className='main-button-tooltip group-hover:scale-100'>
                        Open Popup
                    </span>
                </button>
            </div>
        );
    }

    disconnectedCallback() {
        styles.unuse();
    }
}

customElements.define('custom-button', CustomButton);

webpack.config.js

// Imports...
module.exports = {
    entry: {
        ...
    },
    module: {
        rules: [
            {
                use: 'ts-loader',
                test: /\.tsx?$/,
                exclude: /node_modules/,
            },
            {
                use: [
                    {
                        loader: 'style-loader',
                        options: {
                            injectType: 'lazyStyleTag',
                            insert: function insertIntoTarget(element, options) {
                                var parent = options.target || document.head;
                                parent.appendChild(element);
                            },
                        },
                    },
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            postcssOptions: {
                                indent: 'postcss',
                                plugins: [tailwindcss, autoprefixer],
                            },
                        },
                    },
                ],
                test: /\.css$/i,
            },
            {
                type: 'asset/resource',
                test: /\.(png|jpg|jpeg|gif)$/i,
            },
        ]
    },
    resolve: {
        ...
    },
    plugins: [
        ...
    ],
    output: {
        filename: '[name].js',
        clean: true,
    },
    optimization: {
        ...
    },
}

补充说明

  • 使用Tailwind CSS开发样式,配合postcss和autoprefixer
  • 这是Chrome扩展,组件在contentScript中创建

解决方案

问题根源

style-loader的lazyStyleTag模式生成的样式对象是单例的:

  • 多次调用styles.use({target: shadowRoot})会将同一个样式标签在不同实例的shadowRoot之间移动,导致只有最后一个实例能保留样式
  • 调用styles.unuse()会直接移除样式标签,导致所有实例样式失效

修复步骤

  1. 修改webpack配置,将CSS导出为字符串
    移除style-loader,让css-loader直接将CSS内容导出为字符串:

    // webpack.config.js 中css规则修改为:
    {
        test: /\.css$/i,
        use: [
            'css-loader',
            {
                loader: 'postcss-loader',
                options: {
                    postcssOptions: {
                        indent: 'postcss',
                        plugins: [tailwindcss, autoprefixer],
                    },
                },
            },
        ],
        type: 'asset/source',
    }
    
  2. 在组件中手动插入样式到Shadow DOM
    每个实例创建独立的style元素,插入到自身的shadowRoot中:

    // Component.tsx 修改后的核心代码
    export default class CustomButton extends HTMLElement {
        private styleElement?: HTMLStyleElement;
    
        constructor() {
            super();
            this.attachShadow({ mode: 'open' });
            this.handleShow = this.handleShow.bind(this);
        }
    
        connectedCallback() {
            // 创建独立样式元素并插入当前实例的shadowRoot
            this.styleElement = document.createElement('style');
            this.styleElement.textContent = styles;
            this.shadowRoot.appendChild(this.styleElement);
    
            createRoot(this.shadowRoot).render(this.render());
        }
    
        disconnectedCallback() {
            // 销毁时移除当前实例的样式元素
            if (this.styleElement?.parentElement) {
                this.styleElement.parentElement.removeChild(this.styleElement);
            }
        }
    
        // ... 其他代码保持不变
    }
    

效果说明

每个组件实例拥有独立的样式元素,完全隔离不会互相干扰;销毁组件时精准移除自身的样式元素,避免内存泄漏,多实例场景下样式可以正常注入。

内容的提问来源于stack exchange,提问作者wolfy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 22:55:16