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

Posted

技术标签:

【中文标题】使用 Rollup 和 scss 动态注入每个组件的样式标签【英文标题】:Inject per-component style tags dynamically with Rollup and scss 【发布时间】:2019-11-14 10:21:22 【问题描述】:

我正在构建一个 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...

当我的应用程序使用 webpack 和 style-loader 时,每个组件的 css 在第一次使用组件时动态地附加为 head 中的 style 标签。这是一个很好的性能提升,因为每个组件的样式在真正需要之前不需要被浏览器解析。

不过,现在我想使用 Rollup 分发我的库,因此应用程序使用者会执行以下操作:

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

// ...application code...

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

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

【问题讨论】:

【参考方案1】:

一种方法是将您的 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>
        </>
    );

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

    渲染两个 MyComponent 实例会导致样式表被注入两次,从而导致大量不必要的 DOM 注入 要包装每个组件需要大量样板文件(即使我们将 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 或类似的)。

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

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

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

目前的主流方法似乎是 JSS - 使用类似 nano-renderer 的东西将 JS 对象转换为 CSS,然后注入它们。对于文本 CSS,我似乎找不到任何可以做到这一点的东西。

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

【讨论】:

@o-t-w 我已经继续前进,没有时间尝试这个答案......如果应该接受这个答案,你能评论一下吗?

以上是关于使用 Rollup 和 scss 动态注入每个组件的样式标签的主要内容,如果未能解决你的问题,请参考以下文章

VueJS Rollup 导出多个 scss 资源

rollup开发自己的组件库(5)

如何使用环境变量在 Nuxt 中动态导入 SCSS 样式表

包裹反应16和每个组件SCSS

Parcel:为每个 React 组件使用单独的 SCSS 文件,但使用变量文件

尝试使用 rollup 和 vueJs 3 构建 vue 组件库