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

alb*_*rtR 16 css shadow-dom reactjs webpack webpack-style-loader

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

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

我需要的是加载所有组件的样式并使它们在shadow root.

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

索引.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);
Run Code Online (Sandbox Code Playgroud)

App.ts // 常规反应组件

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

export const App: React.FunctionComponent = (props) => {
    return (
        <MockComponent />
    );
};
Run Code Online (Sandbox Code Playgroud)

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',
});
Run Code Online (Sandbox Code Playgroud)

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

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

use*_*2F7 6

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

 module: {
            rules: [
                {test:/\.css$/, use:'css-loader'}
            ]
        }
Run Code Online (Sandbox Code Playgroud)

然后你想导入一个我们从 css-loader 获得的样式字符串,并在你附加到容器之前的影子根的样式元素中使用它。所以在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);

    }
......
Run Code Online (Sandbox Code Playgroud)

'style-loader' 给你带来问题的原因是它是为了找到在最终的 html 文件中自己注入 css 样式标签的位置,但是你已经知道通过 JS 将它放在你的 shadowDOM 中的哪个位置,所以你只需要'css加载器'。


Jos*_*ann 6

有一种方法可以用于style-loader此目的

这一切都取决于执行顺序。你的index.tsx被执行后style-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>
...
Run Code Online (Sandbox Code Playgroud)

./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'
    })
  ],
};
Run Code Online (Sandbox Code Playgroud)

./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!"
Run Code Online (Sandbox Code Playgroud)

./src/style.css

#app-root {
    background: lightgreen;
}
Run Code Online (Sandbox Code Playgroud)

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


Flo*_*ann 6

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

索引.html

...
<body>
    <script src="main.js"></script>
    <my-custom-element></my-custom-element>
</body>
...
Run Code Online (Sandbox Code Playgroud)

./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"),
  },
};
Run Code Online (Sandbox Code Playgroud)

./src/util/style-loader.ts

const styleTags: HTMLLinkElement[] = [];
export const getStyleTags = () => styleTags.map(node => node.cloneNode(true));

export default function (linkTag) {
    styleTags.push(linkTag);
}
Run Code Online (Sandbox Code Playgroud)

./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 );
    }
  }
);
Run Code Online (Sandbox Code Playgroud)

./src/style.css

div {
    background: lightgreen;
}
Run Code Online (Sandbox Code Playgroud)