通过 Webpack 将 CSS 样式注入到 shadow root

Posted

技术标签:

【中文标题】通过 Webpack 将 CSS 样式注入到 shadow root【英文标题】:Inject CSS styles to shadow root via Webpack 【发布时间】: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 注入样式,但效果不佳。

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

【问题讨论】:

【参考方案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;

【讨论】:

以上是关于通过 Webpack 将 CSS 样式注入到 shadow root的主要内容,如果未能解决你的问题,请参考以下文章

webpack-- 样式加载

无法使用 `sass-loader` 内联样式

html 通过jQuery进行css样式注入。基本上调用body / head附加一个样式标记,你自己的css将添加一个新的标记和你的样式

webpack 样式模块打包深入学习

vue+webpack+element打包后线上样式不一致

Webpack实战:轻松读懂Webpack如何分离样式文件