【问题标题】:What is the risk of global polyfills in an external script breaking the functionality of a website?外部脚本中的全局 polyfill 破坏网站功能的风险是什么?
【发布时间】:2019-08-10 00:04:01
【问题描述】:

@babel/polyfill 的文档有以下注释:

如果您正在寻找不会修改要在工具/库中使用的全局变量的东西,请查看 transform-runtime 插件。

transform-runtime 文档中,它说如下:

虽然这种 [@babel/polyfill 用法] 对于应用程序或命令行工具来说可能没问题,但如果您的代码是一个您打算发布给其他人使用的库,或者您不能完全发布,这就会成为问题控制代码运行的环境。

更一般地说,很多解释 polyfill 使用的文章都说,可能希望使用不同的解决方案如果你关心污染全局命名空间

据我了解,大多数 polyfill 都是有条件地加载的。如果一个实现已经存在,polyfill 不会覆盖它。我的问题是:在什么情况下,外部脚本中的 polyfill 会导致现有网站崩溃?到目前为止,我能够找到的唯一原因是外部脚本可能会比网站本身的代码更早地加载 polyfill。这可能会导致问题,但是当这些 polyfill 基于 Web 标准时,它们的行为应该是相同的。仍然存在严重冲突的可能性有多大?

我在github issue 上发现了一个有趣的讨论。不过,这里主要讨论的是 NPM 生态系统中的模块,而我最感兴趣的是促进小部件或嵌入之类的外部脚本。

感谢任何个人经验或有关该主题的讨论和文章的链接!

更新:这个问题的主要原因之一是转换运行时存在一些问题。随着 core-js 和 babel 的新版本,这些问题似乎已经得到解决。无论如何,我仍然对上述原始问题的答案感兴趣。

【问题讨论】:

    标签: javascript babeljs polyfills web-standards babel-polyfill


    【解决方案1】:

    嗯,polyfills 很少完美,正如你所说,它们几乎都是有条件的。

    假设 library-1 为一个名为 Interface 的功能注入了自己的 polyfill (polyfill-A)。
    这个polyfill-A可以很好的实现Interface完整API的几个方法,比如官方API可能是这样的

    interface Interface {
      constructor(optional (Interface or DOMString) foo);
      undefined doSomething();
      undefined doSomethingElse();
    };
    

    但是在构造函数中传递Interface 实例可能只是稍后才在规范中添加,或者doSomethingElse 可能已被该polyfill 省略,或者根本没有正确测试,所有这些小遗漏可能都很好library-1 因为他们不使用任何这些。
    现在,当 library-2 自己的 polyfill 将检查是否已经有一个 Instance 构造函数可用时,它会看到是的,它已经定义了,因此不会重新实现它。
    但是,library-2 可能需要在构造函数中传递一个接口,或者它可能需要调用其doSomethingElse() 方法。当它尝试这样做时,代码会崩溃,因为即使 library-2 的作者确实包含了一个可以正确实现这两个功能的 polyfill,library-1 的 polyfill 实现是可运行且可访问的。

    <script>
      // library-1.js
      (function polyfillInterface() {
        if (typeof Interface !== "function") {
          class Interface {
            constructor(foo) {
              this.foo = foo.toUpperCase();
            }
            doSomething() {
              return this.foo + "-bar";
            }
          }
          globalThis.Interface = Interface;
        }
      })();
      {
        // for library-1, everything works well
        const instance = new Interface("bla");
        console.log(instance.doSomething());
      }
    </script>
    
    <script>
      // library-2.js
      (function polyfillInterface() {
        if (typeof Interface !== "function") {
          class Interface {
            constructor(foo) {
              if (foo instanceof Interface) {
                this.foo = foo.foo;
              }
              else if (typeof foo === "string") {
                this.foo = foo.toUpperCase();            
              }
              else {
                throw new TypeError("neither an Interface nor a DOMSrting");
              }
            }
            doSomething() {
              return this.foo + "-bar";
            }
            doSomethingElse() {
              return this.foo.toLowerCase() + "-bar";
            }
          }
          globalThis.Interface = Interface;
        }
      })();
      {
        // for library-2, everything is broken
        const instance_1 = new Interface("bla");
        try {
          console.log(instance_1.doSomethingElse());
        }
        catch(err) {
          // instance_1.doSomethingElse is not a function
          console.error(err);
        }
        // TypeError: foo.toUpperCase is not a function
        const instance_2 = new Interface(instance_1);
      }
    </script>

    这可能是很难确定的事情,例如Promise.then() 应该在同一个事件循环中触发,而不是在它们解决的事件循环中(在微任务队列中),而不是像正常任务那样在下一个事件循环中触发,并且很多Promise 库可能一直在使用 setTimeout(fn, 0) 来实现异步性,而不是在可用时使用 MutationObserver

    这就是为什么在编写库时,最好链接到 polyfill,但不要自己包含它们。

    【讨论】:

    • 感谢您的回复。除了完全避免包含全局 polyfill 之外,您还可以采取其他措施来避免或尽量减少冲突的可能性吗?我遇到的一个问题是 webpack import() relies on Promise,并且不可能将你自己的 Promise 实现链接到 webpack runtime(至少不容易)
    • 在研究这个的同时,我还发现 Promise 是最常见的导致问题的 polyfill 之一。这可能是由于 Promises 的相对复杂性以及代码执行的顺序。你有使用相对更直接的 polyfill 方法的经验吗,比如 Object.assign 或 Array.includes?您认为这些内容是否“安全”?
    • @flut1 啊.. 对不起,我真的不喜欢 webpack。但是看到他们的文档要求您添加这样的 polyfill,我看不出这样做是如何不可能的。尽管如果还没有被问到,它可能需要自己的 Q/A。对于 Object.assign 和 Array.includes,它们很容易使用 es5 基础实现,但是像 Array.from 这样的其他方法几乎不可能填充,因为它们依赖于新概念。而且我敢肯定,即使是 Array.includes 在 github 上,您也可以找到有问题的 polyfill ;)所以这实际上取决于 polyfill 的内容以及由谁(有时是何时)。
    • 我仍在尝试使用 webpack 进行这项工作。目前看来,它似乎需要一些来自 webpack 外部的工具,比如对任何 webpack 输出进行后处理。我绝对同意它应该是可能的,所以如果我无法弄清楚,我会确保在 SO 或他们的 Github 上发布另一个问题。
    • @Rainning,我承认我的措辞不是那么清楚,我得稍后再谈(现在在这里有点晚了)。关键是lib1会加载它自己的polyfill版本,这个第一个polyfill可能只实现了部分想要的接口,例如,它可能实现了foo()而不是bar(),或者它可能不接受输入@ 987654332@ 在这些方法中。这对 lib1 来说可能不是问题,因为它们不需要bar() 或不通过baz。然而 lib2 可能需要这些,但它自己的 polyfill 会看到接口已经定义,所以它不会再次对其进行 polyfill。
    猜你喜欢
    • 2021-11-03
    • 2011-06-23
    • 1970-01-01
    • 2014-12-07
    • 2017-06-17
    • 2011-10-24
    • 2021-12-12
    • 2011-07-20
    • 1970-01-01
    相关资源
    最近更新 更多