Javascript 稍后动态插入:如何实现 运行?

Javascript dynamically inserted later on: how to make it run?

我的 React 应用程序中有脚本,稍后会动态插入。脚本未加载。

在我的数据库中有一个名为 content 的字段,其中包含包含 html 和 javascript 的数据。有很多条记录,每条记录可以在 content 字段中包含多个脚本。因此,在我的 React 应用程序中静态指定每个脚本 url 并不是一个真正的选项。例如,记录字段可能如下所示:

<p>Some text and html</p>
<div id="xxx_hype_container">
    <script type="text/javascript" charset="utf-8" src="https://example.com/uploads/hype_generated_script.js?499892"></script>
</div>
<div style="display: none;" aria-hidden="true"> 
<div>Some text.</div> 
Etc…

我使用 dangerouslySetInnerHTML:

在我的 React 应用程序中调用此字段
render() {
    return (
        <div data-page="clarifies">
            <div className="container">
                <div dangerouslySetInnerHTML={{ __html: post.content }} />
                ... some other data
            </div>
        </div>
    );
}

它正确地从数据库加载数据并显示该数据的 html。但是,Javascript 不会被执行。我认为该脚本不起作用,因为它是稍后动态插入的。 如何制作这些脚本work/run?

This post 建议动态插入脚本的解决方案,但我不认为我可以应用此解决方案,因为在我的情况下 script/code 是从数据库插入的(那么如何使用nodeScriptReplace 在代码上...?)。有什么建议可以让我的脚本正常工作吗?


更新 回应@lissettdm 他们的回答:

constructor(props) {
    this.ref = React.createRef();
}

componentDidUpdate(prevProps, prevState) {
    if (prevProps.postData !== this.props.postData) {
        this.setState({
            loading: false,
            post: this.props.postData.data,
            //etc
        });
        setTimeout(() => parseElements());

        console.log(this.props.postData.data.content);
        // returns html string like: `<div id="hype_container" style="margin: auto; etc.`
        const node = document.createRange().createContextualFragment(this.props.postData.data.content);
        console.log(JSON.stringify(this.ref));
        // returns {"current":null}
        console.log(node);
        // returns [object DocumentFragment]
        this.ref.current.appendChild(node);
        // produces error "Cannot read properties of null"
    }
}

render() {
    const { history } = this.props;
    /etc.
    return (
        {loading ? (
            some code
        ) : (
            <div data-page="clarifies">
                <div className="container">
                    <div ref={this.ref}></div>
                    ... some other data
                </div>
            </div>
        );
    );
}

this.ref.current.appendChild(node); 行产生错误:

TypeError: Cannot read properties of null (reading 'appendChild')

在没有 React 推荐方法的情况下渲染原始 HTML 不是一个好习惯。 React 推荐方法 dangerouslySetInnerHTML 来渲染原始 HTML.

有多种方法可以做到这一点。您可以创建一个函数,调用该函数可以动态创建 <script> 标签并将其注入到 React 应用程序的 <body> 中。

const addScript = () => {
    const script = document.createElement("script");
    script.src = '<url-of-the-script>';
    script.async = true;
    script.onload = function() {
        // Do something
    };
    document.head.appendChild(script);
}

您可以在使用 useEffect 挂钩加载所需组件时调用此 addScript 函数。

useEffect(() => {
    addScript();
    return () => {
        // remove the script on component unmount
    };
}, []);

您需要先使用提取调用从数据库中提取动态数据,然后使用 useEffect,在提取数据后,在其中将其设置为 useState 挂钩变量,该变量将为此保存数据。

const [dbData,setDbData] = useState([]);

useEffect(()=>{
  const dbReqFunc = async () => {
    // req your dynamic data here and set the data fetched to dbData
    // using setDbData(...)
  };

  dbReqFunc();
},[]);  // Making the call on component loading (Modify accordingly based on 
        // needs)

在此之后,一旦设置了数据,您就可以使用另一个 useEffect 挂钩,它应该在前一个 useEffect 挂钩的下方。然后,您可以为获取的整个动态 URL 调用函数并将其附加到 HTML 文档(我已将所有脚本附加到 div 以便于清理)。

useEffect(()=>{
  if(dbData.length>0){
    const elem = document.createElement('div');
    dbData.map(data=> {

        // based on data returned modify accordingly

        const script = document.createElement("script");
        script.src = '...script-url...';
        script.async = true;
        script.onload = function() {
          // Do something
        };
     
        //...other script fields...       

        elem.appendChild(script);
      });
     
     // attach elem to body
     document.body.appendChild(elem);

     return ()=>{
      // clean-up function
        document.body.removeChild(elem);
     };
  }

},[dbData]);

这应该加载脚本数据并且应该加载脚本。

注意:确保在第二次 useEffect 之前放置用于获取数据的动态数据库调用。所以,它按顺序运行。

如果您确定 HTML 字符串内容是安全的并且包含具有有效 HTML 的字符串,您可以使用 Range.createContextualFragment()(执行脚本)

function App() {
  const ref = useRef();

  useEffect(() => {
    /* convert your HTML string into DocumentFragment*/
    const node = document.createRange().createContextualFragment(HTML);
    ref.current.appendChild(node);
  }, []);

  return (
    <div>
      <h1>HTML String</h1>
      <div>
        <div ref={ref}></div>
      </div>
    </div>
  );
}

查看 script 内容如何在 JavaScript 控制台 working example

上执行

如果您使用 class component 在 class 构造函数中创建 ref,然后更新节点内容,我在 componentDidMount 中这样做只是为了测试:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.ref = React.createRef();
  }

  componentDidMount() {
    const node = document.createRange().createContextualFragment(HTML);
    this.ref.current.appendChild(node);
  }

  render() {
    return (
      <div>
        <h1>HTML String</h1>
        <div>
          <div ref={this.ref}></div>
        </div>
      </div>
    );
  }
}

看到这个working example