使用 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>
</>
);
}
这个基本想法应该可行,但我不会使用这个实现,原因有两个:
- 呈现两个
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()
重写钩子应该不难,但是那么集成应用程序将需要在根级别设置上下文提供程序,这会为集成商创建更多工作、更多文档等。
- 动态 adding/removing 样式标签的整个方法意味着样式表顺序不仅是不确定的(在使用像 Rollup 这样的打包器加载样式时已经是这样),而且可以在运行时改变,所以如果您有冲突的样式表,行为可能会在运行时发生变化。理想情况下,您的样式表的范围应该太小而不会发生冲突,但是我已经看到 Material UI 应用程序出现了问题,其中加载了 MUI 的多个实例 - 这真的很难调试!
目前占主导地位的方法似乎是 JSS - 使用类似 nano-renderer 的方法将 JS 对象转换为 CSS 然后注入它们。对于文本 CSS.
,我似乎找不到任何可以做到这一点的东西
希望这是一个有用的答案。我已经测试了钩子本身并且它工作正常,但我对 Rollup 并不完全有信心所以我依赖这里的插件文档。不管怎样,祝项目好运!
我正在构建一个 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>
</>
);
}
这个基本想法应该可行,但我不会使用这个实现,原因有两个:
- 呈现两个
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()
重写钩子应该不难,但是那么集成应用程序将需要在根级别设置上下文提供程序,这会为集成商创建更多工作、更多文档等。- 动态 adding/removing 样式标签的整个方法意味着样式表顺序不仅是不确定的(在使用像 Rollup 这样的打包器加载样式时已经是这样),而且可以在运行时改变,所以如果您有冲突的样式表,行为可能会在运行时发生变化。理想情况下,您的样式表的范围应该太小而不会发生冲突,但是我已经看到 Material UI 应用程序出现了问题,其中加载了 MUI 的多个实例 - 这真的很难调试!
目前占主导地位的方法似乎是 JSS - 使用类似 nano-renderer 的方法将 JS 对象转换为 CSS 然后注入它们。对于文本 CSS.
,我似乎找不到任何可以做到这一点的东西希望这是一个有用的答案。我已经测试了钩子本身并且它工作正常,但我对 Rollup 并不完全有信心所以我依赖这里的插件文档。不管怎样,祝项目好运!