单页应用下实现动态的脚本加载

August 2017 · 2 minute read

之前在写自己的博客框架的时候遇到了一个问题:文章中的 script 标签没有任何作用,以 Vue 为 MVVM 框架举个例子:

<body>
  <div id="app">
    <p v-html="html"></p>
  </div>
  <script>
    var vm = new Vue({
      el: "#app",
      data: {
        html: '<script> console.log("JavaScript is awesome"); <\/script>',
      },
    });
  </script>
</body>

这段代码的意思很简单,创建一个 ID 为 app 的 DIV,其中包含一个 p 标签,vm 实例化之后,p 的内容就是 data 字段的 html,渲染结束后效果应该是这样:

<body>
  <div id="app">
    <p>
      <script>
        console.log("JavaScript is awesome");
      </script>
    </p>
  </div>
  <script>
    // emmmm
  </script>
</body>

好吧……看起来这代码很不规矩,但是浏览器应该是接受在任意位置出现的 script 标签的,也就是按照预期,我们会在控制台里看到 JavaScript is awesome 这句话,没毛病。

然而实际结果是……并没有。原因很简单,v-html 是通过 innerHTML 来实现的,而 HTML5 的标准规定了通过 innerHTML 得到的 script 标签不会被执行。此举的目的是为了防止利用这个特性来进行 XSS 攻击,但是依然有不少场景需要使用这些脚本,所以我们需要一些 trick 来让浏览器加载这些脚本。与此同时,如果 script 是通过 appendChild 添加到 DOM,这段脚本是能被正常执行的。

所以解决方案也就很简单了:找到需要执行的 script 标签,然后通过 createElement 创建空 script,并将待执行标签的数据复制到空 script 中(比如 src,或者 innerHTML),最后通过 appendChild 将它添加到 DOM 中去。

其中,找到 script 标签的方法比较多,比如可以通过正则对字符串进行匹配。不过考虑到通过 innerHTML 设置的 script 标签仅仅只是没有执行,他们仍然在 DOM 树中,所以 querySelector 在鲁棒性(划掉)上就比正则高出一截,代码如下:

<body>
  <div id="app">
    <p v-html="html"></p>
  </div>
  <script>
    var vm = new Vue({
      el: "#app",
      data: {
        html: '<script> console.log("JavaScript is awesome"); <\/script>',
      },
      created: function () {
        // 等待 DOM 刷新
        this.$nextTick(function () {
          let app = document.querySelector("#app");
          let scripts = Array.from(app.querySelectorAll("script"));
          scripts.forEach(function (script) {
            let node = document.createElement("SCRIPT");
            let src = script.getAttribute("src");
            if (src) {
              node.setAttribute("src", src);
            } else {
              node.innerHTML = script.innerHTML;
            }
            document.querySelector("body").appendChild(node);
          });
        });
      },
    });
  </script>
</body>

这样,我们就能顺利地在控制台里看到输出的字符串啦(

当然这样做也有他的缺点:

  1. 使用了全局的 JavaScript 环境。如果代码中出现了一些声明(比如用 let 定义了变量),或者是通过 new Audio() 之类的方式播放了音频,即使你在适当的时候将他们从 DOM 树中移除,他们产生的影响仍然将保留在当前的上下文中。我个人的解决方案是,使用一个 flag 来记录是否添加过 script,若添加过,则后续的路由操作将在 beforeEach 的钩子处被劫持,并将 fullPath 直接应用到 window.location.href 上,强制刷新当前页面,来获得一个全新的 JavaScript 上下文。
  2. 没有充分模拟 script 标签的特性。按照正常的网页加载模式,浏览器在加载和执行 script 标签时是阻塞的,顺序执行的。通过这种方式添加的 script 执行顺序并没有任何明确的规定,可能导致出现依赖上的问题。
Copyright © 2016-2023 ntzyz. All rights reserved.
Except where otherwise noted, content on this blog is licensed under CC-BY 2.0.