PAYATHON 2026

如何动态添加 script 标签并执行 $(document).ready

支付老李

结论

不要使用 iframe 或 document.write(),也不要指望通过 innerHTML 插入的脚本自动执行。可以按下面的方式处理:

  1. 让 API 分别返回 <style>、HTML 和 <script>,不要把 CSS 与 JavaScript 混在一段无法区分的纯文本中。
  2. 使用 <template> 解析 HTML。
  3. 将样式和普通 HTML 插入当前页面。
  4. 重新创建解析出的 <script>,并按原有顺序执行。
  5. 在执行依赖 jQuery 的脚本前,确保 jQuery 已加载完成。

即使动态插入 <script> 时 DOMContentLoaded 已经触发,$(document).ready(...) 仍会执行,但前提是 jQuery 已正确加载。

先修正返回内容的格式

问题中的字符串只是把 CSS 和 JavaScript 拼接在了一起,没有真正的 <style> 和 <script> 标签:

const styleScriptText =
  ' #checkoutmodal .checkoutmodal-box{background:#FFF !important} $(document).ready(function() { console.log("test")}); ';

浏览器无法可靠地判断哪部分是 CSS,哪部分是 JavaScript。API 至少应返回结构清晰的内容:

const styleScriptText = `
  <style>
    #checkoutmodal .checkoutmodal-box {
      background: #FFF !important;
    }
  </style>

  <script>
    $(document).ready(function () {
      console.log("test");
    });
  </script>
`;

更合适的方式是直接返回结构化数据:

{
  "html": "<div id=\"checkoutmodal\">...</div>",
  "css": "#checkoutmodal .checkoutmodal-box { background: #FFF !important; }",
  "scripts": [
    {
      "src": "https://example.com/external-script.js"
    },
    {
      "code": "$(document).ready(function () { console.log('test'); });"
    }
  ]
}

结构化数据便于控制执行顺序,也省去了从字符串中区分 HTML、CSS 和 JavaScript 的麻烦。

动态插入并执行完整 HTML 字符串

通过 innerHTML 或 <template> 解析出来的 <script> 通常不会自动执行。要执行这些脚本,需要使用 document.createElement("script") 重新创建脚本节点。

下面的方法支持:

  • <style>
  • <link rel="stylesheet">
  • 普通 HTML
  • 外部 <script src="...">
  • 内联 <script>
  • 按出现顺序执行多个脚本
async function injectApiHtml(apiHtml, mountElement = document.body) {
  const template = document.createElement("template");
  template.innerHTML = apiHtml;

  // 保存 script 引用。它们插入页面后本身不会执行。
  const scripts = Array.from(
    template.content.querySelectorAll("script")
  );

  // 如果脚本依赖外部样式,可以等待 stylesheet 加载完成。
  const stylesheetPromises = Array.from(
    template.content.querySelectorAll('link[rel="stylesheet"]')
  ).map((link) => {
    return new Promise((resolve, reject) => {
      link.addEventListener("load", resolve, { once: true });
      link.addEventListener(
        "error",
        () => reject(new Error(`Stylesheet load failed: ${link.href}`)),
        { once: true }
      );
    });
  });

  // 插入普通 HTML、style、link 以及不会执行的原始 script。
  mountElement.appendChild(template.content);

  await Promise.all(stylesheetPromises);

  // 重新创建 script,确保按顺序执行。
  for (const oldScript of scripts) {
    await activateScript(oldScript);
  }
}

function activateScript(oldScript) {
  return new Promise((resolve, reject) => {
    const newScript = document.createElement("script");

    // 复制 type、src、integrity、crossorigin 等属性。
    for (const attribute of oldScript.attributes) {
      newScript.setAttribute(attribute.name, attribute.value);
    }

    const isExternal = oldScript.hasAttribute("src");
    const isModule =
      (oldScript.getAttribute("type") || "").trim().toLowerCase() === "module";

    // 动态外部脚本默认可能异步执行,设为 false 便于保持顺序。
    if (isExternal && !oldScript.hasAttribute("async")) {
      newScript.async = false;
    }

    if (!isExternal) {
      newScript.textContent = oldScript.textContent;
    }

    if (isExternal || isModule) {
      newScript.addEventListener("load", resolve, { once: true });
      newScript.addEventListener(
        "error",
        () => {
          reject(
            new Error(
              `Script load failed: ${
                oldScript.getAttribute("src") || "inline module"
              }`
            )
          );
        },
        { once: true }
      );
    }

    // 替换由 HTML 解析产生的惰性 script。
    oldScript.replaceWith(newScript);

    // 普通内联脚本在插入时同步执行。
    if (!isExternal && !isModule) {
      resolve();
    }
  });
}

调用方式如下:

try {
  await injectApiHtml(styleScriptText, document.getElementById("parentId"));
} catch (error) {
  console.error("Failed to install API content:", error);
}

加载外部脚本时,代码会等待当前脚本加载完成,再继续执行下一个脚本。因此,在下面的示例中,初始化代码会等到 jQuery 加载完成后再运行:

<script src="https://cdn.example.com/jquery.min.js"></script>

<script>
  $(document).ready(function () {
    console.log("test");
  });
</script>

为什么 $(document).ready() 可以在之后执行

jQuery 的 ready 机制不要求回调必须在 DOMContentLoaded 触发前注册。

如果 DOM 仍在加载,jQuery 会等待 DOM 就绪。如果 DOM 已经就绪,jQuery 会尽快安排回调执行。因此,动态加载下面的代码通常不会有问题:

$(document).ready(function () {
  console.log("test");
});

需要保证的是,执行这段代码时 window.jQuery 已经存在。如果 $ 被其他库占用,可以改成:

jQuery(function ($) {
  console.log("test");
});

脚本不依赖 jQuery 时,也可以使用原生实现:

function onDocumentReady(callback) {
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", callback, { once: true });
  } else {
    queueMicrotask(callback);
  }
}

onDocumentReady(function () {
  console.log("test");
});

为什么其他方式没有生效

直接设置 innerHTML:

element.innerHTML = apiHtml;

这种方式可以插入 <style> 和普通 HTML,但不能把其中的 <script> 当作可靠的自动执行机制。浏览器不会因为插入一段 HTML 就隐式执行其中的脚本。

document.write() 是用于页面解析阶段的旧式 API。在页面加载完成后调用它,可能会清空并替换当前文档。它还会受到跨域、解析阻塞和性能方面的浏览器限制,不适合用来加载 API 返回的动态内容。

必须注意的安全限制

执行 API 返回的脚本,就等于允许该 API 在当前页面中运行任意 JavaScript。这些脚本可以读取页面数据、访问 localStorage、发起请求、修改支付参数或跳转页面。

因此,需要满足以下条件:

  • 内容只能来自完全可信且经过身份验证的服务端。
  • 不要执行用户提交或第三方可编辑的字符串。
  • 最好使用允许列表限制外部脚本域名。
  • 对返回内容进行签名或完整性校验。
  • 不要用 eval() 或 new Function() 代替动态 <script>。
  • 页面启用 CSP 时,需要为动态脚本配置正确的 nonce 或允许的脚本来源。
  • 如果 CSP 禁止内联脚本,动态创建的内联 <script> 仍会被拦截。此时应将代码放入受信任的外部文件。

如果返回内容不可信,就需要使用 iframe 进行隔离。无法在允许未知脚本直接进入主应用上下文运行的同时,保证主应用安全。

备注:内容仅供参考。