我有一个相对简单的问题,试图将内联脚本添加到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实际上在使用,我不想分享。


当前回答

使用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。

其他回答

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

任何渲染脚本标签的方法都不能像预期的那样工作:

为外部脚本使用script标记 使用dangerouslySetInnerHTML

Why

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

createElement使用innerHTML DOM API最终将这些添加到DOM(参见React源代码中的代码)。innerHTML不执行作为安全考虑而添加的脚本标记。这就是为什么React中的渲染脚本标签不能按预期工作的原因。

关于如何在React中使用脚本标签,请查看本页的其他答案。

使用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。

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);
}

我试图编辑@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并保护库函数不被调用。

如果你需要在SSR(服务器端渲染)中有<script>块,使用componentDidMount的方法将不起作用。

您可以使用反应安全库代替。 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>