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

时间:2019-07-03 17:05:08

标签: javascript reactjs sass rollupjs

我正在构建一个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会在首次使用该组件时动态地附加为style中的head标签。这是一个不错的性能胜利,因为在实际需要之前,浏览器不需要解析每个组件的样式。

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

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

// ...application code...

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

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

1 个答案:

答案 0 :(得分:4)

一种方法是将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时动态添加CSS,并在最后一个组件死亡时将其删除。您需要做的就是在单个组件中的每个行中添加一个单独的钩子(假设您仅使用功能React组件-如果您使用的是类,则需要包装它们,也许使用HOC或类似的东西)。

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

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

    1. 动态添加/删除样式标签的整个方法意味着样式表的顺序不仅是不确定的(使用诸如Rollup之类的捆绑器进行样式加载时已经是不确定的),而且在运行时可能会发生变化,因此如果您使用样式表发生冲突时,行为可能会在运行时发生变化。理想情况下,您的样式表应该设置得过于狭窄,以至于无论如何都不会发生冲突,但是我发现Material UI应用程序在其中加载了MUI的多个实例时出现了问题-确实很难调试!

目前主要的方法似乎是JSS-使用类似nano-renderer的方法将JS对象转换为CSS,然后注入它们。对于文本CSS,似乎没有什么可以找到的。

希望这是一个有用的答案。我已经测试了钩子本身,并且可以正常工作,但是我对Rollup并不完全有信心,因此我依赖这里的插件文档。无论哪种方式,祝项目顺利!