使用React动态加载样式表

36

我正在开发一个用于管理营销着陆页的CMS系统。在“编辑着陆页”视图中,我希望能够加载与用户正在编辑的着陆页相关联的样式表。如何在React中实现这样的功能?

我的应用程序完全基于React,是同构的,在Koa上运行。我所涉及页面的基本组件层次结构如下:

App.jsx (has `<head>` tag)
└── Layout.jsx (dictates page structure, sidebars, etc.)
    └── EditLandingPage.jsx (shows the landing page in edit mode)

EditLandingPageComponentDidMount函数中,异步获取着陆页数据(包括要加载的样式表路径)。

如果您需要其他信息,请告诉我。很想解决这个问题!

额外信息:我还想在离开页面时卸载样式表,我认为可以在ComponentWillUnmount中反过来执行与答案相反的操作,对吗?

8个回答

38

只需使用React的状态更新要动态加载的样式表路径。

import * as React from 'react';

export default class MainPage extends React.Component{
    constructor(props){
        super(props);
        this.state = {stylePath: 'style1.css'};
    }

    handleButtonClick(){
        this.setState({stylePath: 'style2.css'});
    }

    render(){
        return (
            <div>
                <link rel="stylesheet" type="text/css" href={this.state.stylePath} />
                <button type="button" onClick={this.handleButtonClick.bind(this)}>Click to update stylesheet</button>
            </div>
        )
    }
};

另外,我已将其实现为React组件。 您可以通过npm install react-dynamic-style-loader进行安装。
请检查我的Github存储库以检查:
https://github.com/burakhanalkan/react-dynamic-style-loader


我不想加载整个样式表,特别是因为我正在使用Rails,所以我根据你的答案做了一些hackery,有条件地添加了一个style标签。https://gist.github.com/siakaramalegos/eafd1b114ddcbe8fac923edbc9f8a553 - Sia
3
这对我所需的工作很好。我正在使用react-create-app,所以我必须将CSS移动到public文件夹中。 - cabaji99
@cabaji99,你的链接元素href是什么样子的?你是用"%PUBLIC_URL%/stylesheet_name.css"还是其他的方式? - Tur1ng
1
这对我也很有效,记住,如果您想从JavaScript代码内动态访问位于public文件夹中的样式表,应使用PUBLIC_URL环境变量,如下所示:<link rel='stylesheet' type='text/css' href={ process.env.PUBLIC_URL + '/foo.css' }/> - Andy
2
@MarcoPrins 您可以尝试使用 react-helmet 将内容渲染到 <head> 元素中。 - senornestor
显示剩余6条评论

18

我认为Burakhan的回答是正确的,但在body标签中加载<Link href = "" />有点奇怪。所以我认为它应该被修改为以下内容[我使用React hooks]:

import * as React from 'react';
export default MainPage = (props) => {
  const [ stylePath, setStylePath ] = useState("style1.css");
    
  const handleButtonClick = () => {
    setStylePath({stylePath: 'style2.css'});
  }

  useEffect(() => {
    var head = document.head;
    var link = document.createElement("link");

    link.type = "text/css";
    link.rel = "stylesheet";
    link.href = stylePath;

    head.appendChild(link);

    return () => { head.removeChild(link); }

  }, [stylePath]);

  return (
    <div>
      <button type="button" onClick={handleButtonClick}>
        Click to update stylesheet
      </button>
    </div>
  );
};

9
我认为你需要在 head.appendChild 节点下面加上 return () => { head.removeChild(link); } 来进行清理,否则当 stylePath 改变时,你只会不断添加节点。 - Emmanuel Touzery

8

这是Prime混合领域。首先我们将定义一个帮助程序来管理样式表。

我们需要一个函数来加载样式表,并返回其成功的承诺。实际上,要检测样式表的加载状态是非常疯狂的...

function loadStyleSheet(url){
  var sheet = document.createElement('link');
  sheet.rel = 'stylesheet';
  sheet.href = url;
  sheet.type = 'text/css';
  document.head.appendChild(sheet);
  var _timer;

  // TODO: handle failure
  return new Promise(function(resolve){
    sheet.onload = resolve;
    sheet.addEventListener('load', resolve);
    sheet.onreadystatechange = function(){
      if (sheet.readyState === 'loaded' || sheet.readyState === 'complete') {
        resolve();
      }
    };

    _timer = setInterval(function(){
      try {
        for (var i=0; i<document.styleSheets.length; i++) {
          if (document.styleSheets[i].href === sheet.href) resolve();
        } catch(e) { /* the stylesheet wasn't loaded */ }
      }
    }, 250);
  })
  .then(function(){ clearInterval(_timer); return link; });
}

哇,我原本只是想在上面放一个onload事件,但不行。这段代码未经测试,请在发现任何错误时更新它——它是从几篇博客文章编译而来的。

其余部分相当简单:

  • 允许加载样式表
  • 在可用时更新状态(以防止FOUC)
  • 在组件卸载时卸载任何已加载的样式表
  • 处理所有异步好处
var mixin = {
  componentWillMount: function(){
    this._stylesheetPromises = [];
  },
  loadStyleSheet: function(name, url){
    this._stylesheetPromises.push(loadStyleSheet(url))
    .then(function(link){
      var update = {};
      update[name] = true;
      this.setState(update);
    }.bind(this));
  },
  componentWillUnmount: function(){
    this._stylesheetPromises.forEach(function(p){
      // we use the promises because unmount before the download finishes is possible
      p.then(function(link){
        // guard against it being otherwise removed
        if (link.parentNode) link.parentNode.removeChild(link);
      });
    });
  }
};

再次提醒,此处未经测试,请如有问题及时更新。

现在我们已经拥有了该组件。

React.createClass({
  getInitialState: function(){
    return {foo: false};
  },
  componentDidMount: function(){
    this.loadStyleSheet('foo', '/css/views/foo.css');
  },
  render: function(){
    if (!this.state.foo) {
      return <div />
    }

    // return conent that depends on styles
  }
});

唯一需要完成的任务是在尝试加载样式表之前检查它是否已经存在。希望这能让您走上正确的道路。

哇,太棒了!我一定会试一试并让你知道它的效果如何。谢谢! - neezer
专注于在Firefox上进行测试,因为它是所有浏览器中支持最差的一个。 - Brigand
这很有帮助,但是有一些错别字(漏洞)。 - Hui-Yu
1
我不明白为什么不根据属性或状态更改项目内链接的href属性。 - jscul

3

我使用react-helmet,在render函数中...

{inject ? 
    <Helmet>
        <link rel="stylesheet" href="css/style.css" />
    </Helmet> : null}

1

https://www.npmjs.com/package/react-helmet

安装react-helmet并将其用于单独组件的动态CSS。例如,对于第一个组件,使用style1.css。

<>
     <Helmet>
         <link rel="stylesheet" href="/css/style1.css" />
     </Helmet>
     ...
</>

For 2nd component, using style2.css
<>
     <Helmet>
         <link rel="stylesheet" href="/css/style1.css" />
     </Helmet>
     ...
</>


1

这是我动态添加样式的方法:

import React, { Component } from "react";

class MyComponent extends Component {
    componentDidMount() {
        const cssUrl = "/public/assets/css/style.css";
        this.addStyle(cssUrl);
    }

    addStyle = url => {
        const style = document.createElement("link");
        style.href = url;
        style.rel = "stylesheet";
        style.async = true;

        document.head.appendChild(style);
    };

    render() {
        return <div> textInComponent </div>;
    }
}

export default MyComponent;

1
关于捆绑、缩小和缓存破坏怎么处理? - Victor Zakharov

0

在我的方法中,我使用了这个:

const TenantSelector = ({ children }) => {
  // imagine its value from a json config
  const config = {
      custom_style: 'css/tenant.css' 
  }
  require(`./assets/${config.custom_style}`)
  return (
    <>
      <React.Suspense fallback={<></>}>
      </React.Suspense>
      {children}
    </>
  )
}

ReactDOM.render(
  <TenantSelector>
   <YourApp>
  </TenantSelector>,
  document.getElementById("root")
)

请在您的代码中添加一些注释,说明它的功能以及如何回答问题。 - Cosmin Staicu

0

除了为样式表创建元素外,您还可以尝试根据某些条件导入基于CSS的样式表。ECMAScript提供了一个提案,使动态模块导入成为可能,其工作方式如下:

if (condition) {
  import('your css path here').then((condition) => {});
}

导入并不能解决主题切换时的问题,我们会遇到 CSS 重叠的情况。 - Vivek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接