JS 模块如何防止内部定义的自定义元素暴露其 API?

How do JS modules prevent custom elements defined inside from exposing their APIs?

这不起作用:

<!-- wtf.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title></title>
  <script type="module" src="./wtf.js"></script>
</head>
<body>
<script>
  const myElement = document.createElement('my-element')
  document.body.appendChild(myElement)
  myElement.callMe()
</script>
</body>
</html>

// wtf.js
customElements.define('my-element', class extends HTMLElement {
  constructor() {
    super()
  }

  callMe() {
    window.alert('I am called!')
  }
})

Firefox 在 myElement.callMe() 行向我抛出一个讨厌的异常。显然,"myElement.callMe is not a function".

我很困惑为什么会这样?据我了解,只要我输入 const myElement = document.createElement('my-element'),我就会收到一个对象,其类型不是通用 HTMLElement,而是我写的 class 的一个对象,它扩展了 HTMLElement !而这个 class 暴露了 callMe.

我已经确认我对模块的使用似乎是这里的罪魁祸首。此代码按预期工作:

<!-- wtf.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title></title>
</head>
<body>
<script>
  customElements.define('my-element', class extends HTMLElement {
      constructor() {
        super()
    }

    callMe() {
        window.alert('I am called!')
    }
  })

  const myElement = document.createElement('my-element')
  document.body.appendChild(myElement)
  myElement.callMe()
</script>
</body>
</html>

是的,我知道模块中定义的内容仅限于此模块。但在这里,它甚至(对我而言)似乎都不是一个范围界定问题。例如,如果我 在模块中 做类似的事情:

function callMe() {/*blah blah */}

window.callMe = callMe

那么我将能够在模块 之外使用 callMe 无论如何,因为模块通过 export 以外的其他方式公开了此功能(这通过将其分配给全局 window 对象来节省时间)。

据我了解,我的用例中也应该发生同样的情况。即使我在模块范围内的 class 中定义了 callMe,这个 class 方法应该可以在模块外部访问,因为它是一个对象的 属性通过调用 document.createElement('my-element') 公开的 class 的一部分。然而显然,这并没有发生。

这对我来说真的很奇怪。看起来好像模块是通过 与类型无关的函数纠缠来强制执行其作用域 return(!!) - 所以在这个在这种情况下,就好像模块神奇地强制 document.createElement 强制转换它在继承层次结构中 return 向上的对象(到 HTMLElement)?!?!这让我兴奋不已。

有人可以解开我的困惑吗?

(如果我在模块内定义自定义元素,我如何将其 API 暴露在此模块外?)

问题是 <script type="module"> implicitly has a defer attribute,所以它不会立即 运行。

Even though I define callMe in a class scoped to the module, this class method should be accessible outside of the module

是的,是的。问题只是它是异步定义的 :-) 要使用模块中的内容,您应该显式 import 该模块来声明依赖关系,以确保以正确的顺序对其进行评估。如果您的全局脚本不知何故 defer 红色,它也会起作用。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title></title>
  <script type="module" src="./wtf.js"></script>
</head>
<body>
  <script type="module">
    import './wtf.js';
//  ^^^^^^^^^^^^^^^^^^
    const myElement = document.createElement('my-element')
    document.body.appendChild(myElement)
    myElement.callMe()
  </script>
</body>
</html>

您还可以等待 window.onload 事件来执行您的内联脚本:

document.onload = () => {
  const myElement = document.createElement('my-element')
  document.body.appendChild(myElement)
  myElement.callMe()
}

或者,您可以使用 classic <script> 加载来确保同步加载您的自定义元素:

<script src="./wtf.js"></script>