我正在构建一个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
标签动态添加?
答案 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>
</>
);
}
这个基本想法应该可行,但是出于两个原因,我不会使用此实现:
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时动态添加CSS,并在最后一个组件死亡时将其删除。您需要做的就是在单个组件中的每个行中添加一个单独的钩子(假设您仅使用功能React组件-如果您使用的是类,则需要包装它们,也许使用HOC或类似的东西)。
它应该可以正常工作,但是它也有一些缺点:
我们正在有效地使用全局状态(cssInstances
,如果我们试图防止来自React树的不同部分的冲突,这是不可避免的。我希望有一种方法可以通过将状态存储在DOM本身中来做到这一点(考虑到我们的重复数据删除阶段是DOM,这很有意义),但我找不到一个方法,另一种方法是使用React Context API而不是模块级全局。这样也可以正常工作,并且更易于测试;如果您想要的话,应该不难用useContext()
重写钩子,但是集成应用程序将需要在根级别设置Context提供程序从而为集成商创造了更多工作,提供了更多文档等。
目前主要的方法似乎是JSS-使用类似nano-renderer的方法将JS对象转换为CSS,然后注入它们。对于文本CSS,似乎没有什么可以找到的。
希望这是一个有用的答案。我已经测试了钩子本身,并且可以正常工作,但是我对Rollup并不完全有信心,因此我依赖这里的插件文档。无论哪种方式,祝项目顺利!