【问题标题】:Inject CSS styles to shadow root via Webpack通过 Webpack 将 CSS 样式注入到 shadow root
【发布时间】:2020-06-24 19:42:00
【问题描述】:

我正在尝试修改使用 shadow root 创建的 Web 组件的样式。

我看到样式被添加到head 标签,但它对shadow root 没有影响,因为它是封装的。

我需要加载所有组件的样式并让它们出现在shadow root 的内部。

这是创建 Web 组件的一部分:

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
import './tmp/mockComponent.css'; // This is the styling i wish to inject


let container: HTMLElement;

class AssetsWebComponent extends HTMLElement {

    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        const { shadowRoot } = this;
        container = document.createElement('div');
        shadowRoot.appendChild(container);
        ReactDOM.render(<App />, container);

    }
}

window.customElements.define('assets-component', AssetsWebComponent);

App.ts // 常规反应组件

import React from 'react';
import './App.css';
import { MockComponent } from './tmp/mockComponent'

export const App: React.FunctionComponent = (props) => {
    return (
        <MockComponent />
    );
};

webpack.config.ts

// @ts-ignore
const path = require('path');
const common = require('./webpack.common.ts');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = merge(common, {
    mode: 'development',
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },

    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        loader: 'style-loader',
                        options: {
                            insert: function (element) {
                                const parent = document.querySelector('assets-component');

                                ***This what i want, to inject inside the shadowRoot but it 
                                never steps inside since the shadowRoot not created yet***

                                if (parent.shadowRoot) {
                                    parent.shadowRoot.appendChild(element);
                                }
                                parent.appendChild(element);
                            },
                        },
                    },
                    'css-loader',
                ],
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader',
                ],
            },
        ],
    },

    plugins: [
        new HtmlWebpackPlugin({
            template: './src/template.html',
        }),
    ],

    devtool: 'inline-source-map',
});

由于 MockComponent 内部可以有更多的组件,我依靠 Webpack 将所有样式注入到shadow root。 我正在使用style-loader 注入样式,但效果不佳。

我做错了什么,或者这个解决方案有什么替代方案。

【问题讨论】:

    标签: css reactjs webpack shadow-dom webpack-style-loader


    【解决方案1】:

    原来你只需要'css-loader',所以你应该完全删除'style-loader'及其选项。 所以在 webpack.config.ts 中:

     module: {
                rules: [
                    {test:/\.css$/, use:'css-loader'}
                ]
            }
    

    然后您想导入我们从 css-loader 获得的样式字符串,并在您附加到容器 before 的影子根的样式元素中使用它。 所以在 index.tsx 中:

    ......
    import style from './tmp/mockComponent.css'; 
    ......
    constructor() {
            super();
            this.attachShadow({ mode: 'open' });
            const { shadowRoot } = this;
            container = document.createElement('div');
    
            styleTag = document.createElement('style');
            styleTag.innerHTML = style;
            shadowRoot.appendChild(styleTag);            
    
            shadowRoot.appendChild(container);
            ReactDOM.render(<App />, container);
    
        }
    ......
    

    'style-loader' 给你带来问题的原因是,它被用来在最终的 html 文件中自行查找将 css 样式标记注入的位置,但你已经知道通过 JS 将它放在你的 shadowDOM 中的位置,所以你只需要'css-loader'。

    【讨论】:

    • 如果您的应用程序的其他部分确实需要“样式加载器”,您可以在导入语句中指定加载器,并在其前面加上“!!”为了禁用所有配置的加载器。更多详情webpack.js.org/concepts/loaders
    【解决方案2】:

    有一种方法可以使用style-loader

    这一切都归结为执行顺序。你的index.tsxstyle-loader::insert 之后被执行。但是影子根必须在之前存在。

    最简单的方法是修改index.html

    这是一个完整的例子:

    ./src/index.html

    ...
    <body>
        <div id="root"></div>
        <script>
            <!-- This gets executed before any other script. -->
            document.querySelector("#root").attachShadow({ mode: 'open' })
        </script>
    </body>
    ...
    

    ./webpack.config.js

    var HtmlWebpackPlugin = require('html-webpack-plugin');
    
    const cssRegex = /\.css$/i
    
    const customStyleLoader = {
      loader: 'style-loader',
      options: {
        insert: function (linkTag) {
          const parent = document.querySelector('#root').shadowRoot
          parent.appendChild(linkTag)
        },
      },
    }
    
    module.exports = {
      module: {
        rules: [
          {
            test: cssRegex,
            use: [
              customStyleLoader,
              'css-loader'
            ],
          },
        ],
      },
    
      plugins: [
        new HtmlWebpackPlugin({
          template: './src/index.html'
        })
      ],
    };
    

    ./src/index.js

    import "./style.css";
    
    const root = document.getElementById('root').shadowRoot
    // Create a new app root inside the shadow DOM to avoid overwriting stuff (applies to React, too)
    const appRoot = document.createElement("div")
    appRoot.id = "app-root"
    root.appendChild(appRoot)
    
    appRoot.textContent = "This is my app!"
    

    ./src/style.css

    #app-root {
        background: lightgreen;
    }
    

    构建并提供您的应用后,您应该会看到带有绿色背景的“这是我的应用”,所有内容都在您的影子根中。

    【讨论】:

      【解决方案3】:

      其他解决方案是直接插入样式:

      import React from 'react';
      import ReactDOM from 'react-dom';
      import { App } from './App';
      import styles from  './tmp/mockComponent.css'; //--> import the styles
      
      
      let container: HTMLElement;
      
      class AssetsWebComponent extends HTMLElement {
      
          constructor() {
              super();
              this.attachShadow({ mode: 'open' });
              const { shadowRoot } = this;
              container = document.createElement('div');
              shadowRoot.appendChild(container);
              ReactDOM.render(<>
                 <style>{styles}</style> {/* add styles*/}
                 <App />
              </>, container);
      
          }
      }
      

      【讨论】:

      • 在应用您的解决方案时出现此错误 - “对象作为 React 子级无效(发现:带有键 {} 的对象)。如果您打算渲染一组子级,请改用数组。 "
      • @ParfectShot 那是因为您没有以字符串形式接收样式,请检查您的模块包配置...从 './tmp/mockComponent.css 导入样式应该返回一个字符串...跨度>
      • 感谢您的澄清。我最终创建了一个元素(附加在阴影中)并将样式放入其中。这对我有用。
      【解决方案4】:

      我想为这个问题添加我的解决方案。它基于@josef-wittmann 的想法,但允许将样式添加到自定义元素的任何新实例,而不仅仅是一个根元素。

      index.html

      ...
      <body>
          <script src="main.js"></script>
          <my-custom-element></my-custom-element>
      </body>
      ...
      

      ./webpack.config.js

      module.exports = {
        module: {
          rules: [
            {
              test: /\.css$/i,
              use: [
                {
                  loader: "style-loader",
                  options: {
                    insert: require.resolve("./src/util/style-loader.ts"),
                  },
                },
                "css-loader",
              ],
            },
            {
              test: /\.tsx?$/,
              exclude: /node_modules/,
              loader: "ts-loader",
              options: {
                configFile: "tsconfig.app.json",
              },
            },
          ],
        },
        plugins: [
          new HtmlWebpackPlugin({
            template: "./src/index.html",
          }),
        ],
        output: {
          filename: "main.js",
          path: path.resolve(__dirname, "dist"),
        },
      };
      

      ./src/util/style-loader.ts

      export const styleTags: HTMLLinkElement[] = [];
      
      export default function (linkTag) {
          styleTags.push(linkTag);
      }
      

      ./src/index.ts

      import "./style.css";
      import { styleTags } from "./util/style-loader";
      
      customElements.define(
        "my-custom-element",
        class extends HTMLElement {
          async connectedCallback() {
            this.attachShadow({ mode: "open" });
      
            const myElement = document.createElement("div");
            myElement.innerHTML = "Hello";
      
            this.shadowRoot?.append(...styleTags, myElement );
          }
        }
      );
      

      ./src/style.css

      div {
          background: lightgreen;
      }
      

      【讨论】:

        【解决方案5】:

        我必须创建一个可以放置在常规 dom 元素内或元素的 shadow dom 内的 Web 组件。我还使用了很多必须先转换为 CSS 的 sass 文件。

        我尝试使用@florian-bachmann 解决方案,但不知何故,require.resolve("./src/util/style-loader.ts") 行导致了一个问题,因为 style-loader.ts 文件没有得到解决,所以我决定使用 window 对象来存储样式标签并将其附加到文档正文或在 shadow dom 内,具体取决于我的自定义元素的放置位置。

        index.html

        ...
        <body>
            <script src="main.js"></script>
            <div>
               #shadow-root(open)
               <my-custom-element></my-custom-element>
            </div>
        </body>
        ...
        

        webpack.config.js

        module.exports = {
            module: {
                rules: [
                    {
                        test: /\.tsx?$/,
                        use: 'ts-loader',
                        exclude: /node_modules/,
                    },
                    {
                        test: /\.scss$/,
                        use: [
                            {
                                loader: 'style-loader',
                                options: {
                                    injectType: 'singletonStyleTag',
                                    insert: function addToWindowObject(element) {
                                        const _window = typeof window !== 'undefined' ? window : {};
                                        if (!_window.myCustomElementStyles) {
                                            _window.myCustomElementStyles = [];
                                        }
                                        element.classList.add('my-custom-element-styles');
                                        _window.myCustomElementStyles.push(element);
                                    },
                                },
                            },
                            'css-loader',
                            {
                                loader: 'sass-loader',
                                options: {
                                    sassOptions: {
                                        outputStyle: 'compressed',
                                    },
                                },
                            },
                        ],
                        exclude: /node_modules/,
                    },
                ],
            },
            output: {
               filename: "main.js",
               path: path.resolve(__dirname, "dist"),
            },
            plugins: [...],
            ...
        };
        

        my-custom-element.ts

        import './styles.scss';
        
        export class MyCustomElement extends HTMLElement {
            constructor() {
               super();
            }
        
            connectedCallback() {
               const styletags = window['myCustomElementStyles'] as HTMLStyleElement[];
               const rootNode = this.getRootNode();
               if (rootNode instanceof ShadowRoot) {
                  rootNode.append(...styletags.map((tag) => tag.cloneNode(true)));
               } else if (rootNode instanceof Document) {
                  !document.getElementById('my-custom-element-styles') && document.head.append(...styletags);
               }
               ...
            }
        
            ...
            
        }
        
        customElements.define("my-custom-element", MyCustomElement);
        

        style.scss

        div {
            background: lightgreen;
        }
        

        【讨论】:

          猜你喜欢
          • 2021-04-30
          • 2020-12-14
          • 2019-03-29
          • 2021-10-12
          • 2018-05-25
          • 1970-01-01
          • 2017-09-01
          • 2018-07-28
          • 1970-01-01
          相关资源
          最近更新 更多