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

Jon*_*ink 11 javascript sass reactjs rollupjs

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

- src
  - common.scss (contains things like re-usable css variables)
  - components
    - button
      - index.js
      - button.scss
    - dialog
      - index.js
      - dialog.scss
Run Code Online (Sandbox Code Playgroud)

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

import "./button.scss";
Run Code Online (Sandbox Code Playgroud)

到目前为止,在我的应用程序中,我一直在直接从源中使用我的库,如下所示:

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

当我的应用程序使用 webpack 和 时style-loader,每个组件的 css 会style在head首次使用该组件时作为标签动态添加。这是一个很好的性能优势,因为在实际需要之前,浏览器不需要解析每个组件的样式。

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

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

// ...application code...
Run Code Online (Sandbox Code Playgroud)

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

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

Dun*_*ker 4

One approach would be to load your SCSS as a CSS stylesheet string the output:false option in the plugin (see the Options section of the docs), then in your component use react-helmet to inject the stylesheet at runtime:

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

This basic idea should work, but I wouldn't use this implementation for 2 reasons:

  1. Rendering two instances of MyComponent will cause the stylesheet to be injected twice, causing lots of unnecessary DOM injection
  2. It's a lot of boilerplate to wrap around every component (even if we factor out our Helmet instance into a nice wrapper)

Therefore you're better off using a custom hook, and passing in a uniqueId that allows your hook to de-duplicate stylesheets. Something like this:

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

This should work much better - the hook will use effectively a app-wide global to track instances of your component, add the CSS dynamically when it gets first rendered, and remove it when the last component dies. All you need to do is add that single hook as an extra line in each of your components (assuming you're using only function React components - if you're using classes you'll need to wrap them, maybe using a HOC or similar).

It should work fine, but it also has some drawbacks:

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

    1. 动态添加/删除样式标签的整个方法意味着样式表顺序不仅是不确定的(在使用 Rollup 等捆绑器进行样式加载时已经是这样),而且还可以在运行时更改,因此如果样式表存在冲突,行为可能会在运行时发生变化。理想情况下,样式表的范围应该太窄,无论如何都不会发生冲突,但我已经看到加载了多个 MUI 实例的 Material UI 应用程序会出现这种问题 - 调试起来真的很困难!

目前占主导地位的方法似乎是 JSS - 使用纳米渲染器之类的东西将 JS 对象转换为 CSS,然后注入它们。我似乎找不到任何可以对文本 CSS 执行此操作的东西。

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