ReactJS中JSX添加外链script标签未加载资源的原因及正确写法
React中插入script标签不加载执行的原因与正确实现
问题场景
在ReactJS应用中尝试通过JSX在页面指定位置插入两个script组件,以下三种实现方式都能将script元素成功添加到DOM树的预期位置,但浏览器都不会发起script标签src属性对应资源的HTTP请求,相关脚本也不会执行:
实现方式1
return ( <div dangerouslySetInnerHTML={{ __html: `<script src='https://path.to/first/script.js'></script><script src='https://path.to/second/script.js'></script>` }} /> );
实现方式2
return ( <div> <script src='https://path.to/first/script.js'></script> <script src='https://path.to/second/script.js'></script> </div> );
实现方式3
import Safe from 'react-safe'; return ( <div> <Safe.script src='https://path.to/first/script.js'></Safe.script> <Safe.script src='https://path.to/second/script.js'></Safe.script> </div> );
问题1:为什么上述方式插入的script标签不会自动加载执行?
这是浏览器原生安全机制和React DOM更新逻辑共同导致的:
- 浏览器层面的核心规则:只有HTML文档初始解析阶段插入的script标签,或者通过
document.write写入的script标签,才会被浏览器自动识别、加载并执行。页面初始加载完成后,通过innerHTML、appendChild这类常规DOM API动态插入的script标签,浏览器默认不会触发其加载和执行逻辑,这是浏览器防止恶意脚本随意注入执行的基础安全防护设计。 - React层面的渲染逻辑:不管是直接在JSX中写script标签,还是用
dangerouslySetInnerHTML、第三方类库插入script,本质都是React完成首次渲染后,通过DOM操作把script节点追加到已存在的DOM树上,完全不属于页面初始解析阶段的插入行为,自然不会触发浏览器的script加载执行逻辑。 - 与img等标签的表现差异:浏览器对img、link这类资源标签的加载规则没有上述限制,只要节点被插入DOM树,不管是初始解析阶段还是后续动态插入,都会自动触发src/href对应资源的请求,因此会出现和script行为不一致的情况。
问题2:React中正确插入并加载执行script标签的实现方式
核心思路是绕过React的渲染流程,手动通过原生DOM API创建script节点,配置好加载规则后插入到目标位置,常见可落地的实现有两种:
方式1:useEffect配合原生DOM操作(函数组件推荐)
在组件挂载完成后,手动创建script标签插入到目标容器,可按需处理加载成功、失败的回调,还能保证脚本执行顺序:
import { useEffect, useRef } from 'react'; export default function ScriptBlock() { const containerRef = useRef(null); useEffect(() => { const scriptList = [ 'https://path.to/first/script.js', 'https://path.to/second/script.js' ]; // 按顺序串行加载脚本,保证执行顺序和插入顺序一致 const loadAllScripts = async () => { for (const src of scriptList) { await new Promise((resolve, reject) => { // 避免重复加载同一份脚本 if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; } const scriptEl = document.createElement('script'); scriptEl.src = src; scriptEl.async = false; // 关闭异步加载,保证执行顺序 scriptEl.onload = resolve; scriptEl.onerror = reject; containerRef.current.appendChild(scriptEl); }); } // 所有脚本加载执行完成后的自定义逻辑可写在此处 }; loadAllScripts(); }, []); return <div ref={containerRef}></div>; }
方式2:封装通用脚本加载方法
如果需要在多个业务场景动态加载脚本,可以封装通用加载函数,不依赖组件渲染逻辑:
/** * 动态加载外部脚本 * @param {string} src 脚本地址 * @param {Object} config 脚本配置项,支持async、defer等原生script属性 * @param {HTMLElement} container 脚本插入的容器节点,默认插入body末尾 * @returns {Promise} 脚本加载完成的Promise */ function loadExternalScript(src, config = {}, container = document.body) { return new Promise((resolve, reject) => { // 重复加载校验 if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; } const scriptEl = document.createElement('script'); scriptEl.src = src; Object.entries(config).forEach(([key, value]) => { scriptEl[key] = value; }); scriptEl.onload = resolve; scriptEl.onerror = reject; container.appendChild(scriptEl); }); } // 使用示例:按顺序加载两个脚本 loadExternalScript('https://path.to/first/script.js', { async: false }) .then(() => loadExternalScript('https://path.to/second/script.js', { async: false })) .then(() => { // 脚本加载完成后的业务逻辑 });
注意:如果加载的是第三方来源的不可信脚本,务必提前做好安全校验,避免引发XSS安全风险。
内容的提问来源于stack exchange,提问作者SYNCRo
相关产品推荐
相关产品推荐

