使用 Rollup 和 scss 动态注入每个组件的样式标签

Inject per-component style tags dynamically with Rollup and scss

我正在构建一个 React 组件库,其源代码采用以下通用结构:

- src
  - common.scss (contains things like re-usable css variables)
  - components
    - button
      - index.js
      - button.scss
    - dialog
      - index.js
      - dialog.scss

我的组件负责导入它们自己的组件样式(使用 scss),例如,button/index.js 有这一行:

import "./button.scss";

到目前为止,在我的应用程序中,我一直在像这样直接从源代码中使用我的库:

// app.js
import "mylib/src/common.scss" // load global styles
import Button from 'mylib/src/components/button/index.js'
import Dialog from 'mylib/src/components/dialog/index.js'

// ...application code...

当我的应用程序与 style-loader 一起使用 webpack 时,每个组件 css 在首次使用组件时动态附加为 head 中的 style 标签.这是一个很好的性能胜利,因为在实际需要之前浏览器不需要解析每个组件的样式。

虽然现在,我想使用 Rollup 分发我的库,所以应用程序消费者会做这样的事情:

import { Button, Dialog } from 'mylib'
import "mylib/common.css" // load global styles

// ...application code...

当我使用 rollup-plugin-scss 时,它只是将每个组件的样式捆绑在一起,而不是像以前那样动态添加它们。

有没有一种技术可以合并到我的 Rollup 构建中,以便我的每个组件样式在使用时作为 style 标签动态添加到 head 标签中?

一种方法是将您的 SCSS 作为 CSS 样式表字符串加载到插件中的 output:false 选项(参见 Options section of the docs),然后在您的组件使用 react-helmet 在运行时注入样式表:

import componentCss from './myComponent.scss'; // plain CSS from rollup plugin
import Helmet from 'react-helmet';

function MyComponent(props) {
    return (
        <>
             <ActualComponentStuff {...props} />
             <Helmet>
                 <style>{ componentCss }</style>
             </Helmet>
        </>
    );
}

这个基本想法应该可行,但我不会使用这个实现,原因有两个:

  1. 呈现两个 MyComponent 实例将导致样式表被注入两次,从而导致大量不必要的 DOM 注入
  2. 包装每个组件的样板很多(即使我们将 Helmet 实例分解成一个漂亮的包装器)

因此,您最好使用自定义挂钩,并传入一个 uniqueId 允许您的挂钩删除重复的样式表。像这样:

// -------------- myComponent.js -------------------
import componentCss from "./myComponent.scss"; // plain CSS from rollup plugin
import useCss from "./useCss";

function MyComponent(props) {
    useCss(componentCss, "my-component");
    return (
        <ActualComponentStuff {...props} />
    );
}

// ------------------ useCss.js ------------------
import { useEffect } from "react";

const cssInstances = {};

function addCssToDocument(css) {
    const cssElement = document.createElement("style");
    cssElement.setAttribute("type", "text/css");

    //normally this would be dangerous, but it's OK for
    // a style element because there's no execution!
    cssElement.innerHTML = css;
    document.head.appendChild(cssElement);
    return cssElement;
}

function registerInstance(uniqueId, instanceSymbol, css) {
    if (cssInstances[uniqueId]) {
        cssInstances[uniqueId].symbols.push(instanceSymbol);
    } else {
        const cssElement = addCssToDocument(css);
        cssInstances[uniqueId] = {
            symbols: [instanceSymbol],
            cssElement
        };
    }
}

function deregisterInstance(uniqueId, instanceSymbol) {
    const instances = cssInstances[uniqueId];
    if (instances) {
        //removes this instance by symbol
        instances.symbols = instances.symbols.filter(symbol => symbol !== instanceSymbol);

        if (instances.symbols.length === 0) {
            document.head.removeChild(instances.cssElement);
            instances.cssElement = undefined;
        }
    } else {
        console.error(`useCss() failure - tried to deregister and instance of ${uniqueId} but none existed!`);
    }
}

export default function useCss(css, uniqueId) {
    return useEffect(() => {
        // each instance of our component gets a unique symbol
        // to track its creation and removal
        const instanceSymbol = Symbol();

        registerInstance(uniqueId, instanceSymbol, css);

        return () => deregisterInstance(uniqueId, instanceSymbol);
    }, [css, uniqueId]);
}

这应该工作得更好 - 挂钩将有效地使用应用程序范围内的全局变量来跟踪组件的实例,在首次呈现时动态添加 CSS,并在最后一个组件消失时将其删除.你需要做的就是在你的每个组件中添加一个钩子作为额外的一行(假设你只使用函数 React 组件 - 如果你使用 类 你需要包装它们,也许使用HOC 或类似的)。

它应该可以正常工作,但也有一些缺点:

  1. 我们正在有效地使用全局状态(cssInstances,如果我们试图防止来自 React 树的不同部分的冲突,这是不可避免的。我希望会有是一种通过在 DOM 本身中存储状态来做到这一点的方法(考虑到我们的重复数据删除阶段是 DOM,这是有道理的),但我找不到。另一种方法是使用 React Context API 而不是模块级全局。这也可以正常工作并且更容易测试;如果你想要的话,用 useContext() 重写钩子应该不难,但是那么集成应用程序将需要在根级别设置上下文提供程序,这会为集成商创建更多工作、更多文档等。

    1. 动态 adding/removing 样式标签的整个方法意味着样式表顺序不仅是不确定的(在使用像 Rollup 这样的打包器加载样式时已经是这样),而且可以在运行时改变,所以如果您有冲突的样式表,行为可能会在运行时发生变化。理想情况下,您的样式表的范围应该太小而不会发生冲突,但是我已经看到 Material UI 应用程序出现了问题,其中加载了 MUI 的多个实例 - 这真的很难调试!

目前占主导地位的方法似乎是 JSS - 使用类似 nano-renderer 的方法将 JS 对象转换为 CSS 然后注入它们。对于文本 CSS.

,我似乎找不到任何可以做到这一点的东西

希望这是一个有用的答案。我已经测试了钩子本身并且它工作正常,但我对 Rollup 并不完全有信心所以我依赖这里的插件文档。不管怎样,祝项目好运!