【问题标题】:Adding script tag to React/JSX将脚本标签添加到 React/JSX
【发布时间】:2019-07-05 20:31:42
【问题描述】:

我有一个相对简单的问题是尝试将内联脚本添加到 React 组件。到目前为止我所拥有的:

'use strict';

import '../../styles/pages/people.scss';

import React, { Component } from 'react';
import DocumentTitle from 'react-document-title';

import { prefix } from '../../core/util';

export default class extends Component {
    render() {
        return (
            <DocumentTitle title="People">
                <article className={[prefix('people'), prefix('people', 'index')].join(' ')}>
                    <h1 className="tk-brandon-grotesque">People</h1>
                    
                    <script src="https://use.typekit.net/foobar.js"></script>
                    <script dangerouslySetInnerHTML={{__html: 'try{Typekit.load({ async: true });}catch(e){}'}}></script>
                </article>
            </DocumentTitle>
        );
    }
};

我也试过了:

<script src="https://use.typekit.net/foobar.js"></script>
<script>try{Typekit.load({ async: true });}catch(e){}</script>

这两种方法似乎都无法执行所需的脚本。我猜这是我错过的一件简单的事情。有人可以帮忙吗?

PS:忽略 foobar,我有一个实际使用的真实 ID,我不想分享。

【问题讨论】:

  • 是否有特定的动机通过 React 加载而不是将其包含在基本页面 HTML 中?即使这确实有效,这也意味着您每次安装组件时都会重新插入脚本。
  • 是这样吗?我认为 DOM diffing 会使情况并非如此,但我承认这将取决于 DocumentTitle 的实现。
  • 正确的@loganfsmyth,如果下一个状态也有脚本,React 不会在重新渲染时重新加载脚本。
  • 这里是why this happens

标签: javascript reactjs ecmascript-6


【解决方案1】:

编辑:事情变化很快,这已经过时了 - 请参阅更新


您想在每次渲染此组件时一次又一次地获取并执行脚本,还是仅在此组件安装到 DOM 时一次?

也许可以试试这样的:

componentDidMount () {
    const script = document.createElement("script");

    script.src = "https://use.typekit.net/foobar.js";
    script.async = true;

    document.body.appendChild(script);
}

但是,只有当您要加载的脚本不能作为模块/包提供时,这才真正有用。首先,我总是:

  • npm 上查找包
  • 在我的项目中下载并安装包 (npm install typekit)
  • import 我需要的包 (import Typekit from 'typekit';)

这可能是您从示例中安装包reactreact-document-title 的方式,并且有一个Typekit package available on npm


更新:

现在我们有了钩子,更好的方法可能是像这样使用useEffect

useEffect(() => {
  const script = document.createElement('script');

  script.src = "https://use.typekit.net/foobar.js";
  script.async = true;

  document.body.appendChild(script);

  return () => {
    document.body.removeChild(script);
  }
}, []);

这使它成为自定义挂钩的绝佳候选者(例如:hooks/useScript.js):

import { useEffect } from 'react';

const useScript = url => {
  useEffect(() => {
    const script = document.createElement('script');

    script.src = url;
    script.async = true;

    document.body.appendChild(script);

    return () => {
      document.body.removeChild(script);
    }
  }, [url]);
};

export default useScript;

可以这样使用:

import useScript from 'hooks/useScript';

const MyComponent = props => {
  useScript('https://use.typekit.net/foobar.js');

  // rest of your component
}

【讨论】:

  • 我认为 TypeKit 的“高级”实现更适合这种方法。
  • 这确实有效 - 加载脚本,但我怎样才能访问脚本中的代码。例如,我想调用一个存在于脚本中的函数,但我无法在加载脚本的组件中调用它。
  • 当脚本附加到页面时,它将正常执行。例如,如果您使用此方法从 CDN 下载 jQuery,则在 componentDidMount 函数下载并将脚本附加到页面后,您将拥有全局可用的 jQuery$ 对象(即: window)。
  • 我在使用身份验证脚本时遇到了类似的问题,结果发现最好将它包含在根目录的 html 文件中,在 react App.js 之上的一层。万一有人觉得这很有用。正如@loganfsmith 提到的......
  • 为什么是return () =&gt; {document.body.removeChild(script);}?为什么需要 removeChild 的返回
【解决方案2】:

我为这个特定案例创建了一个 React 组件:https://github.com/coreyleelarson/react-typekit

只需将您的 Typekit Kit ID 作为道具传入即可。

import React from 'react';
import Typekit from 'react-typekit';

const HtmlLayout = () => (
  <html>
    <body>
      <h1>My Example React Component</h1>
      <Typekit kitId="abc123" />
    </body>
  </html>
);

export default HtmlLayout;

【讨论】:

    【解决方案3】:

    除了上面的答案,你可以这样做:

    import React from 'react';
    
    export default class Test extends React.Component {
      constructor(props) {
        super(props);
      }
    
      componentDidMount() {
        const s = document.createElement('script');
        s.type = 'text/javascript';
        s.async = true;
        s.innerHTML = "document.write('This is output by document.write()!')";
        this.instance.appendChild(s);
      }
    
      render() {
        return <div ref={el => (this.instance = el)} />;
      }
    }
    

    div 绑定到this 并注入脚本。

    演示可以在codesandbox.io找到

    【讨论】:

    • this.instance 对我不起作用,但 document.body.appendChild 对我来说是 Alex McMillan 的回答。
    • 您可能没有将this.instance 绑定到您的渲染方法中的引用。我添加了一个演示链接来展示它的工作原理
    • @ShubhamKushwah 你一定是在做服务器端渲染?
    • @ArrayKnight 是的,我后来发现在服务器上这些对象不存在:documentwindow。所以我更喜欢使用 npm global
    • s.async = true 有什么需要,我找不到任何关于它的参考资料,要知道它的目的,你能解释一下吗
    【解决方案4】:

    解决方案取决于场景。就像我的情况一样,我必须在反应组件中加载一个日历嵌入。

    Calendly 查找一个 div 并从它的 data-url 属性中读取并在该 div 中加载一个 iframe。

    第一次加载页面时一切都很好:首先,呈现带有data-url 的div。然后将日历脚本添加到正文中。浏览器下载并评估它,我们都高兴地回家了。

    当您离开然后返回页面时,问题就来了。这次脚本仍在正文中,浏览器不会重新下载和重新评估它。

    修复:

    1. componentWillUnmount 上查找并删除脚本元素。然后在重新挂载时,重复上述步骤。
    2. 输入$.getScript。它是一个漂亮的 jquery 助手,它接受一个脚本 URI 和一个成功回调。一旦加载了脚本,它就会评估它并触发您的成功回调。我所要做的就是在我的componentDidMount$.getScript(url) 中。我的 render 方法已经有了日历 div。而且运行顺畅。

    【讨论】:

    • 添加 jQuery 来做到这一点是个坏主意,而且你的情况对你来说非常具体。实际上,添加 Calendly 脚本一次没有任何问题,因为我确信 API 有重新检测调用。一遍又一遍地删除和添加脚本是不正确的。
    • @sidonaldson 如果你要维护一个项目,它的架构组合不同的框架(和库)不仅仅是反应,否则我们需要使用原生 js 来访问组件
    【解决方案5】:

    Alex Mcmillan 提供的答案对我帮助最大,但对于更复杂的脚本标签却不太适用。

    我稍微调整了他的答案,为具有各种功能的长标签提出了一个解决方案,该标签还已经设置了“src”。

    (对于我的用例,脚本需要放在 head 中,这也反映在此处):

      componentWillMount () {
          const script = document.createElement("script");
    
          const scriptText = document.createTextNode("complex script with functions i.e. everything that would go inside the script tags");
    
          script.appendChild(scriptText);
          document.head.appendChild(script);
      }
    

    【讨论】:

    • 如果您只是将内联 JS 转储到页面上,我完全不明白您为什么要使用 React...?
    • 您需要在代码中添加document.head.removeChild(script);,否则只要用户访问此页面路径,您的html就会创建无限数量的脚本标签
    【解决方案6】:

    我最喜欢的方式是使用 React Helmet——它是一个允许以您可能已经习惯的方式轻松操作文档头部的组件。

    例如

    import React from "react";
    import {Helmet} from "react-helmet";
    
    class Application extends React.Component {
      render () {
        return (
            <div className="application">
                <Helmet>
                    <script src="https://use.typekit.net/foobar.js"></script>
                    <script>try{Typekit.load({ async: true });}catch(e){}</script>
                </Helmet>
                ...
            </div>
        );
      }
    };
    

    https://github.com/nfl/react-helmet

    【讨论】:

    • 这是迄今为止最好的解决方案。
    • 不幸的是,它不起作用...见codesandbox.io/s/l9qmrwxqzq
    • @Darkowic,我通过将 async="true" 添加到将 jQuery 添加到代码中的 &lt;script&gt; 标记来使您的代码正常工作。
    • @SomaMbadiwe 为什么它与async=true 一起工作而没有它会失败?
    • 试过这个,对我不起作用。我不推荐使用 react-helmet,唯一的原因是它会在脚本中注入额外的属性,而这些属性是无法删除的。这实际上破坏了某些脚本,并且维护人员已经多年没有修复它,并且拒绝github.com/nfl/react-helmet/issues/79
    【解决方案7】:

    如果您需要在 SSR(服务器端渲染)中使用 &lt;script&gt; 块,使用 componentDidMount 的方法将不起作用。

    您可以改用react-safe 库。 React 中的代码将是:

    import Safe from "react-safe"
    
    // in render 
    <Safe.script src="https://use.typekit.net/foobar.js"></Safe.script>
    <Safe.script>{
      `try{Typekit.load({ async: true });}catch(e){}`
    }
    </Safe.script>
    

    【讨论】:

    • 这和 `

      `
      相同
    【解决方案8】:

    使用Range.createContextualFragment 有一个非常好的解决方法。

    /**
     * Like React's dangerouslySetInnerHTML, but also with JS evaluation.
     * Usage:
     *   <div ref={setDangerousHtml.bind(null, html)}/>
     */
    function setDangerousHtml(html, el) {
        if(el === null) return;
        const range = document.createRange();
        range.selectNodeContents(el);
        range.deleteContents();
        el.appendChild(range.createContextualFragment(html));
    }
    

    这适用于任意 HTML,并且还保留上下文信息,例如 document.currentScript

    【讨论】:

    • 您能与使用示例合作吗?对我来说,它不适用于传递脚本和正文,例如..
    【解决方案9】:

    您可以使用npm postscribe 在 react 组件中加载脚本

    postscribe('#mydiv', '<script src="https://use.typekit.net/foobar.js"></script>')
    

    【讨论】:

    • 解决了我的问题
    【解决方案10】:

    对于多个脚本,使用这个

    var loadScript = function(src) {
      var tag = document.createElement('script');
      tag.async = false;
      tag.src = src;
      document.getElementsByTagName('body').appendChild(tag);
    }
    loadScript('//cdnjs.com/some/library.js')
    loadScript('//cdnjs.com/some/other/library.js')
    

    【讨论】:

      【解决方案11】:
      componentDidMount() {
        const head = document.querySelector("head");
        const script = document.createElement("script");
        script.setAttribute(
          "src",
          "https://assets.calendly.com/assets/external/widget.js"
        );
        head.appendChild(script);
      }
      

      【讨论】:

        【解决方案12】:

        您可以在以下链接找到最佳答案:

        https://cleverbeagle.com/blog/articles/tutorial-how-to-load-third-party-scripts-dynamically-in-javascript

        const loadDynamicScript = (callback) => {
        const existingScript = document.getElementById('scriptId');
        
        if (!existingScript) {
            const script = document.createElement('script');
            script.src = 'url'; // URL for the third-party library being loaded.
            script.id = 'libraryName'; // e.g., googleMaps or stripe
            document.body.appendChild(script);
        
            script.onload = () => {
              if (callback) callback();
            };
          }
        
          if (existingScript && callback) callback();
        };
        

        【讨论】:

        • document.getElementById('scriptId'); 不应该是document.getElementById('libraryName');
        【解决方案13】:

        根据Alex McMillan的解决方案,我有以下改编。
        我自己的环境:React 16.8+,下一个 v9+

        // 添加一个名为 Script 的自定义组件
        // 钩子/Script.js

        import { useEffect } from 'react'
        
        
        // react-helmet don't guarantee the scripts execution order
        export default function Script(props) {
        
          // Ruels: alwasy use effect at the top level and from React Functions
          useEffect(() => {
            const script = document.createElement('script')
        
            // src, async, onload
            Object.assign(script, props)
        
            let { parent='body' } = props
        
            let parentNode = document.querySelector(parent)
            parentNode.appendChild(script)
        
            return () => {
              parentNode.removeChild(script)
            }
          } )
        
          return null  // Return null is necessary for the moment.
        }
        

        // 使用自定义组件,只需导入它并将旧的小写 &lt;script&gt; 标记替换为自定义驼峰式 &lt;Script&gt; 标记就足够了。 // index.js

        import Script from "../hooks/Script";
            
        <Fragment>
          {/* Google Map */}
          <div ref={el => this.el = el} className="gmap"></div>
        
          {/* Old html script */}
          {/*<script type="text/javascript" src="http://maps.google.com/maps/api/js"></script>*/}
        
          {/* new custom Script component */}
          <Script async={false} type="text/javascript" src='http://maps.google.com/maps/api/js' />
        </Fragment>
        

        【讨论】:

        • 这个组件有一点需要注意:这个Script组件只能保证它自己兄弟的顺序。如果您在同一页面的多个组件中多次使用此组件,则脚本块可能会乱序。原因是所有脚本都是由 document.body.appendChild 以编程方式而不是声明方式插入的。好吧,头盔将所有脚本标签移动到head标签中,这不是我们想要的。
        • 嘿@sully,我的问题是将脚本分别添加到DOM中,到目前为止我看到的最佳解决方案是在组件卸载期间,删除子元素(即
        【解决方案14】:

        聚会有点晚了,但我在查看@Alex Macmillan 的答案后决定创建自己的,那就是传递两个额外的参数;放置脚本的位置,例如 or 并将异步设置为 true/false,这里是:

        import { useEffect } from 'react';
        
        const useScript = (url, position, async) => {
          useEffect(() => {
            const placement = document.querySelector(position);
            const script = document.createElement('script');
        
            script.src = url;
            script.async = typeof async === 'undefined' ? true : async;
        
            placement.appendChild(script);
        
            return () => {
              placement.removeChild(script);
            };
          }, [url]);
        };
        
        export default useScript;
        

        调用它的方式与本文接受的答案中显示的完全相同,但有两个额外的(再次)参数:

        // First string is your URL
        // Second string can be head or body
        // Third parameter is true or false.
        useScript("string", "string", bool);
        

        【讨论】:

          【解决方案15】:

          你也可以使用 react 头盔

          import React from "react";
          import {Helmet} from "react-helmet";
          
          class Application extends React.Component {
            render () {
              return (
                  <div className="application">
                      <Helmet>
                          <meta charSet="utf-8" />
                          <title>My Title</title>
                          <link rel="canonical" href="http://example.com/example" />
                          <script src="/path/to/resource.js" type="text/javascript" />
                      </Helmet>
                      ...
                  </div>
              );
            }
          };
          

          Helmet 采用纯 HTML 标签并输出纯 HTML 标签。它非常简单,而且对 React 初学者很友好。

          【讨论】:

            【解决方案16】:

            要在头标签&lt;head&gt; 中添加脚本标签或代码,请使用react-helmet package。它很轻并且有很好的文档。

            在body里面的script标签中添加Js代码,

                function htmlDecode(html) {
                  return html.replace(/&([a-z]+);/ig, (match, entity) => {
                    const entities = { amp: '&', apos: '\'', gt: '>', lt: '<', nbsp: '\xa0', quot: '"' };
                    entity = entity.toLowerCase();
                    if (entities.hasOwnProperty(entity)) {
                      return entities[entity];
                    }
                    return match;
                  });
                }
              render() {
                const scriptCode = `<script type="text/javascript">
                      {(function() {
                      window.hello={
                        FIRST_NAME: 'firstName',
                        LAST_NAME: 'lastName',
                      };
                      })()}
                      </script>`
                return(
                  <div dangerouslySetInnerHTML={{ __html: this.htmlDecode(scriptCode) }} />;
                );
              }
            
            

            此代码可以被console.log(windows.hello)测试

            【讨论】:

            • 在打字稿中这一行return entities[entity]; 导致编译错误。
            【解决方案17】:

            这个答案解释了这种行为背后的原因

            渲染 script 标记的任何方法都无法按预期工作:

            1. script 标记用于外部脚本
            2. 使用dangerouslySetInnerHTML

            为什么

            React DOM(react on web 的渲染器)使用 createElement 调用将 JSX 渲染为 DOM 元素。

            createElement 使用innerHTML DOM API 最终将这些添加到DOM (see code in React source)。 innerHTML does not execute script 标签作为安全考虑添加。这就是为什么反过来在 React 中渲染 script 标签不能按预期工作的原因。

            关于如何在 React 中使用 script 标签,请查看此页面上的其他答案。

            【讨论】:

              【解决方案18】:

              我尝试编辑@Alex McMillan 接受的答案,但它不允许我这样做,所以这里有一个单独的答案,您可以在其中获得加载的库的价值。人们要求的一个非常重要的区别,我我使用 stripe.js 实现所需的。

              useScript.js

              import { useState, useEffect } from 'react'
              
              export const useScript = (url, name) => {
              
                const [lib, setLib] = useState({})
              
                useEffect(() => {
                  const script = document.createElement('script')
              
                  script.src = url
                  script.async = true
                  script.onload = () => setLib({ [name]: window[name] })
              
                  document.body.appendChild(script)
              
                  return () => {
                    document.body.removeChild(script)
                  }
                }, [url])
              
                return lib
              
              }
              

              用法看起来像

              const PaymentCard = (props) => {
                const { Stripe } = useScript('https://js.stripe.com/v2/', 'Stripe')
              }
              

              注意: 将库保存在对象中,因为库通常是一个函数,React 会在存储状态时执行该函数以检查更改——这会破坏期望使用特定参数调用的库(如 Stripe)——因此我们将其存储在一个对象中以对 React 隐藏它并保护库函数不被调用。

              【讨论】:

                【解决方案19】:

                这是我最终能够在我的 React JS 代码中添加两个外部 JavaScript 文件的方法:

                这些是我遵循的步骤。

                第 1 步: 我在我的 react-app 文件夹路径中使用 终端 中的 npm i react-helmet 安装了 React-Helmet

                第 2 步: 然后我在代码中添加了import {Helmet} from "react-helmet"; 标头。

                第 3 步: 最后,在我的代码中,这是 我如何使用 Helment

                添加外部 JS 文件
                <Helmet>
                    <script src = "path/to/my/js/file1.js" type = "text/javascript" />
                    <script src = "path/to/my/js/file2.js" type = "text/javascript" />  
                </Helmet>
                

                【讨论】:

                  【解决方案20】:

                  只需在html文件中添加正文

                  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
                  

                  【讨论】:

                    【解决方案21】:

                    您可以在调用 react 之前将脚本放入 Html 文件中。

                    【讨论】:

                      【解决方案22】:

                      老实说,对于 React - 不要为在标题中添加 &lt;script&gt; 标签而烦恼。当它们完全加载时获得回调是一件很痛苦的事情。相反,请使用 @charlietango/useScript 之类的包在需要时加载脚本,并在完成时获取状态更新。

                      示例用法:

                      import React from 'react'
                      import useScript, { ScriptStatus } from '@charlietango/use-script'
                       
                      const Component = () => {
                        const [ready, status] = useScript('https://api.google.com/api.js')
                       
                        if (status === ScriptStatus.ERROR) {
                          return <div>Failed to load Google API</div>
                        }
                       
                        return <div>Google API Ready: {ready}</div>
                      }
                       
                      export default Component
                      

                      PS。如果您使用 redux 在脚本加载时告诉其他组件,并且像我一样使用 redux-persist,请不要忘记在您的 redux-persist 设置中包含一个修饰符,该修饰符始终将脚本加载的 redux 值设置为 false在 redux 备份中。

                      【讨论】:

                        【解决方案23】:

                        我看到了同样的问题,直到我找到了这个包,很容易实现,我希望它对我有用:)

                        https://github.com/gumgum/react-script-tag

                        import React from 'react';
                        import Script from '@gumgum/react-script-tag';
                        
                        import './App.css';
                        
                        function App() {
                          return (
                            <div >
                        
                              <h1> Graphs</h1>
                              <div class="flourish-embed flourish-network" data-src="visualisation/8262420">
                                <Script  src"your script"
                                </Script>
                              </div>
                            </div>
                          );
                        }
                        
                        export default App;
                        

                        【讨论】:

                        • 正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center
                        猜你喜欢
                        • 2019-11-20
                        • 2018-01-25
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2014-07-22
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多